terça-feira, 8 de julho de 2008

Algoritmo para Quebra-Cabeça

Este exemplo mostra um algorítmo para resolver aqueles quebra-cabeças formados por blocos de numeros, onde é possivel mover uma peça de cada vez e o objetivo é deixar os numeros alinhados (1, 2, 3, 4, .... ):

+---+---+---+
+ 1 + 2 + 3 +
+---+---+---+
+ 4 + 5 + 6 +
+---+---+---+
+ 7 + 8 + +
+---+---+---+

O segredo deste algorítmo esta na definição da função heuristica que irá determinar um valor para cada estado do tabuleiro. O objetivo é maximizar este valor, isto é, a solução será encontrada quando o valor for 8.
A função calcula a quantidade de números que estão alinhados com a solução.
Por exemplo, se tivermos um tabuleiro na seguinte configuração:

+---+---+---+
+ 4 + 2 + 3 +
+---+---+---+
+ 1 + 5 + +
+---+---+---+
+ 7 + 8 + 6 +
+---+---+---+

teremos como resultado 5 pois os numeros 2, 3, 5, 7 e 8 estão posicionados corretamente.
O algoritmo irá efetuar uma pesquisa numa arvore de solução onde a cada iteração estará expandindo a arvore e depois ordenando pelos melhores resultados e estes terão preferencia de processamento. Existe uma coleção de estados processados onde é verificado se o novo estado já foi processado e caso afirmativo é eliminado da fila de processamento.

O problema inicial foi configurado como:

+---+---+---+
+ 8 + 4 + 0 +
+---+---+---+
+ 5 + 1 + 7 +
+---+---+---+
+ 6 + 2 + 3 +
+---+---+---+

O numero '0' é o calhau, isto é, o espaço vazio onde no exemplo acima o 4 pode ser movido para a direita ou o 7 movido para cima.
Cada novo passo tem um novo custo de 1.0 ponto.
Após rodar o algoritmo, a solução foi encontrada e foi necessário expandir 1013 nós e o custo da solução ficou em 130. Expandir 1013 nós significa que o algoritmo produziu 1013 configurações de tabuleiros para encontrar a solução e que 130 seriam suficientes, para este algoritmo, encontrar a solução.
Evidente que ainda não estamos falando de Inteligencia Artificial mas sim de pesquisa operacional. O codigo a seguir pode ser melhorado dando a capacidade do algorítmo aprender com a experiencia mas isso será assunto para discutir no próximo artigo.
A função heuristica pode ser melhorada também, estarei fazendo isso em breve e publicarei uma função que agilizará a solução do problema.

Voce podera alterar a configuração inicial para outra qualquer, basta alterar a matriz "inicio". Poderá aumentar o tamanho do tabuleiro para uma dimensao maior. Para isso altere a matriz inicio, a matriz solucao e a constante dimensao.

Vamos ao código:


import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Set;

/**
* @author Ricardo Alberto Harari - ricardo.harari@gmail.com
*
*/
public class QuebraCabeca {

static int CIMA = 1;
static int BAIXO = 2;
static int DIREITA = 3;
static int ESQUERDA = 4;

static boolean printOn = true;

static int tamanho_fila = 0;
static int tamanho_fila_max = 0;

static double custo_solucao = 0.0;

static int nos_expandidos = 0;

static int dimensao = 3;

static int[] solucao = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 0 };

static int[] inicio = new int[] { 8, 4, 0, 5, 1, 7, 6, 2, 3 };

static PriorityQueue fila = new PriorityQueue(5, new ComparadorNo());
static Set processados = new HashSet();

public static void main(String[] args) {
long l = System.currentTimeMillis();
No no = iniciarBusca(inicio);
if (no != null) {
imprimirResultado(no, true);
} else {
System.out.println("SOLUCAO NAO ENCONTRADA!");
}
System.out.println("\r\n\r\nElapsed time=" + (System.currentTimeMillis() - l) + "ms");
}

