BUSQUEDA POR BACKTRACKING

Introduccion:

         Backtracking:Dentro de las técnicas de diseño de algoritmos, el método de Vuelta Atrás
         (del inglés Backtracking) es uno de los de más amplia utilización, en el sentido de que
         puede aplicarse en la resolución de un gran número de problemas, muy especialmente en
         aquellos de optimización. En ocasiones se ven enfrentados a problemas de optimización
         que solo pueden ser resueltos a través de un estudio exhaustivo de un conjunto conocido
         a priori de posibles soluciones, en las que se trata de encontrar una o todas las soluciones
         y por tanto también la óptima. Para llevar a cabo este estudio exhaustivo, el diseño Vuelta
         Atrás proporciona una manera sistemática de generar todas las posibles soluciones
         siempre que dichas soluciones sean susceptibles de resolverse en etapas.


Objetivo:

Investigar y desarrollar el método de búsqueda por Backtracking (vuelta a tras), para
comprender el método de búsqueda.

Marco Teorico:

Técnica de Backtracking

En su forma básica la Vuelta Atrás se asemeja a un recorrido en profundidad dentro de un árbol
cuya existencia sólo es implícita, y que se denomina árbol de expansión. Este árbol es
conceptual, en donde cada nodo de nivel k representa una parte de la solución y está formado por
k etapas que se suponen ya realizadas. Sus hijos son las prolongaciones posibles al añadir una
nueva etapa. Para examinar el conjunto de posibles soluciones es suficiente recorrer este árbol
construyendo soluciones parciales a medida que se avanza en el recorrido. En este recorrido
pueden suceder dos cosas. La primera es que tenga éxito si, procediendo de esta manera, se llega
a una solución (una hoja del árbol). Si lo único que buscábamos era una solución al problema, el
algoritmo finaliza aquí; ahora bien, si lo que se busca eran todas las soluciones o la mejor de
entre todas ellas, el algoritmo seguirá explorando el árbol en búsqueda de soluciones
alternativas.Por otra parte, el recorrido no tiene éxito si en alguna etapa la solución parcial
construida hasta el momento no se puede completar; s encuentran en lo que llamamos nodos
fracaso. En tal caso, el algoritmo vuelve atrás (y de ahí su nombre) en su recorrido eliminando
los elementos que se hubieran añadido en cada etapa a partir de ese nodo. Si en este retroceso, si
existe uno o más caminos aún no explorados que puedan conducir a solución, el recorrido del
árbol continúa por ellos.

Algoritmo Genético

El algoritmo genérico del backtracking es el siguiente:

Inicio
IniciarOpciones;
 Repetir
SeleccionarNuevaOpcion;
  Si Aceptable entonces
  Inicio
AnotarOpcion;
   Si SolucionIncompleta entonces
Backtracking(etapa_siguiente);
     Si NOT éxito entonces
CancelarAnotacion;
End
   Else /*Solución Completa*/
Éxito = true;
   End
  End
Until(Exito) OR (UltimaOpcion)
End Backtracking.

En este esquema se puede observar que están presentes tres elementos principales. En primer
lugar hay una generación de descendientes, en donde para cada nodo generamos sus
descendientes con posibilidad de solución. A este paso se le denomina expansión, ramificación o
bifurcación. A continuación, y para cada uno de estos descendientes, debe de aplicar lo que se
denomina prueba de fracaso (segundo elemento). Finalmente, caso de que sea aceptable este
nodo, aplicaran la prueba de solución (tercer elemento) que comprueba si el nodo que es posible
solución efectivamente lo es. Tal vez lo más difícil de ver en este esquema es donde se realiza la
vuelta atrás, y para ello han de pensar en la propia recursión y su mecanismo de funcionamiento,
que es la que permite ir recorriendo el árbol en profundidad. Cuando se desea encontrar todas las
soluciones habrá que alterar ligeramente el esquema dado, de forma que una vez conseguida una
solución se continúe buscando hasta agotar todas las posibilidades. Queda por tanto el siguiente
esquema general para este caso:

Algoritmo BacktrackingTodaslasSoluciones(etapa)

Inicio
IniciarOpciones;
 Repetir
SeleccionarNuevaOpcion;
   Si Aceptable entonces
AnotarOpcion;
       Si SolucionIncompleta entonces
BactrackingTodaslasSoluciones(etapa_siguiente);
Else
ComuncicarSolucion;
End;
CancelarAnotacion;
End;
Until(UltimaOpcion); EndBacktrackingTodaslasSoluciones;

