Algoritmo de Recozimento Simulado: Teoria e Implementação Prática

Introdução ao Recozimento Simulado

O Recozimento Simulado (Simulated Annealing - SA) é um algoritmo metaheurístico e probabilístico utilizado para aproximar a solução ótima global em espaços de busca vastos e complexos. Diferente de algoritmos de busca local tradicioanis que podem ficar presos em ótimos locais (especialmente em funções multimodais), o SA introduz uma componente estocástica que permite a exploração de soluções subótimas temporárias, facilitando a fuga de mínimos ou máximos locais.

Fundamentos Físicos e Mapeamento Computacional

A inspiração para este algoritmo provém da termodinâmica, especificamente do processo de recozimento de metais. Na metalurgia, um material é aquecido a uma temperatura elevada, aumentando a energia interna e a desordem de seus átomos. Em seguida, o resfriamento é realizado de forma lenta e controlada, permitindo que a estrutura atômica se reorganize em um estado de mínima energia (equilíbrio termodinâmico).

Na computação, esse processo é mapeado da seguinte forma:

  • Temperatura: Controla a probabilidade de aceitar soluções piores. Inicia-se alta e decai gradualmente.
  • Energia: Representa o valor da função objetivo que se deseja minimizar (ou maximizar).
  • Estado: Uma configuração ou solução candidata no espaço de busca.

Mecanismo e Critério de Metropolis

A execução do algoritmo requer a definição de uma temperatura inicial T, tipicamente entre 103 e 105, e uma taxa de resfriamento α (ou fator de decaimento), geralmente entre 0.9 e 0.9995. A cada iteração, a temperatura é atualizada por T = T × α até atingir um limiar mínimo.

A transição entre estados segue o Critério de Metropolis. Seja ΔE a diferença de energia entre o novo estado e o estado atual (ΔE = Enovo - Eatual). Se o novo estado for melhor (ΔE < 0 para minimização), ele é aceito incondicionalmente. Caso contrário, ele é aceito com uma probabilidade P dada por:

P = e-ΔE / T

Um número aleatório r ∈ [0, 1) é gerado. Se P > r, a solução pior é aceita. Conforme a temperatura T diminui, a probabilidade de aceitar soluções piores decai exponencialmente, convergindo o algoritmo para uma busca puramente gulosa nas fases finais.

Estrutura Básica em Código

Abaixo está um modelo genérico da implementação do algoritmo em C++. As variáveis e a estrutura foram organizadas para clareza e reutilização.

#include <iostream>
#include <cmath>
#include <cstdlib>

// Gera um número pseudoaleatório no intervalo [0, 1)
double gerarProbabilidade() {
    return static_cast<double>(rand()) / RAND_MAX;
}

// Função objetivo: calcula a "energia" de um dado estado
double avaliarEnergia(double parametro1, double parametro2) {
    // Lógica de cálculo da função objetivo
    return 0.0; 
}

void recozimentoSimulado() {
    double temperatura = 10000.0;      // Temperatura inicial
    double temperaturaMinima = 1e-5;   // Critério de parada
    double taxaResfriamento = 0.995;   // Fator de decaimento
    
    double estadoAtual = 0.0;          // Solução inicial
    double melhorEnergia = avaliarEnergia(estadoAtual, 0.0);

    while (temperatura > temperaturaMinima) {
        // Gera um estado vizinho (a lógica depende do problema)
        double proximoEstado = estadoAtual + temperatura * (gerarProbabilidade() * 2 - 1);
        
        double energiaAtual = avaliarEnergia(estadoAtual, 0.0);
        double energiaProxima = avaliarEnergia(proximoEstado, 0.0);
        double deltaE = energiaProxima - energiaAtual;
        
        // Critério de Metropolis
        if (deltaE < 0 || exp(-deltaE / temperatura) > gerarProbabilidade()) {
            estadoAtual = proximoEstado;
            if (energiaProxima < melhorEnergia) {
                melhorEnergia = energiaProxima;
            }
        }
        
        temperatura *= taxaResfriamento;
    }
}

Estudo de Caso 1: Ponto de Equilíbrio de Toças

Considere um probleam onde n massas estão conectadas por cordas a um nó central sobre uma mesa, passando por orifícios. O objetivo é encontrar a posição de equilíbrio (x, y) do nó central que minimiza a energia potencial total do sistema. A energia do sistema é proporcional à soma das distâncias do nó até cada orifício, ponderada pela massa correspondente.

#include <iostream>
#include <vector>
#include <cmath>
#include <cstdlib>
#include <ctime>

using namespace std;

struct Massa {
    double x, y, peso;
};

int numMassas;
vector<Massa> objetos;
double melhorX, melhorY, menorEnergia;

double gerarAleatorio() {
    return static_cast<double>(rand()) / RAND_MAX;
}

double calcularEnergia(double cx, double cy) {
    double energiaTotal = 0.0;
    for (int i = 0; i < numMassas; ++i) {
        double dx = objetos[i].x - cx;
        double dy = objetos[i].y - cy;
        energiaTotal += sqrt(dx * dx + dy * dy) * objetos[i].peso;
    }
    if (energiaTotal < menorEnergia) {
        menorEnergia = energiaTotal;
        melhorX = cx;
        melhorY = cy;
    }
    return energiaTotal;
}