/**
* efetua a busca da solucao retorna a solucao ou em caso de nao encontrar null
*
* @param posicoesIniciais
* @return
*/
private static No iniciarBusca(int[] posicoesIniciais) {
No noInicial = new No();
noInicial.estado = posicoesIniciais;
fila.add(noInicial);
tamanho_fila = 1;
while (!(fila.isEmpty())) {
tamanho_fila--;
No no = fila.remove();
if (goal(no.estado)) {
custo_solucao = no.custoCaminho;
return no;
}
adicionarNosAlternativosFila(no);
tamanho_fila = fila.size();
if (tamanho_fila_max < tamanho_fila) tamanho_fila_max = tamanho_fila; // estatistica
}
return null; // falhou
}

/**
* imprimir o resultado da solucao
*
* @param no
* @param imprimeTabuleiros
*/
private static void imprimirResultado(No no, boolean imprimeTabuleiros) {
print("ESTADO FINAL:");
print("===============");
imprimirTabuleiro(no);
print("===============");
print("tamanho_fila_max = " + tamanho_fila_max);
print("nos_expandidos = " + nos_expandidos);
print("===============");
print("OPERACOES REVERSA:");

while (no.pai != null) {
print("===============");
print("Step: " + no.step);
print("Custo:" + no.custoCaminho);
print("Acao:" + getAcaoReversa(no.acao));
no = no.pai;
if (imprimeTabuleiros) imprimirTabuleiro(no);
}
}

/**
* retorna o nome da acao, nao do calhau, mas do bloco a ser movido
*
* @param acao
* @return
*/
private static String getAcaoReversa(int acao) {
switch (acao) {
case 1:
return "BAIXO";
case 2:
return "CIMA";
case 3:
return "ESQUERDA";
case 4:
return "DIREITA";
}
return "NENHUMA";
}

/**
* @param no
*/
private static void imprimirTabuleiro(No no) {
if (!printOn) return;
for (int i = 0; i < dimensao; i++) {
imprimirTabuleiroLinha();
for (int j = 0; j < dimensao; j++) {
int n = i * dimensao + j;
System.out.print("+ " + no.estado[n] + " ");
}
System.out.println("+");
}
imprimirTabuleiroLinha();
}

/**
* imprime uma linha separadora do tabuleiro
*/
private static void imprimirTabuleiroLinha() {
if (!printOn) return;
for (int i = 0; i < dimensao; i++) {
System.out.print("+---");
}
System.out.println("+");
}

/**
* imprime 's'
*
* @param s
*/
private static void print(String s) {
if (printOn) System.out.println(s);

}

/**
* esta eh a chave do problema, determinar uma fucao heuristica que retorne
*
* o quanto esta perto da solucao neste problema a heuristica eh calculada
*
* verificando quantos blocos estao na posicao correta - isto eh, de acordo
*
* com a matriz 'solucao'
*
* @param estado
* @return
*/
private static double heuristica(int[] estado) {
double valor = 0.0;
for (int i = 0; i < dimensao; i++) {
for (int j = 0; j < dimensao; j++) {
int n = i * dimensao + j;
valor += estado[n] == solucao[n] ? 1 : 0;
}
}
return valor;
}

private static boolean goal(int[] estado) {
for (int i = 0; i < dimensao * dimensao; i++) {
if (estado[i] != solucao[i]) return false;
}
return true;
}

private static List recuperarSucessores(int[] estado) {
List filhos = new ArrayList();
if (podeMoverCalhau(estado, CIMA)) {
int[] novoEstado = clonar(estado);
moverCima(novoEstado);
filhos.add(new Sucessor(novoEstado, CIMA));
}
if (podeMoverCalhau(estado, ESQUERDA)) {
int[] novoEstado = clonar(estado);
moverEsquerda(novoEstado);
filhos.add(new Sucessor(novoEstado, ESQUERDA));
}
if (podeMoverCalhau(estado, DIREITA)) {
int[] novoEstado = clonar(estado);
moverDireita(novoEstado);
filhos.add(new Sucessor(novoEstado, DIREITA));
}
if (podeMoverCalhau(estado, BAIXO)) {
int[] novoEstado = clonar(estado);
moverBaixo(novoEstado);
filhos.add(new Sucessor(novoEstado, BAIXO));
}
return filhos;
}