Eficiencia y complejidad del método

Gran parte de la eficiencia (siempre relativa) de un algoritmo de Vuelta Atrás proviene de
considerar el menor conjunto de nodos que puedan llegar a ser soluciones, aunque siempre
asegurar de que el árbol “podado” siga conteniendo todas las soluciones. Por otra parte debe
tener cuidado a la hora de decidir el tipo de condiciones (restricciones) que comprueban en cada
nodo a fin de detectar nodos fracaso. Evidentemente el análisis de estas restricciones permite
ahorrar tiempo, al delimitar el tamaño del árbol a explorar. Sin embargo esta evaluación requiere
a su vez tiempo extra, de manera que aquellas restricciones que vayan a detectar pocos nodos
fracaso no serán normalmente interesantes. No obstante, y como norma de actuación general,
podrías decir que las restricciones sencillas son siempre apropiadas, mientras que las más
sofisticadas que requieren más tiempo en su cálculo deberían reservarse para situaciones en las
que el árbol que se genera sea muy grande.

Usualmente los algoritmos Vuelta Atrás(backtracking) son de complejidad exponencial por la
forma en la que se busca la solución mediante el recorrido en profundidad del árbol. De esta
forma estos algoritmos van a ser de un orden de complejidad al menos del número de nodos del
árbol que se generen y este número, si no se utilizan restricciones, es de orden de zⁿ donde z son
las posibles opciones que existen en cada etapa, y n el número de etapas que es necesario
recorrer hasta construir la solución (esto es, la profundidad del árbol o la longitud de la n-tupla
solución). El uso de restricciones, tanto implícitas como explícitas, trata de reducir este número
tanto como sea posible, pero sin embargo en muchos casos no son suficientes para conseguir
algoritmos “tratables”, es decir, que sus tiempos de ejecución sean de orden de complejidad
razonable. Para aquellos problemas en donde se busca una solución y no todas, es donde entra en
juego la posibilidad de considerar distintas formas de ir generando los nodos del árbol. Y como
la búsqueda que realiza la Vuelta Atrás es siempre en profundidad, para lograr esto sólo debe de
ir variando el orden en el que se generan los descendientes de un nodo, de manera que trate de
ser lo más apropiado a nuestra estrategia.
El problema de las N reinas
Implementacion
    - Distinta columna:
Todos los xi diferentes.
    - Distinta diagonal:
Las reinas (i,j) y (k,l) estan
en la misma diagonal sii-j=k-l o bien i+j=k+l,
lo que se puede resumir en|j-l| = |k-i|.
En terminos de los xi, tendremos |xk-xi|=|k-i|. 100

// Comprobar si la reina de la fila k está bien colocada
// (si no está en la misma columna ni en la misma diagonal
// que cualquiera de las reinas de las filas anteriores)
// Eficiencia: O(k-1).
bool comprobar (int reinas[], int n, int k)
{
inti;
for (i=0; i<k; i++)
if ( ( reinas[i]==reinas[k] )
|| ( abs(k-i) == abs(reinas[k]-reinas[i]) ) )
return false;
return true;
}

Implementacion recursiva
mostrando todas las soluciones
// Inicialmente, k=0 y reinas[i]=-1.
voidNReinas (int reinas[], int n, int k)
{
if (k==n) { // Solución (no quedan reinas por colocar)
print(reinas,n);
} else { // Aún quedan reinas por colocar (k<n)
for (reinas[k]=0; reinas[k]<n; reinas[k]++)
if (comprobar(reinas,n,k))
NReinas (reinas, n, k+1);
}}

mostrando solo la primera solucion
boolNReinas (int reinas[], int n, int k)
{
bool ok = false;
if (k==n) { // Caso base: No quedan reinas por colocar
ok = true;
} else { // Aún quedan reinas por colocar (k<n)
while ((reinas[k]<n-1) && !ok) {
reinas[k]++;
if (comprobar(reinas,n,k))
ok = NReinas (reinas, n, k+1);
}}
return ok; // La solución está en reinas[] cuando ok==true
}

Implementacion iterativa
mostrando todas las soluciones
voidNReinas (int reinas[], int n)
{
int k=0; // Fila actual = k
for (inti=0; i<n; i++) reinas[i]=-1; // Configuracióninicial
while (k>=0) {
reinas[k]++; // Colocar reina k en la siguiente columna…
while ((reinas[k]<n) && !comprobar(reinas,n,k))
reinas[k]++;
if (k<n) { // Reina colocada
if (k==n-1) { print(reinas,n); // Solución
} else { k++; reinas[k] = -1; } // Siguiente reina
} else { k--; }
}}
Conclusion:

