Vamos a acercarnos un poco más a cómo es una búsqueda exhaustiva "real".
Hemos visto cómo generar todas las posibles variaciones de las cifras 1, 2 y 3. Lo habitual es que los datos obtenidos deban cumplir alguna condición, lo que hace que ciertas soluciones no sean válidas. En ese caso, podremos mirar si vamos a obtener un dato válido antes de llamar a la función recursiva o, en el peor de los casos, al comienzo del análisis de un caso.
Por ejemplo, si queremos sólo ver las permutaciones de 1, 2 y 3 (cambios de orden, sin permitir que ninguna de las cifras se repita), podríamos filtrar directamente en el momento de hacer la llamada recursiva: sólo añadimos una cifra si no está contenida todavía en nuestro número:
1: // Backtracking - ejemplo 03 2: // Permutaciones de 123: 123, 132, 213, ... 3: using System; 4: 5: public class Backtracking03 6: { 7: public static void BT(string textoActual) 8: { 9: // Si "datos" no puede dar una solución válida, salir 10: if (textoActual.Length >= 4) 11: return; 12: 13: // Si "datos" ya es solución, devolver o mostrar y terminar 14: if (textoActual.Length == 3) 15: { 16: Console.WriteLine(textoActual); 17: return; 18: } 19: 20: // Si no, completar con la siguiente solución parcial 21: // Y volver a ver de forma recursiva 22: for (char letra = '1'; letra <= '3'; letra++) 23: if ( ! textoActual.Contains ( ""+letra) ) 24: BT(textoActual + letra); 25: 26: } 27: 28: public static void Main() 29: { 30: // Crear datos y llamar a la función de Backtracking 31: BT(""); 32: } 33: }
Si las condiciones son más complejas, puede ser preferible analizarlas al comienzo de cada "pasada" por la función. Por ejemplo, si queremos mostrar todas las permutaciones de 1234 que no sean pares y que tengan un "2" en la segunda posición, podríamos hacer:
1: // Backtracking - ejemplo 04 2: // Permutaciones de 1234 que tengan un 2 en la segunda posición 3: // y no sean múltiplos de 2 4: using System; 5: 6: public class Backtracking04 7: { 8: public static void BT(string textoActual) 9: { 10: // Si "datos" no puede dar una solución válida, salir 11: 12: // Puede no ser vaĺida si la segunda cifra es 2 13: if ((textoActual.Length >= 2) && (textoActual[1] != '2')) 14: return; 15: 16: // O si tiene toda la longitud pero no es múltiplo de 2 17: if ((textoActual.Length == 4) && 18: (Convert.ToInt32(textoActual) % 2 == 0)) 19: return; 20: 21: // O si ya hemos superado la longitud máxima (no debería ocurrir) 22: if (textoActual.Length >= 5) 23: return; 24: 25: // Si "datos" ya es solución, devolver o mostrar y terminar 26: if (textoActual.Length == 4) 27: { 28: Console.WriteLine(textoActual); 29: return; 30: } 31: 32: // Si no, completar con la siguiente solución parcial 33: // Y volver a ver de forma recursiva 34: for (char letra = '1'; letra <= '4'; letra++) 35: if ( ! textoActual.Contains ( ""+letra) ) 36: BT(textoActual + letra); 37: 38: } 39: 40: public static void Main() 41: { 42: // Crear datos y llamar a la función de Backtracking 43: BT(""); 44: } 45: }
Vamos a ver un caso más real: vamos a formar todas las palabras posibles, que contengan ciertas letras y que a la vez estén contenidas en un diccionario. Lo podríamos hacer así:
1: // Backtracking - ejemplo 05 2: // Palabras de un diccionario a partir ciertas letras 3: using System; 4: using System.Collections.Generic; 5: 6: public class Backtracking05 7: { 8: 9: public static void BT(string pruebaActual, 10: List<string> soluc, List<string> validas, 11: string letras, int posicion) 12: { 13: // Si no se puede obtener una solución válida, salir 14: if (letras.Length == 0) 15: return; 16: 17: // Si "datos" ya es solución, devolver o mostrar y terminar 18: if (validas.Contains(pruebaActual) 19: && ( ! soluc.Contains(pruebaActual))) 20: soluc.Add(pruebaActual); 21: 22: // Si no, completar con la siguiente solución parcial 23: // Y volver a ver de forma recursiva 24: for (int i=0; i<letras.Length; i++) 25: { 26: BT(pruebaActual+letras[i], soluc, validas, 27: letras.Remove(i,1), posicion+1); 28: } 29: 30: 31: } 32: 33: public static void Main() 34: { 35: // Crear datos y llamar a la función de Backtracking 36: List<string> palabrasValidas = new List<string>( 37: new string[]{ 38: "A", "ALA", "ALADA", "ALOHA", "HOLA", "LA", "LAS", "SALA" 39: }); 40: Console.WriteLine("Contiene SALA? " + 41: palabrasValidas.Contains("SALA")); 42: string conjuntoLetras = "ALHZADST"; 43: List<string> solucion = new List<string>(); 44: BT("", solucion, palabrasValidas, conjuntoLetras, 0); 45: 46: // Y mostrar todas las soluciones almacenadas 47: foreach (string texto in solucion) 48: Console.WriteLine(texto); 49: } 50: }
Puede no ser evidente hasta qué punto puede consumir recursos un programa de este tipo. Para descubrirlo, podemos añadir un contador, que se vaya actualizando en cada pasada, y mostrar su resultado al final:
1: // Backtracking - ejemplo 05b 2: // Palabras de un diccionario a partir ciertas letras 3: // Versión con contador 4: using System; 5: using System.Collections.Generic; 6: 7: public class Backtracking05b 8: { 9: 10: static int llamadas = 0; 11: 12: public static void BT(string pruebaActual, 13: List<string> soluc, List<string> validas, 14: string letras, int posicion) 15: { 16: llamadas++; 17: 18: // Si no se puede obtener una solución válida, salir 19: if (letras.Length == 0) 20: return; 21: 22: // Si "datos" ya es solución, devolver o mostrar y terminar 23: if (validas.Contains(pruebaActual) 24: && ( ! soluc.Contains(pruebaActual))) 25: soluc.Add(pruebaActual); 26: 27: // Si no, completar con la siguiente solución parcial 28: // Y volver a ver de forma recursiva 29: for (int i=0; i<letras.Length; i++) 30: { 31: BT(pruebaActual+letras[i], soluc, validas, 32: letras.Remove(i,1), posicion+1); 33: } 34: 35: 36: } 37: 38: public static void Main() 39: { 40: // Crear datos y llamar a la función de Backtracking 41: List<string> palabrasValidas = new List<string>( 42: new string[]{ 43: "A", "ALA", "ALADA", "ALOHA", "HOLA", "LA", "LAS", "SALA" 44: }); 45: Console.WriteLine("Contiene SALA? " + 46: palabrasValidas.Contains("SALA")); 47: string conjuntoLetras = "ALHZADST"; 48: List<string> solucion = new List<string>(); 49: BT("", solucion, palabrasValidas, conjuntoLetras, 0); 50: 51: // Y mostrar todas las soluciones almacenadas 52: foreach (string texto in solucion) 53: Console.WriteLine(texto); 54: 55: Console.WriteLine("Llamadas a la función: "+ llamadas); 56: } 57: }
El resultado puede ser ser mucho más alto de lo que esperamos. En este ejemplo, con tan pocas letras, se nos informará de que se han realizado 109.601 llamadas a la función (sí, más de cien mil).
Para mejorarlo, podemos "podar" esa ramificación de la función, no permitiéndole avanzar cuando sepamos de antemano que no podemos llegar a una solución válida. Por ejemplo, si la secuencia que estamos probando no es el comienzo de ninguna de las palabras válidas de nuestro diccionario, no seguimos buscando por ese camino:
1: // Backtracking - ejemplo 05c 2: // Palabras de un diccionario a partir ciertas letras 3: // Versión "podada" 4: using System; 5: using System.Collections.Generic; 6: 7: public class Backtracking05c 8: { 9: 10: static int llamadas = 0; 11: 12: public static void BT(string pruebaActual, 13: List<string> soluc, List<string> validas, 14: string letras, int posicion) 15: { 16: llamadas++; 17: 18: // Si no se puede obtener una solución válida, salir 19: if (letras.Length == 0) 20: return; 21: // (Si ninguna palabra comienza con esta prueba, no seguimos) 22: int palabrasQueEmpiezanPorLaActual = 0; 23: foreach(string palabra in validas) 24: if (palabra.StartsWith(pruebaActual)) 25: palabrasQueEmpiezanPorLaActual ++; 26: if (palabrasQueEmpiezanPorLaActual == 0) 27: return; 28: 29: // Si "datos" ya es solución, devolver o mostrar y terminar 30: if (validas.Contains(pruebaActual) 31: && ( ! soluc.Contains(pruebaActual))) 32: soluc.Add(pruebaActual); 33: 34: // Si no, completar con la siguiente solución parcial 35: // Y volver a ver de forma recursiva 36: for (int i=0; i<letras.Length; i++) 37: { 38: BT(pruebaActual+letras[i], soluc, validas, 39: letras.Remove(i,1), posicion+1); 40: } 41: 42: 43: } 44: 45: public static void Main() 46: { 47: // Crear datos y llamar a la función de Backtracking 48: List<string> palabrasValidas = new List<string>( 49: new string[]{ 50: "A", "ALA", "ALADA", "ALOHA", "HOLA", "LA", "LAS", "SALA" 51: }); 52: Console.WriteLine("Contiene SALA? " + 53: palabrasValidas.Contains("SALA")); 54: string conjuntoLetras = "ALHZADST"; 55: List<string> solucion = new List<string>(); 56: BT("", solucion, palabrasValidas, conjuntoLetras, 0); 57: 58: // Y mostrar todas las soluciones almacenadas 59: foreach (string texto in solucion) 60: Console.WriteLine(texto); 61: 62: Console.WriteLine("Llamadas a la función: "+ llamadas); 63: } 64: }
Esto reduce la cantidad de llamadas a la función a... ¡126! Casi mil veces menos. El programa no será mil veces más rápido, porque está haciendo comprobaciones adicionales, pero indudablemente será más rápido y consumirá menos memoria.
Terminemos con un "problema típico": el "Problema de la mochila". Se trata de llevar en una mochila una serie de objetos. Cada objeto tiene un cierto valor (económico, por ejemplo), y queremos llevar el mayor valor total posible, pero la limitación es que la mochila tiene un límite de peso, así que debemos escoger con cuidado qué objetos introducir. Para plantear una solución exhaustiva, podemos crearnos clases auxiliares que representen el conjunto de la mochila y cada uno de sus elementos, de modo que una solución podría ser:
1: // Backtracking - ejemplo 06 2: // Problema de la mochila 3: using System; 4: using System.Collections.Generic; 5: 6: public class Backtracking06 7: { 8: 9: class ElementoMochila 10: { 11: private float valor, peso; 12: public float Valor { 13: get { return valor; } 14: set { valor = value; } 15: } 16: public float Peso { 17: get { return peso; } 18: set { peso = value; } 19: } 20: public ElementoMochila(float valorInicial, float pesoInicial) 21: { 22: valor = valorInicial; 23: peso = pesoInicial; 24: } 25: } 26: 27: 28: class Mochila 29: { 30: private float valor, peso; 31: List<ElementoMochila> elementos; 32: public float Valor { 33: get { return valor; } 34: } 35: public float Peso { 36: get { return peso; } 37: } 38: public Mochila() 39: { 40: elementos = new List<ElementoMochila>(); 41: } 42: public void Add(ElementoMochila e) 43: { 44: peso += e.Peso; 45: valor += e.Valor; 46: elementos.Add(e); 47: } 48: public bool Contains(ElementoMochila e) 49: { 50: return elementos.Contains(e); 51: } 52: public void MostrarContenido() 53: { 54: foreach (ElementoMochila e in elementos) 55: Console.WriteLine(" V: " + e.Valor + ", P: "+ e.Peso); 56: } 57: public Mochila NuevaMochila(ElementoMochila nuevoElemento) 58: { 59: Mochila m2 = new Mochila(); 60: foreach (ElementoMochila e in elementos) 61: m2.Add(e); 62: m2.Add(nuevoElemento); 63: return m2; 64: } 65: } 66: 67: 68: static void BT(Mochila mochilaActual, 69: List<ElementoMochila> elemPosibles, 70: int maximoPeso, 71: ref Mochila mejorMochila) 72: { 73: // Si no se puede obtener una solución válida, salir 74: if (mochilaActual.Peso > maximoPeso) 75: return; 76: 77: // Si "datos" ya es solución, devolver o mostrar y terminar 78: if (mochilaActual.Valor > mejorMochila.Valor) 79: mejorMochila = mochilaActual; 80: 81: // Si no, completar con la siguiente solución parcial 82: // Y volver a ver de forma recursiva 83: foreach (ElementoMochila e in elemPosibles) 84: { 85: if (! mochilaActual.Contains(e)) 86: { 87: BT(mochilaActual.NuevaMochila(e), elemPosibles, 88: maximoPeso, ref mejorMochila); 89: } 90: } 91: } 92: 93: public static void Main() 94: { 95: // Crear datos y llamar a la función de Backtracking 96: List<ElementoMochila> elementosPosibles = new List<ElementoMochila>(); 97: elementosPosibles.Add( new ElementoMochila(200, 12) ); 98: elementosPosibles.Add( new ElementoMochila(500, 6) ); 99: elementosPosibles.Add( new ElementoMochila(400, 7) ); 100: elementosPosibles.Add( new ElementoMochila(800, 3) ); 101: elementosPosibles.Add( new ElementoMochila(5000, 20) ); 102: elementosPosibles.Add( new ElementoMochila(100, 10) ); 103: 104: int maximoPesoMochila = 18; 105: 106: Mochila mochilaVacia = 107: new Mochila(); 108: 109: Mochila mejorSolucion = 110: new Mochila(); 111: 112: 113: BT(mochilaVacia, elementosPosibles, 114: maximoPesoMochila, ref mejorSolucion); 115: 116: // Y mostrar todas las soluciones almacenadas 117: Console.WriteLine("Valor: "+mejorSolucion.Valor); 118: mejorSolucion.MostrarContenido(); 119: } 120: }
Hay algoritmos que buscan aproximar la solución de problemas como éste, tomando cada vez la "solución más prometedora", de forma que se obtenga una solución rápida y "buena", pero que quizá no sea óptima.
Pero eso queda para otro día...
No hay comentarios:
Publicar un comentario