private static void moverCima(int[] estado) {
int pos = 0;
for (int i = dimensao; i < dimensao * dimensao; i++) {
if (estado[i] == 0) {
pos = i;
break;
}
}
if (pos > 0) {
int valor = estado[pos - dimensao];
estado[pos - dimensao] = 0;
estado[pos] = valor;
}
}

private static void moverBaixo(int[] estado) {
int pos = 0;
for (int i = 0; i < dimensao * dimensao; i++) {
if (estado[i] == 0) {
pos = i;
break;
}
}
int valor = estado[pos + dimensao];
estado[pos + dimensao] = 0;
estado[pos] = valor;
}

private static void moverEsquerda(int[] estado) {
int pos = 0;
for (int i = 0; i < dimensao * dimensao; i++) {
if (estado[i] == 0) {
pos = i;
break;
}
}
int valor = estado[pos - 1];
estado[pos - 1] = 0;
estado[pos] = valor;
}

private static void moverDireita(int[] estado) {
int pos = 0;
for (int i = 0; i < dimensao * dimensao; i++) {
if (estado[i] == 0) {
pos = i;
break;
}
}
int valor = estado[pos + 1];
estado[pos + 1] = 0;
estado[pos] = valor;
}

/**
* expandir e adicionar nós subsequentes na fila
*
* @param no
*/
private static void adicionarNosAlternativosFila(No no) {
if (!(processados.contains(no.toString()))) {
processados.add(no.toString());
List expandidos = expandirNos(no);
for (No o : expandidos) {
fila.add(o);
}
}
}

/**
* retorna um estado clonado
*
* @param estado
* @return
*/
private static int[] clonar(int[] estado) {
int[] ret = new int[estado.length];
for (int i = 0; i < estado.length; i++) {
ret[i] = estado[i];
}
return ret;
}

/**
* expandir o no com os seus sucessores possiveis
*
* @param no
* @return
*/
private static List expandirNos(No no) {
List nos = new ArrayList();
List proximos = recuperarSucessores(no.estado);
for (Sucessor prox : proximos) {
No no0 = new No();
no0.pai = no;
no0.estado = prox.estado;
no0.step = no.step + 1;
no0.acao = prox.acao;
// o custo é sempre 1 pois movemos 1 bloco a cada passo
no0.custoStep = 1.0;
no0.custoCaminho += no0.pai.custoCaminho + 1.0;
nos.add(no0);
// if (!processados.contains(no0.toString())) nos.add(no0);
}
nos_expandidos++;
return nos;
}

/**
* verifica se o calhau pode ser movido para uma direcao(acao)
*
* @param estado
* @param acao
* @return
*/
private static boolean podeMoverCalhau(int[] estado, int acao) {
int posicao = 0;
for (int i = 0; i < dimensao * dimensao; i++) {
if (estado[i] == 0) {
posicao = i;
break;
}
}
if (acao == ESQUERDA) {
while (posicao >= 0) {
if (posicao == 0) return false;
posicao -= dimensao;
}
} else if (acao == CIMA) {
if (posicao >= 0 && posicao < dimensao) return false;
} else if (acao == DIREITA) {
posicao++;
while (posicao >= dimensao) {
if (posicao == dimensao) return false;
posicao -= dimensao;
}
} else if (acao == BAIXO) {
if (posicao >= dimensao * (dimensao - 1)) return false;
}
return true;
}

/**
* representa um estado sucessor do estado atual dada uma acao
*
* @author Ricardo A Harari - ricardo.harari@gmail.com
*
*/
static class Sucessor {
public Sucessor(int _estado[], int _acao) {
estado = _estado;
acao = _acao;
}

public int[] estado;
public int acao;
}