Bibliografia:

http://www.ecured.cu/index.php/Backtracking
http://www.lcc.uma.es/~av/Libro/CAP6.pdf

Busqueda por backtracking

  • 1.
    BUSQUEDA POR BACKTRACKING Introduccion: Backtracking:Dentro de las técnicas de diseño de algoritmos, el método de Vuelta Atrás (del inglés Backtracking) es uno de los de más amplia utilización, en el sentido de que puede aplicarse en la resolución de un gran número de problemas, muy especialmente en aquellos de optimización. En ocasiones se ven enfrentados a problemas de optimización que solo pueden ser resueltos a través de un estudio exhaustivo de un conjunto conocido a priori de posibles soluciones, en las que se trata de encontrar una o todas las soluciones y por tanto también la óptima. Para llevar a cabo este estudio exhaustivo, el diseño Vuelta Atrás proporciona una manera sistemática de generar todas las posibles soluciones siempre que dichas soluciones sean susceptibles de resolverse en etapas. Objetivo: Investigar y desarrollar el método de búsqueda por Backtracking (vuelta a tras), para comprender el método de búsqueda. Marco Teorico: Técnica de Backtracking En su forma básica la Vuelta Atrás se asemeja a un recorrido en profundidad dentro de un árbol cuya existencia sólo es implícita, y que se denomina árbol de expansión. Este árbol es conceptual, en donde cada nodo de nivel k representa una parte de la solución y está formado por k etapas que se suponen ya realizadas. Sus hijos son las prolongaciones posibles al añadir una nueva etapa. Para examinar el conjunto de posibles soluciones es suficiente recorrer este árbol construyendo soluciones parciales a medida que se avanza en el recorrido. En este recorrido pueden suceder dos cosas. La primera es que tenga éxito si, procediendo de esta manera, se llega a una solución (una hoja del árbol). Si lo único que buscábamos era una solución al problema, el algoritmo finaliza aquí; ahora bien, si lo que se busca eran todas las soluciones o la mejor de entre todas ellas, el algoritmo seguirá explorando el árbol en búsqueda de soluciones alternativas.Por otra parte, el recorrido no tiene éxito si en alguna etapa la solución parcial construida hasta el momento no se puede completar; s encuentran en lo que llamamos nodos fracaso. En tal caso, el algoritmo vuelve atrás (y de ahí su nombre) en su recorrido eliminando los elementos que se hubieran añadido en cada etapa a partir de ese nodo. Si en este retroceso, si existe uno o más caminos aún no explorados que puedan conducir a solución, el recorrido del árbol continúa por ellos. Algoritmo Genético El algoritmo genérico del backtracking es el siguiente: Inicio
  • 2.
    IniciarOpciones; Repetir SeleccionarNuevaOpcion; Si Aceptable entonces Inicio AnotarOpcion; Si SolucionIncompleta entonces Backtracking(etapa_siguiente); Si NOT éxito entonces CancelarAnotacion; End Else /*Solución Completa*/ Éxito = true; End End Until(Exito) OR (UltimaOpcion) End Backtracking. En este esquema se puede observar que están presentes tres elementos principales. En primer lugar hay una generación de descendientes, en donde para cada nodo generamos sus descendientes con posibilidad de solución. A este paso se le denomina expansión, ramificación o bifurcación. A continuación, y para cada uno de estos descendientes, debe de aplicar lo que se denomina prueba de fracaso (segundo elemento). Finalmente, caso de que sea aceptable este nodo, aplicaran la prueba de solución (tercer elemento) que comprueba si el nodo que es posible solución efectivamente lo es. Tal vez lo más difícil de ver en este esquema es donde se realiza la vuelta atrás, y para ello han de pensar en la propia recursión y su mecanismo de funcionamiento, que es la que permite ir recorriendo el árbol en profundidad. Cuando se desea encontrar todas las soluciones habrá que alterar ligeramente el esquema dado, de forma que una vez conseguida una solución se continúe buscando hasta agotar todas las posibilidades. Queda por tanto el siguiente esquema general para este caso: Algoritmo BacktrackingTodaslasSoluciones(etapa) Inicio IniciarOpciones; Repetir SeleccionarNuevaOpcion; Si Aceptable entonces AnotarOpcion; Si SolucionIncompleta entonces BactrackingTodaslasSoluciones(etapa_siguiente); Else ComuncicarSolucion; End; CancelarAnotacion; End;
  • 3.
    Until(UltimaOpcion); EndBacktrackingTodaslasSoluciones; Eficiencia ycomplejidad del método Gran parte de la eficiencia (siempre relativa) de un algoritmo de Vuelta Atrás proviene de considerar el menor conjunto de nodos que puedan llegar a ser soluciones, aunque siempre asegurar de que el árbol “podado” siga conteniendo todas las soluciones. Por otra parte debe tener cuidado a la hora de decidir el tipo de condiciones (restricciones) que comprueban en cada nodo a fin de detectar nodos fracaso. Evidentemente el análisis de estas restricciones permite ahorrar tiempo, al delimitar el tamaño del árbol a explorar. Sin embargo esta evaluación requiere a su vez tiempo extra, de manera que aquellas restricciones que vayan a detectar pocos nodos fracaso no serán normalmente interesantes. No obstante, y como norma de actuación general, podrías decir que las restricciones sencillas son siempre apropiadas, mientras que las más sofisticadas que requieren más tiempo en su cálculo deberían reservarse para situaciones en las que el árbol que se genera sea muy grande. Usualmente los algoritmos Vuelta Atrás(backtracking) son de complejidad exponencial por la forma en la que se busca la solución mediante el recorrido en profundidad del árbol. De esta forma estos algoritmos van a ser de un orden de complejidad al menos del número de nodos del árbol que se generen y este número, si no se utilizan restricciones, es de orden de zⁿ donde z son las posibles opciones que existen en cada etapa, y n el número de etapas que es necesario recorrer hasta construir la solución (esto es, la profundidad del árbol o la longitud de la n-tupla solución). El uso de restricciones, tanto implícitas como explícitas, trata de reducir este número tanto como sea posible, pero sin embargo en muchos casos no son suficientes para conseguir algoritmos “tratables”, es decir, que sus tiempos de ejecución sean de orden de complejidad razonable. Para aquellos problemas en donde se busca una solución y no todas, es donde entra en juego la posibilidad de considerar distintas formas de ir generando los nodos del árbol. Y como la búsqueda que realiza la Vuelta Atrás es siempre en profundidad, para lograr esto sólo debe de ir variando el orden en el que se generan los descendientes de un nodo, de manera que trate de ser lo más apropiado a nuestra estrategia.
  • 8.
    El problema delas N reinas Implementacion - Distinta columna: Todos los xi diferentes. - Distinta diagonal: Las reinas (i,j) y (k,l) estan en la misma diagonal sii-j=k-l o bien i+j=k+l, lo que se puede resumir en|j-l| = |k-i|. En terminos de los xi, tendremos |xk-xi|=|k-i|. 100 // Comprobar si la reina de la fila k está bien colocada // (si no está en la misma columna ni en la misma diagonal // que cualquiera de las reinas de las filas anteriores) // Eficiencia: O(k-1). bool comprobar (int reinas[], int n, int k) { inti; for (i=0; i<k; i++) if ( ( reinas[i]==reinas[k] ) || ( abs(k-i) == abs(reinas[k]-reinas[i]) ) )
  • 9.
    return false; return true; } Implementacionrecursiva mostrando todas las soluciones // Inicialmente, k=0 y reinas[i]=-1. voidNReinas (int reinas[], int n, int k) { if (k==n) { // Solución (no quedan reinas por colocar) print(reinas,n); } else { // Aún quedan reinas por colocar (k<n) for (reinas[k]=0; reinas[k]<n; reinas[k]++) if (comprobar(reinas,n,k)) NReinas (reinas, n, k+1); }} mostrando solo la primera solucion boolNReinas (int reinas[], int n, int k) { bool ok = false; if (k==n) { // Caso base: No quedan reinas por colocar ok = true; } else { // Aún quedan reinas por colocar (k<n) while ((reinas[k]<n-1) && !ok) { reinas[k]++; if (comprobar(reinas,n,k)) ok = NReinas (reinas, n, k+1); }} return ok; // La solución está en reinas[] cuando ok==true } Implementacion iterativa mostrando todas las soluciones voidNReinas (int reinas[], int n) { int k=0; // Fila actual = k for (inti=0; i<n; i++) reinas[i]=-1; // Configuracióninicial while (k>=0) { reinas[k]++; // Colocar reina k en la siguiente columna… while ((reinas[k]<n) && !comprobar(reinas,n,k)) reinas[k]++; if (k<n) { // Reina colocada if (k==n-1) { print(reinas,n); // Solución } else { k++; reinas[k] = -1; } // Siguiente reina } else { k--; } }}
  • 10.