void executarSA() {
    double temp = 10000.0;
    double atualX = melhorX, atualY = melhorY;
    
    while (temp > 1e-6) {
        double novoX = atualX + temp * (gerarAleatorio() * 2 - 1);
        double novoY = atualY + temp * (gerarAleatorio() * 2 - 1);
        
        double delta = calcularEnergia(novoX, novoY) - calcularEnergia(atualX, atualY);
        
        if (exp(-delta / temp) > gerarAleatorio()) {
            atualX = novoX;
            atualY = novoY;
        }
        temp *= 0.9995;
    }
    
    // Refinamento final na vizinhança da melhor solução encontrada
    for (int i = 0; i < 5000; ++i) {
        double novoX = melhorX + temp * (gerarAleatorio() * 2 - 1);
        double novoY = melhorY + temp * (gerarAleatorio() * 2 - 1);
        calcularEnergia(novoX, novoY);
    }
}

int main() {
    srand(time(nullptr));
    cin >> numMassas;
    objetos.resize(numMassas);
    
    double somaX = 0, somaY = 0;
    for (int i = 0; i < numMassas; ++i) {
        cin >> objetos[i].x >> objetos[i].y >> objetos[i].peso;
        somaX += objetos[i].x;
        somaY += objetos[i].y;
    }
    
    melhorX = somaX / numMassas;
    melhorY = somaY / numMassas;
    menorEnergia = calcularEnergia(melhorX, melhorY);
    
    executarSA();
    
    printf("%.3f %.3f\n", melhorX, melhorY);
    return 0;
}

Estudo de Caso 2: Problema do Clique Máximo

Em um grafo não direcionado, o objetivo é encontrar o maior subconjunto de vértices onde todos são adjacentes entre si (Clique Máximo). Para aplicar o SA, o espaço de busca é definido como o conjunto de todas as permutações dos vértices. A função de avaliação percorre a permutação e adiciona vértices ao clique desde que sejam adjacentes a todos os já selecionados. A aleatoriedade na permutação permite explorar diferentes formações de cliques.

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <ctime>

using namespace std;

int numVertices;
bool matrizAdj[60][60];
int permutacaoAtual[60], melhorPermutacao[60];
int tamanhoMaximo = 0;

int gerarIndice(int limite) {
    return rand() % limite;
}

double gerarProbabilidade() {
    return static_cast<double>(rand()) / RAND_MAX;
}

int avaliarClique() {
    int tamanho = 0;
    vector<int> clique;
    
    for (int i = 0; i < numVertices; ++i) {
        int v = permutacaoAtual[i];
        bool podeAdicionar = true;
        for (int u : clique) {
            if (!matrizAdj[v][u]) {
                podeAdicionar = false;
                break;
            }
        }
        if (podeAdicionar) {
            clique.push_back(v);
            tamanho++;
        }
    }
    return tamanho;
}

void otimizarSA() {
    double temp = 1.0;
    for (int i = 0; i < numVertices; ++i) {
        permutacaoAtual[i] = melhorPermutacao[i];
    }
    
    while (temp > 1e-8) {
        int idx1 = gerarIndice(numVertices);
        int idx2 = gerarIndice(numVertices);
        swap(permutacaoAtual[idx1], permutacaoAtual[idx2]);
        
        int tamanhoAtual = avaliarClique();
        int delta = tamanhoAtual - tamanhoMaximo;
        
        if (delta > 0) {
            tamanhoMaximo = tamanhoAtual;
            for (int i = 0; i < numVertices; ++i) {
                melhorPermutacao[i] = permutacaoAtual[i];
            }
        } else if (exp(delta / temp) > gerarProbabilidade()) {
            // Mantém a mudança (não reverte)
        } else {
            swap(permutacaoAtual[idx1], permutacaoAtual[idx2]); // Reverte a mudança
        }
        temp *= 0.95;
    }
}

int main() {
    srand(time(nullptr));
    cin >> numVertices;
    
    int u, v;
    while (cin >> u >> v) {
        matrizAdj[u][v] = matrizAdj[v][u] = true;
    }
    
    for (int i = 0; i < numVertices; ++i) {
        melhorPermutacao[i] = i + 1;
    }
    
    // Execução contínua dentro do limite de tempo
    clock_t inicio = clock();
    while (clock() - inicio < 0.95 * CLOCKS_PER_SEC) {
        otimizarSA();
    }
    
    cout << tamanhoMaximo << endl;
    return 0;
}

Otimização Baseada em Tempo de Execução

Em problemas competitivos ou com restrições rígidas de tempo, executar o algoritmo uma única vez pode não ser suficiente para garantir a convergência para o ótimo global. Uma técnica eficaz é encapsular a execução do SA em um loop controlado por tempo. Utilizando a função clock(), o algoritmo é reiniciado múltiplas vezes até que o tempo limite (por exemplo, 95% do tempo total permitido) seja atingido. Isso maximiza o número de explorações do espaço de busca sem exceder o tempo limite da aplicação.

Tags: simulated-annealing Algoritmos Otimização cpp metaheuristicas

Publicado em 6-2 20:37 por Thomas