/**
* representa um nó de um grafo possui o estado, o nó pai,
*
* a acao, o custo para chegar até este nó, a profundidade do nó
*
* e o custo unitário para ir do pai até o filho
*
* @author Ricardo A Harari - ricardo.harari@gmail.com
*
*/
static class No {
public int[] estado;
public int acao;
public No pai;
public int step = 0;
public double custoCaminho = 0.0;
public double custoStep = 0.0;

public List recuperarArvore() {
No atual = this;
List ret = new ArrayList();
while (!(atual.pai != null)) {
ret.add(0, atual);
atual = atual.pai;
}
ret.add(0, atual);
return ret;
}

@Override
public String toString() {
String ret = "";
for (int i = 0; i < dimensao * dimensao; i++) {
ret += estado[i];
}
return ret;
}

@Override
public boolean equals(Object o) {
if ((o == null) || (this.getClass() != o.getClass())) return false;
if (this == o) return true;
No x = (No) o;
for (int i = 0; i < dimensao; i++) {
if (estado[i] != x.estado[i]) {
return false;
}
}
return true;
}
}

/**
* comparador de Nós chamando a heuristica
*
* @author Ricardo A Harari - ricardo.harari@gmail.com
*
*/
static class ComparadorNo implements Comparator {
/*
* (non-Javadoc)
*
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
@Override
public int compare(No o1, No o2) {
double d1 = heuristica(o1.estado);
double d2 = heuristica(o2.estado);
return (int) (d2 - d1);
}
}

}

segunda-feira, 7 de julho de 2008

Algoritmo Genetico - Considerações

Existe um problema conceitual na implementação do problema anterior das 8 rainhas que é o seguinte:

- no algoritmo genético, a probabilidade de sobrevivencia dos individuos é proporcional ao seu fitness. Eu nao impementlei como probabilidade mas sim como certo, sempre pegando os mais adaptados.

O método selecionarAleatorio poderia ser otimizado tambem, para considerar os mais adaptados em serem selecionados com maior probabilidade.

Isto pode gerar a uma situação onde a resolução demore muito, mais que o previsto ou até mesmo que não encontre a solução, após milhares de iterações.

Coloco aqui então uma outra abordagem que me ocorreu. A idéia central é aumentar a probabilidade de mutação caso o fitness do melhor elemento não aumente apos 1mil iteracoes (poderia considerar o fitness global tambem). Depois de 2mil iteracoes a probabilidade de mutação aumenta ainda mais.

Outra idéia é tambem fazer com que somente 2 dos melhores pais sobrevivam e 2 dos piores filhos sejam eliminados. Assim garantimos que outros pais tambem possam disseminar seus genes, mesmo não sendo tão competitivos no momento.

Ainda, ocorrendo mais de 5mil iterações e não atingindo a solução ótima, toda população é eliminada e uma nova é gerada.

Pensando na linha darwinista podemos traduzir que se uma população não se adaptar rapidamente ao ambiente então poderá ser dizimada e uma nova população tomará o seu lugar. Os parametros de quando isso deve ocorrer dependerá do tipo de problema.
Uma metáfora: se não encontrarmos uma maneira de viver em outros planetas poderemos ser solapados por um meteoro ou por mudanças climáticas.
A opção por manter alguns da espécie nesta matança pode ser uma opção tambem mas não foi implementada.

Outra mudança no algoritmo abaixo é que o filho irá sofrer mutação somente se o seu fitness for menor ou igual que o maior fitness.

Após 2mil testes, com estas considerações, a média de iterações foi de 3096,98 o que é considerado satisfatório e não ocorreu nenhuma falha.

Abaixo o algoritmo modificado:



import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
* @author Ricardo Alberto Harari - ricardo.harari@gmail.com
*
*
*/
public class OitoRainhas {

private static final int MAX_FITNESS = 8;
private static Random randomico = new Random();

// parametros de stress test
private static boolean ocorreuFalha = false;
private static int totalMaxIteracoes = 0;
private static int totalIteracoes = 0;
private static boolean disablePrint = false;

public static void main(String[] args) {
// disablePrint = true;
// for (int i = 0; i <>
go();
// System.out.println("i=" + i);
// }
// System.out.println("------- MAX ITERACOES=" + totalMaxIteracoes);
// System.out.println("-------MEDIA ITERACOES="
// + (totalIteracoes / 2000.0));
// System.out.println("------- OCORREU FALHA?" + ocorreuFalha);
}

/**
* inicio do processo
*/
private static void go() {
long startTime = System.currentTimeMillis();
println("Populacao Inicial");

// gerar a populacao inicial com 10 individuos
Set populacao = new HashSet();
carregarPopulacao(populacao);
println("------------------------------");

double probabilidadeMutacao = 0.15;
int maxIteracoes = 300000;

String melhorIndividuo = null;
boolean achouSolucao = false;
int bestFitness = 0;

int i = 0;
int counter = 0;

for (i = 0; i <>
melhorIndividuo = geneticAlgorithm(populacao, probabilidadeMutacao,
bestFitness);
int ftness = fitness(melhorIndividuo);
if (ftness > bestFitness) {
probabilidadeMutacao = 0.10;
counter = 0;
println("novo fitness = " + ftness);
bestFitness = ftness;
if (ftness == MAX_FITNESS) {
achouSolucao = true;
break;
}
} else {
counter++;
if (counter > 1000) {
probabilidadeMutacao = 0.30;
} else if (counter > 2000) {
probabilidadeMutacao = 0.50;
} else if (counter > 5000) {
populacao.clear();
carregarPopulacao(populacao);
probabilidadeMutacao = 0.10;
bestFitness = -1;
}
}
}

println("------------------------------");

if (achouSolucao) {
println("Solucao encontrada em " + i + " iteracoes");
println("Solucao =" + melhorIndividuo);
println("Fitness =" + fitness(melhorIndividuo));
} else {
System.out.println("Solucao nao encontrada após " + i
+ " iteracoes");
System.out.println("Melhor Individuo =" + melhorIndividuo);
System.out.println("Fitness =" + fitness(melhorIndividuo));
ocorreuFalha = true;
}

totalIteracoes += i;
if (i > totalMaxIteracoes)
maxIteracoes = i;

println("------------------------------");

mostrarTabuleiro(melhorIndividuo);
println("Elapsed time = " + (System.currentTimeMillis() - startTime)
+ "ms");
}

/**
* @param string
*/
private static void println(String string) {
if (!disablePrint)
System.out.println(string);
}

/**
* @param populacao
*/
private static void carregarPopulacao(Set populacao) {
while (populacao.size() <>
String individuo = gerarIndividuo(8);
println("individuo=" + individuo);
populacao.add(individuo);
}
}

/**
* mostrar o tabuleiro graficamente
*
* @param melhorIndividuo
* @return
*/
private static void mostrarTabuleiro(String melhorIndividuo) {
println("|---+---+---+---+---+---+---+---|");
for (int i = 0; i <>
print("|");
int posicaoRainha = Integer.parseInt(melhorIndividuo.substring(i,
i + 1)) - 1;
for (int j = 0; j <>
if (posicaoRainha == j) {
print(" x |");
} else {
print(" |");
}
}
println("\r\n|---+---+---+---+---+---+---+---|");
}
}

/**
* @param string
*/
private static void print(String string) {
if (!disablePrint)
System.out.print(string);

}

/**
* logica GA mantendo os melhores na populacao retorna o melhor individuo
*
* @param populacao
* @param probabilidadeMutacao
*/
private static String geneticAlgorithm(Set populacao,
double probabilidadeMutacao, int fitnessAtual) {
String melhor = null;
Set filhos = new HashSet();
int tamanhoPopulacao = populacao.size();

while (filhos.size() <>
String p1 = selecionarAleatorio(populacao, "");
String p2 = selecionarAleatorio(populacao, p1);
String filho = crossover(p1, p2);
if (randomico.nextDouble() <= probabilidadeMutacao) {
int ffitness = fitness(filho);
if (ffitness <= fitnessAtual)
filho = mutate(filho);
}
filhos.add(filho);
}

// adicionar dois dos melhores pais
Object[] pais = populacao.toArray();
int[] f = new int[pais.length];
int melhorF = -1;
for (int i = 0; i <>
f[i] = fitness((String) pais[i]);
if (melhorF <>
melhorF = f[i];
}
}
populacao.clear();
while (populacao.size() <>
for (int i = 0; i <>
if (f[i] == melhorF) {
populacao.add((String) pais[i]);
}
if (populacao.size() == 2)
break;
}
melhorF--;
}

filhos.addAll(populacao);
Object[] pop = filhos.toArray();
f = new int[pop.length];
melhorF = -1;
for (int i = 0; i <>
f[i] = fitness((String) pop[i]);
if (melhorF <>
melhorF = f[i];
melhor = (String) pop[i];
}
}
populacao.clear();
while (populacao.size() <>
if (melhorF <>
// should never happen...
System.out.println("???????");
break;
}
for (int i = 0; i <>
if (f[i] == melhorF && populacao.size() <>
populacao.add((String) pop[i]);
}
}
melhorF--;
}
return melhor;
}

/**
* @param filho
* @return
*/
private static String mutate(String filho) {
int mp = randomico.nextInt(filho.length());
int mc = randomico.nextInt(filho.length()) + 1;
filho = filho.substring(0, mp) + mc
+ (mp + 1 == filho.length() ? "" : filho.substring(mp + 1));
return filho;
}

/**
* crossover
*
* @param p1
* @param p2
* @return
*/
private static String crossover(String p1, String p2) {
int i = randomico.nextInt(p1.length());
String ret = p1.substring(0, i) + p2.substring(i);
return ret;
}

/**
* seleciona um individuo da populacao aleatoriamente
*
* @param populacao
* @return
*/
private static String selecionarAleatorio(Set populacao, String px) {
String pn = px;
Object[] tmp = populacao.toArray();
while (pn.equals(px)) {
int i = randomico.nextInt((populacao.size()));
pn = (String) tmp[i];
}
return pn;
}

/**
* gerar um individuo com n posicoes
*
* @param n
* @return
*/
private static String gerarIndividuo(int n) {
String ret = "";
while (ret.length() <>
ret += (randomico.nextInt(n) + 1);
return ret;
}

/**
* função fitness, retorna a quantidade de rainhas a salvo.
*
* @param individuo
* @return
*/
public static int fitness(String individuo) {
int ret = 0;

int[][] tabuleiro = new int[8][8];
// primeiro representamos o tabuleiro com 0 e 1
for (int i = 0; i <>
int posicaoRainha = Integer.parseInt(individuo.substring(i, i + 1)) - 1;
for (int j = 0; j <>
tabuleiro[i][j] = posicaoRainha == j ? 1 : 0;
}
}

// agora verificamos quantas rainhas estao a salvo, este será o nosso
// retorno da função fitness
for (int i = 0; i <>
for (int j = 0; j <>
if (tabuleiro[i][j] == 1) {
if (!temAtacante(tabuleiro, i, j)) {
ret++;
}
}
}
}
return ret;
}

/**
* verifica se existe uma rainha ameaçando a posicao i,j existindo retorna
* true caso contrário retorna false
*
* @param tabuleiro
* @param i
* @param j
* @return
*/
private static boolean temAtacante(int[][] tabuleiro, int i, int j) {
// verificar na horizontal
for (int k = 0; k <>
if (k != i && tabuleiro[k][j] == 1)
return true;
}
// verificar na vertical
for (int k = 0; k <>
if (k != j && tabuleiro[i][k] == 1)
return true;
}
// verificar na diagonal1
int i0 = i - 1;
int j0 = j - 1;
while (i0 >= 0 && j0 >= 0) {
if (tabuleiro[i0][j0] == 1)
return true;
i0--;
j0--;
}
// verificar na diagonal2
i0 = i + 1;
j0 = j + 1;
while (i0 <>
if (tabuleiro[i0][j0] == 1)
return true;
i0++;
j0++;
}
// verificar na diagonal3
i0 = i + 1;
j0 = j - 1;
while (i0 <>= 0) {
if (tabuleiro[i0][j0] == 1)
return true;
i0++;
j0--;
}
// verificar na diagonal4
i0 = i - 1;
j0 = j + 1;
while (i0 >= 0 && j0 <>
if (tabuleiro[i0][j0] == 1)
return true;
i0--;
j0++;
}
return false; // esta a salvo
}

}