E aí, galera da programação! 👋 Hoje, vamos embarcar em uma aventura que une a beleza da otimização com a praticidade do desenvolvimento Java. Afinal, quem não gosta de ver um problema complexo resolvido com elegância? Vamos criar um programa que resolve o clássico “Problema do Caixeiro Viajante” usando um algoritmo genético, tudo isso com uma interface gráfica feita em Swing. Parece desafiador? Pois acredite, é mais divertido do que você imagina!
Por que o problema do caixeiro viajante?
O “Problema do Caixeiro Viajante” (PCV) é um desafio famoso na ciência da computação. Imagine um vendedor que precisa visitar várias cidades e quer encontrar o caminho mais curto para percorrer todas elas. Ou seja, o PCV busca o circuito de menor distância que passa por todos os pontos dados uma única vez. Esse problema, aparentemente simples, torna-se extremamente complexo à medida que o número de pontos aumenta, sendo classificado como NP-difícil.
E por que algoritmo genético?
Nesse sentido, é que os algoritmos genéticos entram em cena. Eles são inspirados na seleção natural e evolução biológica. Assim, em vez de tentar todas as combinações possíveis (o que seria inviável), o algoritmo genético cria uma “população” de soluções (rotas), simula um processo evolutivo para aprimorar as rotas e ao fim, encontra a melhor rota para nosso caixeiro viajante. É uma abordagem inteligente para problemas de otimização, já que funciona muito bem em problemas complexos como o PCV.
Mão na massa: Implementação em Java com Swing
Vamos ao código! Primeiramente, dividiremos o problema em classes seguindo os princípios SOLID, para deixar tudo organizado e fácil de manter.
Classe Point (representando os pontos)
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double distance(Point other) {
double dx = this.x - other.x;
double dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
Classe Path (representando o caminho)
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Path {
private List<Point> points;
private double distance = 0;
public Path(List<Point> points) {
this.points = new ArrayList<>(points);
calculateDistance();
}
public void setPoints(List<Point> points) {
this.points = new ArrayList<>(points);
calculateDistance();
}
public List<Point> getPoints() {
return points;
}
public double getDistance() {
return distance;
}
public void calculateDistance() {
distance = 0;
for (int i = 0; i < points.size() - 1; i++) {
distance += points.get(i).distance(points.get(i + 1));
}
distance += points.get(points.size() - 1).distance(points.get(0));
}
public void swapPoints(int index1, int index2){
Collections.swap(points, index1, index2);
calculateDistance();
}
}
Classe GeneticAlgorithm (implementando o algoritmo)
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
public class GeneticAlgorithm {
private int populationSize;
private double mutationRate;
private int generations;
private List<Point> points;
private Random random;
public GeneticAlgorithm(List<Point> points, int populationSize, double mutationRate, int generations) {
this.points = points;
this.populationSize = populationSize;
this.mutationRate = mutationRate;
this.generations = generations;
this.random = new Random();
}
public Path run() {
List<Path> population = initializePopulation();
for (int i = 0; i < generations; i++) {
population = evolvePopulation(population);
}
population.sort(Comparator.comparingDouble(Path::getDistance));
return population.get(0);
}
private List<Path> initializePopulation() {
List<Path> population = new ArrayList<>();
for (int i = 0; i < populationSize; i++) {
List<Point> shuffledPoints = new ArrayList<>(points);
Collections.shuffle(shuffledPoints, random);
population.add(new Path(shuffledPoints));
}
return population;
}
private List<Path> evolvePopulation(List<Path> population) {
List<Path> nextGeneration = new ArrayList<>();
population.sort(Comparator.comparingDouble(Path::getDistance));
for (int i = 0; i < populationSize / 2 ; i++) {
nextGeneration.add(population.get(i));
}
while(nextGeneration.size() < populationSize) {
int index1 = random.nextInt(populationSize/2);
int index2 = random.nextInt(populationSize/2);
Path parent1 = population.get(index1);
Path parent2 = population.get(index2);
Path child = crossover(parent1, parent2);
mutate(child);
nextGeneration.add(child);
}
return nextGeneration;
}
private Path crossover(Path parent1, Path parent2) {
List<Point> childPoints = new ArrayList<>();
int start = random.nextInt(parent1.getPoints().size());
int end = random.nextInt(parent1.getPoints().size());
if (start > end) {
int temp = start;
start = end;
end = temp;
}
for (int i = start; i <= end; i++) {
childPoints.add(parent1.getPoints().get(i));
}
for (Point point : parent2.getPoints()) {
if (!childPoints.contains(point)) {
childPoints.add(point);
}
}
return new Path(childPoints);
}
private void mutate(Path path){
if (random.nextDouble() < mutationRate) {
int index1 = random.nextInt(path.getPoints().size());
int index2 = random.nextInt(path.getPoints().size());
path.swapPoints(index1, index2);
}
}
}
A classe GeneticAlgorithm é a responsável por encapsular a lógica do algoritmo genético para resolver o Problema do Caixeiro Viajante (PCV). Ela é o motor que cria, evolui e encontra a melhor rota através de um processo de “seleção natural” simulado.
Atributos Principais
- populationSize: Um inteiro que define o tamanho da população, ou seja, quantas rotas (caminhos) serão consideradas em cada geração. Uma população maior pode explorar mais soluções, mas também aumenta o custo computacional.
- mutationRate: Um valor double (entre 0 e 1) que determina a probabilidade de ocorrer mutação em um indivíduo. A mutação é uma pequena alteração aleatória que pode gerar diversidade na população, evitando que ela fique presa em soluções subótimas.
- generations: Um inteiro que define quantas gerações o algoritmo genético irá rodar. Quanto mais gerações, mais tempo o algoritmo terá para evoluir e encontrar melhores soluções.
- points: Uma lista de objetos Point que representam as cidades que precisam ser visitadas.
- random: Um objeto da classe Random, utilizado para geração de números aleatórios.
Métodos Chave
- public Path run():
- Propósito: Este é o método que inicia e executa todo o processo do algoritmo genético.
- Funcionamento:
- Ele primeiro chama initializePopulation() para criar a população inicial de rotas.
- Em seguida, ele itera através das gerações (for (int i = 0; i < generations; i++)). Em cada geração:
- A função evolvePopulation é chamada, simulando a reprodução e evolução da população.
- Finalmente, ele ordena a população pelo custo (distância) e retorna a melhor rota encontrada.
- private List<Path> initializePopulation():
- Propósito: Cria a população inicial de rotas de forma aleatória.
- Funcionamento:
- Para cada indivíduo da população, ele cria uma lista de Point e aleatoriamente embaralha a ordem dos pontos e armazena dentro do objeto Path.
- Essas rotas aleatórias representam os primeiros indivíduos (caminhos) que serão avaliados pelo algoritmo genético.
- private List<Path> evolvePopulation(List<Path> population):
- Propósito: Simula a evolução da população atual para a próxima geração.
- Funcionamento:
- Primeiro, ordena a população atual de rotas, com base na sua distância total (do melhor para o pior).
- Então, seleciona a metade melhor da população (os “pais”).
- Depois, cria uma nova geração usando reprodução.
- Para cada filho, executa crossover (recombinação) com pais diferentes.
- A mutação pode ser aplicada no indivíduo filho de forma aleatória.
- O método continua este processo até que o tamanho da nova população seja o mesmo da população anterior.
- private Path crossover(Path parent1, Path parent2):
- Propósito: Combina material genético (rotas) de dois pais para criar um filho (nova rota).
- Funcionamento:
- Escolhe um intervalo aleatório de pontos do primeiro pai.
- Adiciona esse intervalo ao filho.
- Adiciona os pontos restantes do segundo pai, mantendo a ordem original.
- Retorna um novo objeto Path (o filho).
- private void mutate(Path path):
- Propósito: Aplica uma mutação a uma rota com uma determinada probabilidade (determinada pelo mutationRate).
- Funcionamento:
- Verifica a probabilidade do path atual sofrer mutação
- Se a probabilidade for alta, a mutação troca a posição de dois pontos do caminho de forma aleatória.
Entendendo a Lógica do Algoritmo Genético na Classe
A ideia principal é criar uma “população” de soluções (rotas) aleatórias, avaliá-las (pela distância total), e fazer com que as soluções melhores se reproduzam (através do crossover) e sofram pequenas alterações (através da mutação), ao longo de várias gerações. Dessa forma, o algoritmo tenta convergir para a melhor solução possível sem ter que avaliar todos os caminhos possíveis.
- Inicialização: Criar rotas aleatórias para começar o processo evolutivo.
- Seleção: Escolher as rotas de melhor desempenho para se reproduzirem.
- Crossover: Criar novos caminhos, combinando informações de caminhos existentes.
- Mutação: Introduzir variedade nas rotas, evitando que o algoritmo fique preso em soluções ruins.
- Iteração: Repetir esse processo por várias gerações até encontrar uma solução que seja satisfatória.
Em resumo, a classe GeneticAlgorithm encapsula a inteligência do algoritmo genético, guiando a busca pela melhor rota através da simulação de um processo evolutivo. Cada método dentro dessa classe tem um propósito claro e bem definido para fazer essa busca funcionar de forma eficaz.
Classe Main (Interface Swing)
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Main extends JFrame {
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
private static final int POINT_SIZE = 10;
private static final double MUTATION_RATE = 0.01;
private static final int POPULATION_SIZE = 100;
private static final int GENERATIONS = 500;
private JPanel panel;
private JButton generatePointsButton;
private JButton findPathButton;
private JTextField numberOfPointsField;
private List<Point> points;
private Path bestPath;
public Main() {
setTitle("Problema do Caixeiro Viajante");
setSize(WIDTH, HEIGHT);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(null);
initComponents();
}
private void initComponents() {
panel = new JPanel(){
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
drawPoints(g);
if(bestPath != null){
drawPath(g);
}
}
};
panel.setLayout(null);
panel.setBackground(Color.WHITE);
panel.setBounds(0,100,WIDTH,HEIGHT-100);
add(panel);
numberOfPointsField = new JTextField("5");
numberOfPointsField.setBounds(10, 20, 50, 30);
add(numberOfPointsField);
generatePointsButton = new JButton("Gerar Pontos");
generatePointsButton.setBounds(70, 20, 120, 30);
generatePointsButton.addActionListener(this::generatePoints);
add(generatePointsButton);
findPathButton = new JButton("Encontrar Melhor Rota");
findPathButton.setBounds(200, 20, 170, 30);
findPathButton.addActionListener(this::findBestPath);
add(findPathButton);
}
private void generatePoints(ActionEvent e) {
points = new ArrayList<>();
int numberOfPoints = Integer.parseInt(numberOfPointsField.getText());
Random random = new Random();
for (int i = 0; i < numberOfPoints; i++) {
double x = random.nextDouble() * (WIDTH - POINT_SIZE);
double y = random.nextDouble() * (HEIGHT - 100 - POINT_SIZE);
points.add(new Point(x,y));
}
bestPath = null;
panel.repaint();
}
private void findBestPath(ActionEvent e) {
if (points == null || points.isEmpty())
return;
GeneticAlgorithm ga = new GeneticAlgorithm(points, POPULATION_SIZE, MUTATION_RATE, GENERATIONS);
bestPath = ga.run();
new Thread(() -> {
animatePath();
}).start();
}
private void animatePath() {
List<Point> pathPoints = bestPath.getPoints();
for (int i = 0; i < pathPoints.size() - 1; i++) {
bestPath.setPoints(pathPoints.subList(0, i + 2));
panel.repaint();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
bestPath.setPoints(pathPoints);
panel.repaint();
}
private void drawPoints(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
if (points != null) {
g2d.setColor(Color.BLACK);
for (Point point : points) {
Ellipse2D circle = new Ellipse2D.Double(point.getX(), point.getY(), POINT_SIZE, POINT_SIZE);
g2d.fill(circle);
}
}
}
private void drawPath(Graphics g){
if (bestPath != null) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.RED);
List<Point> pathPoints = bestPath.getPoints();
for(int i = 0; i < pathPoints.size() - 1; i++) {
Point p1 = pathPoints.get(i);
Point p2 = pathPoints.get(i + 1);
Line2D line = new Line2D.Double(p1.getX() + POINT_SIZE/2 , p1.getY()+ POINT_SIZE/2 , p2.getX() + POINT_SIZE/2 , p2.getY() + POINT_SIZE/2);
g2d.draw(line);
}
Point p1 = pathPoints.get(pathPoints.size() - 1);
Point p2 = pathPoints.get(0);
Line2D line = new Line2D.Double(p1.getX() + POINT_SIZE/2 , p1.getY()+ POINT_SIZE/2 , p2.getX() + POINT_SIZE/2 , p2.getY() + POINT_SIZE/2);
g2d.draw(line);
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new Main().setVisible(true));
}
}
O que cada parte faz
- Point: Representa um ponto com coordenadas x e y.
- Path: Representa um caminho, um sequência de pontos e sua distância total.
- GeneticAlgorithm: Implementa o algoritmo genético, com métodos para inicializar a população, evoluir através das gerações e calcular a aptidão dos indivíduos.
- Main: A classe principal, cria a interface gráfica usando Swing e orquestra o algoritmo genético.
Como o programa funciona
- O usuário digita a quantidade de pontos e clica em “Gerar Pontos”.
- O programa cria pontos aleatórios na tela e os exibe.
- O usuário clica em “Encontrar Melhor Rota”.
- O algoritmo genético entra em ação, evoluindo rotas até encontrar uma solução otimizada.
- Assim que o melhor caminho for encontrado, uma animação exibe a rota sendo percorrida ponto a ponto.

Considerações Finais
Este é um exemplo básico, mas ele ilustra bem como aplicar um algoritmo genético para resolver problemas de otimização complexos de forma visualmente intuitiva. Ademais, o código está bem estruturado, seguindo os padrões SOLID, o que facilita futuras expansões e manutenções.
Espero que tenha gostado deste post! Deixe seus comentários e sugestões, e vamos continuar explorando o mundo da programação juntos! 😉