Вы находитесь на странице: 1из 48

Ejemplo de recursividad para facilitar la comprensión del texto

Que es recursividad
1-Definición: Consiste en la definición de algo (una propiedad, una operación, un
procedimiento o una función) en términos de sí mismo( definición dada en clase).

Esta última definición esta en el archivo ayudaRecursion.pdf

2-Elementos de un algoritmo recursivo

 Los casos base: Son los casos del problema que se resuelven con un segmento
de código sin recursividad. Normalmente corresponden a instancias del problema
simples y fáciles de implementar cuya solución es conocida. Ejemplo, Factorial de
0.

 Los casos recursivos: son casos que se resuelven mediante invocaciones a sí


mismo, y por lo general, reduciendo el problema de dimensión para que se
aproxime cada vez más a un caso base.

 Los parámetros: Toda acción o función recursiva tiene en general al menos un


parámetro, normalmente pasado por valor, que debe cambiar en cada llamada
recursiva hasta aproximarse o converger al caso base. El resultado puede ser
retornado, o bien dejado en un parámetro por referencia.
3- Ejemplo de algoritmo recursivo
Ejemplo dado en clase:

Ejemplos adicionales
Function Factorial(Integer n) : Integer

If n<=0 then

Return 1;

Else

Return n * Factorial(n-1);

EndIf

EndFunction

Function Potencia(Integer x, Integer y) : Integer

If y==0 then

Return 1;

Else

Integer aux = Potencia(x, y div 2);

aux = aux * aux;

If (y mod 2  0) then

aux = x * aux;

Return aux;

EndIf EndFunction
Function Potencia(Integer x, Integer y) : Integer

If y==0 then

Return 1;

Else

Return x*Potencia(x,y-1);

EndIf

EndFunction

¿Cuál algoritmo de potencia creen ustedes que es más eficiente?

4- Diseño de funciones recursivas

El problema original se puede transformar en otro problema similar “más simple”.

Tenemos alguna manera directa de solucionar “problemas triviales”.

Ejemplo: El cálculo del factorial de un número entero, no negativo.


1.1.- Algoritmos Recursivos

Un algoritmo se dice que es recursivo cuando tiene la capacidad de llamarse o invocarse


a sí mismo o de llamar a otro algoritmo desde el cual se puede volver a invocar al primero.
En cualquier caso, se garantiza que el número de llamadas recursivas que se realizan es
finito (es decir, en algún momento DEBE finalizar la ejecución del algoritmo). Para ello,
cada llamada recursiva resuelve un problema de menor tamaño, uno tras otro, hasta
llegar a un problema cuya resolución sea directa o conocida.

5-La técnica que se aplica para definir la solución recursiva de problemas se conoce como
divide y vencerás, y se trata de resolver un problema mediante su descomposición en
varios subproblemas similares al original (o del mismo tipo) pero con datos más
pequeños. Si un problema puede subdividirse en subproblemas similares, entonces el
mismo proceso puede ser aplicado a cada uno de los subproblemas hasta que se pueda
encontrar una solución directa o conocida. Finalmente se combinan las soluciones de los
subproblemas para producir la solución final del problema original.

6- Ejecución

Cuando se invoca a una función o acción, se almacena el ambiente local en el tope de la


pila del programa. En este ambiente se tienen los parámetros y demás variables locales.
La función o acción podrá acceder a estas variables, así como el resto de su ambiente de
referenciación (atributos del objeto al cual la función o acción pertenece, variables
globales, y demás variables alcanzables según las reglas de alcance).

Si una función recursiva contiene variables locales y/o parámetros, se creará un nivel en
la pila diferente por cada llamada. Los nombres de las variables locales serán los mismos,
pero en cada nivel recursivo son un grupo nuevo de variables, que tienen posiciones de
memoria distintas (están en distintos ambientes de referenciación). Las variables locales a
las que puede acceder la función o acción recursiva son las definidas en su propio
ambiente.

Así como se apilan las variables, se guarda la posición de la última instrucción ejecutada,
de manera tal que, cuando la función termina, se desapila todo el ambiente local, y se
devuelve el control al punto en donde se hizo la invocación. Los resultados pueden ser
retornados, o dejados en variables pasadas por referencia, en atributos, o en variables
globales.

Como ejercicio, hacer una traza del Factorial o de la Potencia.

EJEMPLO: Revisar el ejemplo práctico realizado en clase para la función Factorial

7- Tipos de algoritmos recursivos


 Recursividad simple: Aquella en cuya definición sólo aparece una llamada
recursiva. Se puede transformar con facilidad en algoritmos iterativos.
Como ejemplos tenemos, Factorial, Potencia, Búsqueda Binaria.

Ejemplos:

NOTA: REVISAR EL MATERIAL ADICIONAL ENVIADO POR CORREO PARA


COMPLEMENTAR ESTA CLASE. CABE DESTACAR QUE ESTA CLASE EN SU
MAYORIA ES CONSTRUIDA DEL MATERIAL QUE SE LES ENVIO.
 Recursividad múltiple: Se da cuando hay más de una llamada a sí misma dentro
del cuerpo de la función, resultando más difícil de representar de forma iterativa.
Ejemplo dado en clase:

1- La función QuickSort

Procedure QuickSort(Ref Array A[1..*] of Integer, Integer N)


If (N > 1) Then
Integer S1, S2, S3;
Array L1,L2,L3[1..N] of Integer;
Integer x = A[1]; //el 1ro. u otro elemento
Menores(L1, A, x, S1); //L1 = { y en A : y < x }
Mayores(L2, A, x, S2); //L2 = { y en A : y > x }
Iguales(L3, A, x, S3); //L3 = { y en A : y = x }
// Si es el tamaño del arreglo Li
QuickSort(L1, S1); //llamada recursiva 01
QuickSort(L2, S2); //llamada recursiva 02
Concatenar(A,L1,L3,L2,N,S1,S3,S2); // A=L1+L3+L2
EndIf

EndProcedure

Procedure Concatenar(Ref Array Result[1..*] of Integer,

Array X,Y,Z[1..*] of Integer, Integer Sr, Sx, Sy, Sz)

// concatena los arreglos X,Y,Z de tamaños Sx,Sy y Sz resp.

// dejando el resultado en el arreglo Result de tamaño Sr

Integer i;

For i=1 To Sx Do

Result[i] = X[i];

EndFor

For i=1 To Sy Do

Result[Sx+i] = Y[i];

EndFor

For i=1 To Sz Do

Result[Sx+Sy+i] = Z[i];

EndFor

EndProcedure
2- El Fibonacci en su forma original

Ejemplo con el fibonacci: TAREA RECOMENDADA

int fibonacci(int n){ TRADUZA ESTE ALGORITMO A LA


if n==0 return 1; NOTACION PSEUDO-FORMAL
if n==1 1 return = 1;{ VERSIÓN ALFA ENVIADA POR LA
} else{ COORDINACIÓN, para que vayan
//tiene más de una llamada recursiva a practicando
si mismo
fibonacci(n) = Fib(n-1)+Fib(n-2);
}
}

3- La función MergeSort

Procedure MergeSort(Ref Array A[1..N] of Integer, Integer Li, Ls)

If Ls>Li Then

Integer c = (Li+Ls) div 2;

MergeSort(A, Li, c);

MergeSort(A, Li + c + 1, Ls);

Mezclar(A, Li, c, Ls); // mezcla ordenada de A[Li..c] con

// A[Li+c+1..Ls], y lo deja en A[Li..Ls]

EndIf

EndProc

Procedure Mezclar(Ref Array A[1..N] of Integer, Integer Li, c, Ls)

Array Aux[1..Ls-Li+1] of Integer;

Integer i,j,k;

j = Li; // índice del primer subarreglo

k = c+1; // índice del Segundo subarreglo

For i=1 To Ls-Li+1 Do

Select

j == c+1: // sub arreglo A[Li..c] ya tratado


Aux[i] = A[k]; k = k+1;

k == Ls+1: // sub arreglo A[Li+c+1..Ls] ya tratado

Aux[i] = A[j]; j=j+1;

j<= c and k <= Ls: // no se han terminado los subarreglos

// tomamos el más pequeño

If A[j]>A[k] Then

Aux[i] = A[k]; k=k+1;

Else

Aux[i] = A[j]; j=j+1;

EndIf

EndSelect

EndFor

// copiando Aux en el subarreglo A[Li..Ls]

For i=Li To Ls Do

A[i] = Aux[i-Li+1];

EndFor

EndProcedure

4- torres de Hanoi,( este ejemplo esta en una de las guías enviadas por correo de la
clase02) Revise Google Drive, DropBox o su correo)
1.Tipos de algoritmos recursivos(continuación)

8-Recursividad anidada: En algunos de los argumentos de la llamada recursiva


hay una nueva llamada a sí misma.

Ejemplo: Función de Ackerman

int ackerman(int n, int m)


{
if (n == 0) return m + 1;
else{
if(m == 0) return ackerman(n - 1, 1)
else;
return ackerman(n - 1, ackerman(n, m - 1));
{

 Recursividad cruzada o indirecta: Son


algoritmos donde una función/acción provoca
una llamada a sí misma de forma indirecta, a
través de otras funciones/acciones.

Ejemplo
int par(int n){
if (n == 0) return 1;
return impar(n-1);
}
int impar(int n){
if (n == 0) return 0;
return par(n-1);
}

main(){
printf("Ingrese un numero natural:");
scanf("%d",&n);

if (par(n)) {
printf("El numero %d es par\n", n);
}else{
printf("El numero %d es impar\n", n);
}
}
2.Clasificación de funciones recursivas
3.Técnica de divide y vencerás
Los problemas divide y conquista por lo general particionan el problema en varios
subproblemas similares más pequeños, y tienen un costo adicional para combinar las
soluciones parciales. Por ejemplo, el binary search reduce un problema de tamaño n a
tamaño n/2. El MergeSort particiona un conjunto en dos, con un costo de combinación
igual a la mezcla ordenada de ambos conjuntos. El QuickSort particiona el conjunto en 3,
con un costo adicional de particionamiento y de concatenación.

function BSearch(Array A[1..*] of Integer; Integer x, Li, Ls): Integer


Interger c;
c = (Li+Ls) div 2;
if A[c]==x then
return c;
else if Ls<=Li then
return -1;
else
if A[c]>x then
return BSearch(A, x, Li, c-1);
else
return BSearch(A, x, c+1, Ls);
endIf
endIf
endFunction

Procedure MergeSort(Ref Array A[1..N] of Integer, Integer Li, Ls)

If Ls>Li Then

Integer c = (Li+Ls) div 2;

MergeSort(A, Li, c);

MergeSort(A, Li + c + 1, Ls);

Mezclar(A, Li, c, Ls); // mezcla ordenada de A[Li..c] con

// A[Li+c+1..Ls], y lo deja en A[Li..Ls]

EndIf

EndProc

Procedure Mezclar(Ref Array A[1..N] of Integer, Integer Li, c, Ls)

Array Aux[1..Ls-Li+1] of Integer;

Integer i,j,k;

j = Li; // índice del primer subarreglo


k = c+1; // índice del Segundo subarreglo

For i=1 To Ls-Li+1 Do

Select

j == c+1: // sub arreglo A[Li..c] ya tratado

Aux[i] = A[k]; k = k+1;

k == Ls+1: // sub arreglo A[Li+c+1..Ls] ya tratado

Aux[i] = A[j]; j=j+1;

j<= c and k <= Ls: // no se han terminado los subarreglos

// tomamos el más pequeño

If A[j]>A[k] Then

Aux[i] = A[k]; k=k+1;

Else

Aux[i] = A[j]; j=j+1;

EndIf

EndSelect

EndFor

// copiando Aux en el subarreglo A[Li..Ls]

For i=Li To Ls Do

A[i] = Aux[i-Li+1];

EndFor

EndProcedure

Procedure QuickSort(Ref Array A[1..*] of Integer, Integer N)


If (N > 1) Then
Integer S1, S2, S3;
Array L1,L2,L3[1..N] of Integer;
Integer x = A[1]; //el 1ro. u otro elemento
Menores(L1, A, x, S1); //L1 = { y en A : y < x }
Mayores(L2, A, x, S2); //L2 = { y en A : y > x }
Iguales(L3, A, x, S3); //L3 = { y en A : y = x }
// Si es el tamaño del arreglo Li
QuickSort(L1, S1);
QuickSort(L2, S2);
Concatenar(A,L1,L3,L2,N,S1,S3,S2); // A=L1+L3+L2
EndIf
EndProcedure

Procedure Concatenar(Ref Array Result[1..*] of Integer,

Array X,Y,Z[1..*] of Integer, Integer Sr, Sx, Sy, Sz)

// concatena los arreglos X,Y,Z de tamaños Sx,Sy y Sz resp.

// dejando el resultado en el arreglo Result de tamaño Sr

Integer i;

For i=1 To Sx Do

Result[i] = X[i];

EndFor

For i=1 To Sy Do

Result[Sx+i] = Y[i];

EndFor

For i=1 To Sz Do

Result[Sx+Sy+i] = Z[i];

EndFor

EndProcedure
4.Ventajas y desventajas de usar recursividad
Ventajas:

1) Permite obtener la solución a ciertos problemas de manera natural, sencilla,


comprensible y elegante.

COLOCAS UN EJEMPLO DE FACTORIAL USANDO CICLOS VS FACTORIAL CON


RECURSIÓN.

FACTORIAL ITERATIVO FACTORIAL RECURSIVO

#include <iostream.h> #include <iostream>

int a,b,factorial; int factorial(int n)


{
int main (void) if(n<2) return 1;
{ else return n * factorial(n-1);
cout << "Este programa calculara el factorial del }
numero entero que ingrese \nPor favor ingrese su
numero " <<endl; int main()
cin >> a; {
factorial=1;
for (b=1 ; b<=a ; b++) int num=0;
{ printf("::CALCULAR FACTORIAL::\n");
factorial=b*factorial; printf("Introduce un numero: ");
} scanf("%i",&num); //Pedir variable num
printf("tEl resultado es: %i\n", factorial(num));
cout << "El factorial del numero ingresado es " //Llama la funcion e imprime resultado
<<factorial<<endl; }

system("pause");
return 0;
}

2) La facilidad de poder comprobar o verificar que la solución es correcta gracias al


principio de inducción (que dice que si todas las llamadas recursivas de un programa
funcionan bien, entonces el módulo está bien).
EN C++ --- LLEVAR ESTO A PSEUDOFORMAL SEGUN LO VISTO EN LA CLASE PASADA

Desventajas
TIPS RECOMENDADOS

 Leer página 13 en adelante del archivo pdf denominado ayudaRecursion.pdf.


Aparece resultados de ejercicios intente hacerlos por su cuenta
 Busque quices y parciales de semestres anteriores resueltos preferiblemente. Si
no los consigue intente resolverlos y lléveselos hechos a su preparador para que
se los revise. Estos además también tienen ejercicios interesantes a realizar de
recursividad ,etc.
 Lea el material que se le envían el correo. No lo olviden.
 Intenten resolver la complejidad del ejercicio de las torres de hanoi. Seguramente
algún compañero de otras secciones ya lo tiene listo. Así usted ve como lo hicieron
en clases y usted intenta resolverlo con lo aprendido en clase.

Nota: Estas sugerencias no incitan al estudiante a que se olvide de leer el resto del
material enviado por el docente.
Miercoles 25 Clase 04- Backtracking. Esquema de
Soluciones
Motivación de la técnica:

1- Hay problemas para los que NO se conoce un algoritmo para su resolución o al menos, NO
cuentan con un algoritmo eficiente para calcular su solución en estos casos, la único posibilidad es
una exploración directa de todas las posibilidades.
O

Supongamos que tenemos que tomar una serie de decisiones pero…


 No disponemos de suficiente información como para saber cual elegir.
 Cada decisión nos lleva a un nuevo conjunto de decisiones.
 Alguna secuencia de decisiones (y puede que mas de una) puede solucionar nuestro
problema.
Necesitamos un método de búsqueda que nos permita encontrar una solución…
Inicialmente usted dirá: Bueno, voy a probar con todas las posibles combinaciones para resolver
el problema, que es lo más común y que generalmente es la primera idea que a uno se le suele
venir a la cabeza. Esta forma de resolver los problemas se llama Resolución por fuerza bruta

Ejemplo de resolucion por fuerza bruta: El caso de lanzar dos dados:

Sabemos que cada cada del dado tiene 6 caras es decir

D1={1,2,3,4,5,6}.

D2={1,2,3,4,5,6}.

Pero al lanzar los 2 dados al mismo tiempo podemos obtener cualquiera de las siguiente
combinaciones:

Ω={

(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),

(2,1),(2,2) (2,3) (2,4), (2,5) ,(2,6),

(3,1), (3,2), (3,3), (3,4), (3,5),(3,5), (3,6),

(4,1),(4,2),(4,3),(4,4),(4,5),4,6),

(5,1),(5,2) (5,3) (5,4), (5,5) ,(5,6),

(6,1),(6,2) (6,3) (6,4), (6,5) ,(6,6)

Es decir hay 36 posibles resultados

Backtracking=Fuerza bruta
 Es un método sistemático que itera a través de todas las combinaciones posibles del
espacio de búsqueda para cada aplicación particular.
 Siempre puede encontrar todas las posibles soluciones existentes

EL PROBLEMA ES EL TIEMPO QUE TOMA EN HACERLO

Pero. ¿existe una forma mejor de obtener la solución deseada?

Sí, si eliminamos la necesidad de obtener todas las combinaciones posibles del conjunto

¿Cómo se hace esto?

Cuando al comenzar a construir la solución nos encontramos con nodos o valores iniciales a partir
del cual sabemos que no vamos a alcanzar la solución, o leído de otro modo cuando sabemos que
no nos va a llevar a soluciones útiles. Este proceso de descartar estas soluciones que no son útiles
es lo que se denomina podar la rama completa de un árbol.

Entonces para tener que recorrer todas las soluciones e ir descartando de primera mano las
que sabemos no nos van a llevar a ningún lado o no nos interesan se utliza la técnica de
Backtracking.

Antes de entrar en la definción

4- Aplicaciones adicionales del backtracking en la


vida real
La técnica de Backtracking es usada en muchos ámbitos de la programación, por ejemplo, para el
cálculo de expresiones regulares o para tareas de reconocimiento de texto y de sintaxis de
lenguajes regulares. También es usado incluso en la implementación de algunos lenguajes de
programación, tales como Planner o Prolog y da soporte a muchos algoritmos en inteligencia
artificial.

Existen diversos algoritmos clásicos que se resuelven con algoritmos de backtracking: rompecabezas,
laberintos, permutaciones, problemas de las 8-reinas, crucigramas, Sudoku, problema de la mochila, problema
del agente viajero, etc.

Ejemplo de aplicación: Una agencia de viajes que determina la ruta más corta para viajar de
Venezuela a Australia por ejemplo. Pueden haber diversas rutas por las que usted puede pasar
pero hay una ruta que será la más económica en tiempo o en costo para usted, según sus
necesidades.
SALTO A SECCIÓN. CUANDO DEBE APLICARSE LA TÉCNICA DE BACKTRACKING

PROBLEMAS QUE SE AJUSTAN A ESTE ESQUEMA:

1- permutar caracteres de un string


Este es el ejercicio que utilizaría para hacer una corrida de backtracking

5-¿Cuando debe aplicarse la técnica de backtracking?


Este algoritmo debe ser aplicado a problemas donde existan elementos considerados ”candidatos
parciales de la solución” para permitir realizar verificaciones rápidas si dichos candidatos
pertenecen a la solución ´o no.

No resulta útil para conjuntos no ordenados, donde la exploración es total (explorar todos los
candidatos). Entonces, el punto clave de los algoritmos de backtracking es: descartar/seleccionar
rápidamente las soluciones invalidas/validas.

2-Backtracking
O en otras palabras el backtracking es: “Encontrar una solución intentándolo con una de varias
opciones. Si la elección es incorrecta, el computo retrocede o vuelve a comenzar desde el punto de
decisión anterior y lo intenta con otra opción.

La definición formal que esta en el material enviado por la coordinación de la


materia: En general se puede decir que el backtracking es un algoritmo general para
encontrar un conjunto de soluciones a un problema computacional donde se va creando de forma
incremental un conjunto de candidatos que formaran parte de una solución final. El algoritmo
explora los elementos de un conjunto y selecciona/descarta subconjuntos de candidatos que
pertenecen/no-pertenecen a la solución.

El algoritmo explora los elementos de un conjunto y selecciona/descarta subconjuntos de candidatos que


pertenecen/no-pertenecen a la soluci´on.

Visto de otro modo, la solución deseada debe expresarse como una n-tupla (x1, ..., xn) donde los xi
son elegidos de algún conjunto finito Si. Usualmente el problema a resolver requiere encontrar un
vector que maximice/minimice/satisfaga una función criterio P(x1, ..., xn). A veces, se busca todos
los vectores que satisfagan P.

En un algoritmo de backtracking con datos de entrada P a resolver se pueden definir cinco


funciones: aceptar, rechazar, primero, próximo y solución. Cada uno opera de la siguiente forma:
1. Inicio (P): Retorna el candidato parcial de la raíz del problema con datos P; sirviendo como
inicialización
2. Aceptar (P, C): Retorna true si el candidato parcial C es una solución de P, y falso en caso
contrario
3. Rechazar (P, C): Retorna true si un candidato parcial C no debería continuar su exploración o
b´usqueda de más candidatos
4. Primero (P, C): Extrae el primer elemento/componente del candidato parcial C llamado s
5. Próximo (P, s): Genera el pr´oximo conjunto de candidatos posterior a s
6. Solución (P, c): El subconjunto c 2 P se considera solución.

La llamada inicial del algoritmo se hace como Backtrack (P, Inicio (P)), donde Backtrack se puede
escribir como:
void B a c k t r a c k ( Set P , C )
if (R e c h a z a r ( P , C )) then A b o r t a r ( )
if (A c e p t a r ( P , C )) then S o l u c i o n ( P , C )
Set s = P r i m e r o ( P , C )
while not s . E s V a c i o ( ) do
Backtrack(P,s)
s=Proximo(P,s)
end
end

3-¿COMO FUNCIONA LA TÉCNICA DE BACKTRACKING?

Los algoritmos de vuelta atrás (backtracking) hacen una búsqueda sistemática de todas las
posibilidades, sin dejar ninguna por considerar. Cuando intenta una solución que no lleva a
ningún sitio, retrocede deshaciendo el último paso, e intentando una nueva variante desde
esa posición (es normalmente de naturaleza recursiva).

Estos algoritmos se pueden modificar fácilmente para obtener una única solución o todas las
soluciones posibles al problema dado.

Esta técnica ofrece un método para resolver problemas tratando de completar una o varias
soluciones por etapas. En cada paso se intenta extender una solución parcial de todos los modos
posibles, y si ninguno resulta satisfactorio se produce la vuelta atrás hasta el último punto donde
quedaban alternativas por explorar. Es una mejora de la búsqueda completa de soluciones y una
alternativa a los algoritmos voraces

Un algoritmo genérico de backtracking sería algo como esto:


Solución: Es una función que retorna verdadero si su argumento es una solución.

ProcesarSolución: Depende del problema y que maneja una solución

Sucesores: Es una función que dado un candidato genera todos los candidatos que son
exensiones de éste.

Terminar: En caso de que solo le interesa encontrar una solución.

También se dice que es posible clasificar un algoritmo de backtracking de acuerdo al tamaño del
subconjunto C solución:

EJEMPLO PARA FORMARSE UNA IDEA GENERAL DE COMO COMENZAR A ATACAR LOS
EJERCICIOS DE BACKTRACKING

2- El laberinto

Pseudocodigo algoritmo de backtracking:

Backtracking (A[], k){


Si Es_Solucion(A[], k) Entonces Procesar_Solucion(A[], k)
Sino
Por cada Encontrar_Sucesores(A[], k) hacer
Backtracking(A[], k+1)
Si terminar=true
Entonces return
}
Donde:
k=En el seudocódigo, es el número de elemento del arreglo, en el caso del laberinto caso va a
representar la ubicación en el árbol.
Es_Solucion() retorna verdadero o falso si el argumento es una solución posible, en nuestro caso
solo será verdadero cuando se llegue al fin del laberinto.
Procesar_Solucion(): depende del problema, en este caso sería terminar la búsqueda, ya que se
encontró la solución.
Encontrar_Sucesores(): es una función que dado un candidato, genera todos los candidatos que
son extensiones de este, es decir los caminos que puedo tomar a partir de esta celda.
terminar: es una variable global booleana inicialmente es falsa, que establece la condición de
terminar la búsqueda dado determinados casos (ej.: Número máximo de iteraciones).

Algoritmo implementado en el programa:

En los casos planteados se toma como posición inicial las coordenadas (1,1) siendo esta la que representa al
inicio del laberinto, por ende se toma como final del laberinto a la esquina opuesta, al llegar a esta la función
se deja de llamarse recursivamente y retorna el ultimo nodo.

La función cargar_posibles_caminos es vital para el funcionamiento del programa, esta se encarga de


verificar en qué dirección se puede seguir caminando en base a la posición actual y al paso anterior que
realizamos. Cada vez que una celda vecina esta libre y se puede avanzar hacia ella, esta función asigna
memoria y crea un nuevo nodo en función de los datos actuales y lo almacena en una de las ramas del nodo
actual.

Una vez cargados los caminos posibles, cada rama del nodo actual representa un camino que seguir, entonces
se evalúan una a una mediante la misma función de backtracking.
El hecho que la función retorne el último elemento del árbol, el cual representa el fin del laberinto hace
posible que a partir de este se recorra el árbol de forma inversa y se pueda construir un nuevo árbol de forma
muy simple eliminando los demás nodos que no pertenecen a la solución.
a). El problema del cambio:

Supongamos que el cajero sólo tiene billetes de 2000, 5000 y 10000 y nos debe dar 21.000
pts. Con una estrategia voraz nunca llegaría a la solución correcta (daría 2 de 10.000 y no
podría dar el resto), mientras que con backtracking podemos ir dando billetes (empezando
por el mayor valor posible) y si llegamos a una combinación sin solución, volvemos atrás
intentándolo con el segundo billete más grande, y así sucesivamente.

EJEMPLO BÁSICO DE APLICAR BACKTRACKING

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/**
* Progrmaa que calcula las permutaciones O Resultados a obtener para la cadena: abc
*abc
*acb
*bac
*bca
*cab
*cba
*/

void permutaciones(char *conjunto, size_t card, size_t elem);

int main(void)
{
char conjunto[] = "abc";
size_t card = sizeof conjunto - 1;

permutaciones(conjunto, card, 0);


system("pause");
return EXIT_SUCCESS;
}

void permutaciones(char *conjunto, size_t card, size_t elem)


{
if (card > 1){
int i;

permutaciones(conjunto, card - 1, elem + 1);

for (i = 1; i < card; i++){


char temp;

temp = conjunto[elem + i];


memmove(conjunto + elem + 1, conjunto + elem, i);
conjunto[elem] = temp;

permutaciones(conjunto, card - 1, elem + 1);

memmove(conjunto + elem, conjunto + elem + 1, i);


conjunto[elem + i] = temp;
}
}else
puts(conjunto);
}

correr y depurar programas con el editor dev-c++:


1. hacer clic con boton izquierdo del mouse en la linea a partir de la cual desean realizar la corrida
paso a paso
2. presionar el boton depurar
3. presionar SHIFT+F7 para realizar la corrida paso a paso

Para más información del uso del editor consulte el siguiente enlace:
http://programacionymetodos.blogspot.com/2012/05/depuracion-de-programas-dev-c.html

Tipos de Backtracking

La primera solución o Una solución


Este es el esquema general, donde el algoritmo se detiene y se devuelve al momento de
encontrar la primera solución, sin considerar el resto de las ramas del árbol de exploración, ni
siquiera que la que ha conseguido no sea la mejor. El siguiente “pseudocódigo” muestra una
plantilla del esquema general.
Otra definición: Cuando el algoritmo encuentre una solución, su ejecución finaliza. En este
enfoque el algoritmo se queda con la primera solución que consigue. ( aparece en la guía
elaborada por la coordinación o en el material complementario de la material)

Todas las soluciones


Explora todo el espacio de búsqueda para obtener el conjunto de todas las soluciones que
satisfacen el problema. Cada vez que se encuentre con una solución completa, la guarda en
una estructura de datos y continúa el recorrido hasta que no queden más posibilidades por
explorar. El esquema general se puede modificar para que adopte esta variante.

Otra definición: • El algoritmo colecta todos los subconjuntos encontrados en la exploración y


forman parte de la solución. .( aparece en la guía elaborada por la coordinación o en el
material complementario de la material)

La mejor solución o Solución Optima


Procesa todo el espacio de búsqueda, similar a la anterior, a diferencia en que a medida que
se van obteniendo nuevas soluciones se compararan con la mejor que se tenía hasta los
momentos, y de ser mejor se queda con la nueva.
Otra definición: Cuando el algoritmo explora todos los subconjuntos de soluciones posibles y se
queda con la `optima para el problema a resolver.( aparece en la guía elaborada por la
coordinación o en el material complementario de la material)

Las plantillas son una guía referencial

Los algoritmos que hacen uso de la técnica del backtracking no siempre tendrán la mismas
estructuras de una de las plantillas, siempre dependerán del problema. Lo importante es que
entiendas la técnica del backtracking y te enfoques a trabajar en función de una búsqueda
sistematizada.

En el esquema general la técnica backtracking encuentra una única solución,


independientemente de que hubiesen más, pero existen variantes del esquema que ofrecen
más posibilidades a la hora de buscar soluciones.

Ejemplos DE UNA SOLUCION- UNA SOLUCION

Un ejemplo clásico del backtracking es el problema del laberinto. El algoritmo básicamente


funciona asi

 1. Comprueba si la casilla donde está actualmente es la salida, si es así, sale de la


función indicando que se ha hallado la solución; de lo contrario pasa al siguiente paso.
 2. Si es posible visita las casilla de arriba llamando recursivamente a la misma función.
Comienza nuevamente en el paso 1.
 3. Si no es posible visitar o hallar la salida por arriba intenta con la casilla de la
derecha. Comienza nuevamente en el paso 1.
 4. Si no es posible visitar o hallar la salida por la derecha intenta con la casilla de
abajo. Comienza nuevamente en el paso 1.
 5. Si no es posible visitar o hallar la salida por abajo intenta con la casilla de la
izquierda. Comienza nuevamente en el paso 1.
 6. Si no se encontró la salida por ninguna de las cuatros casillas vecinas, entonces
sale de la función indicando que no existe salida del laberinto por la casilla actual.
 Alternativas: arriba, derecha, izquierda y abajo
 Subtarea: actualizar la posición del recorrido
 Solución: llegar a la posición de salida

OBTENER UNA SOLUCION

//C++

#include <iostream>

using namespace std;

//define el tamano del arreglo laberinto

#define F 7

#define C 8

//imprime el laberinto en el estado actual

void imprimir(char laberinto[F][C]){

for(int i=0; i<F; i++){

for(int j=0; j<C; j++)

cout<<laberinto[i][j];

cout<<endl;

cout<<endl;

//función backtracking

bool recorrer(char laberinto[F][C], int i, int j){

//comprueba si es solución

if(laberinto[i][j]=='S'){

cout<<"Solución!nLaberinto - estado final:"<<endl;

laberinto[i][j]='.';

imprimir(laberinto);

return true;

//marcar el camino

laberinto[i][j]='.';

//imprimir(laberinto);
//Alternativas

if(i-1>=0 && i-1<F && (laberinto[i-1][j]==' ' || //arriba

laberinto[i-1][j]=='S'))

if(recorrer(laberinto, i-1, j))

return true;

if(j+1>=0 && j+1<C && (laberinto[i][j+1]==' ' || //derecha

laberinto[i][j+1]=='S'))

if(recorrer(laberinto, i, j+1))

return true;

if(i+1>=0 && i+1<F && (laberinto[i+1][j]==' ' || //abajo

laberinto[i+1][j]=='S'))

if(recorrer(laberinto, i+1, j))

return true;

if(j-1>=0 && j-1<C && (laberinto[i][j-1]==' ' || //izquierda

laberinto[i][j-1]=='S'))

if(recorrer(laberinto, i, j-1))

return true;

//desmarcar el camino porque devolverá un paso

laberinto[i][j]=' ';

return false;

int main(){

//'#' = barrera

//'.' = marca del recorrido

//' ' = espacio libre

//'S' = salida

char laberinto[F][C]={{'#','#','#','#','#','#','#','#'},
{'#',' ',' ',' ',' ',' ','#','#'},

{'#',' ','#','#',' ',' ','#','#'},

{'#',' ','#','#','#',' ',' ','#'},

{'#',' ','#',' ','#','#',' ','#'},

{'#',' ','.',' ',' ','#',' ','#'},

{'#','#','#','#','S','#','#','#'}};

cout<<"nLaberinto - estado inicial:"<<endl;

imprimir(laberinto);

//recorrer(laberinto, posición inicial en fila, posición inicial en columna)

if(!recorrer(laberinto, 5, 2))

cout<<"No tiene soluciónn"<<endl;

system("pause");

TRAZA DEL PROGRAMA:


CLASE 07: LUNES 30-6 CLASE 05 - Ejercicios de backtracking

ejemplo de un problema con la mejor solucion


c) el problema de la mochila usando vuelta atrás:

El problema de la mochila consiste en llenar una mochila con una serie de objetos que
tienen una serie de pesos con un valor asociado. Es decir, se dispone de n tipos de
objetos y que no hay un número limitado de cada tipo de objeto (si fuera limitado no
cambia mucho el problema). Cada tipo i de objeto tiene un peso wi positivo y un valor
vi positivo asociados. La mochila tiene una capacidad de peso igual a W. Se trata de
llenar la mochila de tal manera que se maximice el valor de los objetos incluidos pero
respetando al mismo tiempo la restricción de capacidad. Notar que no es obligatorio
que una solución óptima llegue al límite de capacidad de la mochila.

Ejemplo: se supondrá:
n=4
W=8
w() = 2, 3, 4, 5
v() = 3, 5, 6, 10
Es decir, hay 4 tipos de objetos y la mochila tiene una capacidad de 8. Los pesos
varían entre 2 y 5, y los valores relacionados varían entre 3 y 10.
Una solución no óptima de valor 12 se obtiene introduciendo cuatro objetos de peso 2,
o 2 de peso 4. Otra solución no óptima de valor 13 se obtiene introduciendo 2 objetos
de peso 3 y 1 objeto de peso 2. ¿Cuál es la solución óptima?.

A continuación se muestra una solución al problema, variante del esquema para


obtener todas las soluciones.

void mochila(int i, int r, int solucion, int *optimo)


{
int k;

for (k = i; k < n; k++) {


if (peso[k] <= r) {
mochila(k, r - peso[k], solucion + valor[k], optimo);
if (solucion + valor[k] > *optimo) *optimo = solucion+valor[k];
}
}
}

Dicho procedimiento puede ser ejecutado de esta manera, siendo n, W, peso y valor
variables globales para simplificar el programa:

n = 4,
W = 8,
peso[] = {2,3,4,5},
valor[] = {3,5,6,10},
optimo = 0;
...
mochila(0, W, 0, &optimo);

Observar que la solución óptima se obtiene independientemente de la forma en que se


ordenen los objetos.

TRAZA DEL PROGRAMA:


EJEMPLO DE UN PROBLEMA CON TODAS LAS SOLUCIONES
Suma igual a N
Este problema fue el primero del que hablamos al inicio del tutorial.
Refresquemos en que consiste y revisemos la solución en código.
Considerando un conjunto X (conjunto={1, 2, 3}), encontrar todos los
subconjuntos que entre sus elementos sumen N (valorRef=4). Tomando en
cuenta el orden de los elementos; por ejemplo, el subconjunto {1, 3} será
distinto de {3, 1}.

 Alternativas: los dígitos del conjunto


 Subtarea: suma acumulativa
 Solución: suma igual a N, donde N en un número entero

//C++ int main(){


#include <iostream>
using namespace std;
int valorOptimo = 0, capacidad = 2;
int cantPesas = 3,
peso[] = {2, 3, 1}, mochila(0, capacidad, 0, &valorOptimo);
valor[] = {6, 15, 7};
cout<<"El valor optimo es "<<valorOptimo<<endl;
void mochila(int k, int capacidad, int valorAcum, int
*valorOptimo){ }
//alternativas
for(int i = k; i < cantPesas; i++)
if(peso[i] <= capacidad){
mochila(i, capacidad - peso[i], valorAcum +
valor[i], valorOptimo);
//comparación con la mejor solución
if(valorAcum + valor[i] > *valorOptimo)
*valorOptimo = valorAcum + valor[i];
}
}

TRAZA DEL PROGRAMA


.

http://www.widget-101.com/codigo/backtracking-
recursivo/
El resto de los ejercicios contenidos en la guía se haría una corrida GENERAL sin colocar
el algoritmo, ya que no alcanzaría el tiempo el hacer una corrida en frio utilizando los
algoritmos.

b) Problema de las 4 reinas:


“Sobre un tablero de ajedrez hay que colocar 4 reinas de forma que ninguna de ellas se
amenace.” Como cada reina estará en una fila diferente, podemos representar la solución
con un vector de columnas donde están situadas las según la fila.

Para resolverlo se situaría la dama sobre la primera fila y se colocaría la segunda


dama en un lugar donde no amenace a las demás. Si no encontramos solución parcial,
volvemos un paso atrás y nos replanteamos la solución de la etapa anterior (una nueva
posición).

ALGORITMO:
ALGORITMO:
ALGORITMO:
ALGORITMO:

ALGORITMO:
ALGORITMO: IDEM

ALGORITMO: IDEM
ALGORITMO: IDEM

ALGORITMO: ALGORITMO: IDEM

ALGORITMO: IDEM
ALGORITMO: IDEM

ALGORITMO: IDEM

ALGORITMO: IDEM
ALGORITMO: IDEM

ALGORITMO: IDEM

ALGORITMO: IDEM
EL CASO ANALOGO SERIA PARA El problema de las 8 reinas:

Enunciado: “Sobre un tablero de ajedrez hay que colocar 8 reinas de forma que ninguna de
ellas se amenace.” Como cada reina estará en una fila diferente, podemos representar la
solución con un vector de columnas donde están situadas las según la fila. Para resolverlo
se situaría la dama sobre la primera fila y se colocaría la segunda dama en un lugar donde
no amenace a las demás. Si no encontramos solución parcial, volvemos un paso atrás y nos
replanteamos la solución de la etapa anterior (una nueva posición).

Escenario inicial Uno de los casos solución

Void Reinas8( int i, int S[9], int *halladaSol)


{
int col=0;
*halladaSol = 0;
while(! (*halladaSol) || col != 8 )
col = col +1;
if( puede situarse la dama i en la posición [i,col]
sin estar amenazada por las anteriores)
S[i] = col;

if( i == 8 )
*halladaSol = 1;
else
Reinas8(i+1, S, halladaSol );
}

http://www.uhu.es/nieves.pavon/pprogramacion/temario/tema4/tema4.html

COMPLEMENTAR CON GUIA 5 Backtracking.pdf

EL PROBLEMA DE LAS n REINAS


REVISEN GUIA 5 Backtracking.pdf para obtener más información de este ejercicio.

REVISEN ESTE LINK: ES UN PROGRAMA QUE TIENE EL CODIGO FUENTE


EN EL LENGUAJE DE PROGRAMACION PROLOG Y EL ARCHIVO
EJECUTABLE QUE PUEDEN LLAMAR DE UNAVEZ SIN DESCARGAR EL
PROGRAMA

http://www.uhu.es/nieves.pavon/pprogramacion/temario/tema4/tema4.html

NOTA: El programa les pedira el tamañ del tablero(n)= pueden probar con n=4.

OJO CON LA NOTA DEL PORQUE EL PROGRAMA ARROJA 2 SOLUCIONES EN


LUGAR DE UNA, NO SE CONFUNDAN CON ESO.

d). El problema del agente viajante (PAV):

Podemos intentar todas las combinaciones posibles, obteniendo un árbol completo donde
los nodos terminales serían las ciudades de partida, y asociados a los mismos los kilómetros
asociados al camino desde el raíz a ese nodo.

Mejoras en el esquema de la vuelta atrás:

 Poda del árbol de vuelta atrás: Consiste en la eliminación de posibilidades (poda del
árbol):
o Exclusión previa: Tras un análisis previo, puede organizarse la búsqueda
para que ignore situaciones infructuosas (ej: problema 8 reinas, cuando
ponemos cada dama en una fila diferente).
o Fusión de ramas: Cuando la búsqueda a partir de diferentes ramas lleve a la
misma solución, podemos limitar la búsqueda (por ejemplo en problemas
con simetría como el de las 8 reinas o el del PAV).
o Ramificación y acotamiento: Cuando lo que se busca es una solución
óptima, es posible reducir el árbol de búsqueda abandonando las ramas que
se sepa con certeza que no llevan hacia soluciones óptimas. (por ejemplo en
el problema del viajante podríamos tener una variable T que almacene el
valor más óptimo hasta el momento, y abortar un camino en el mismo
momento en que se rebase T).
 Reordenación de la búsqueda: Consiste en reorganizar las ramas del árbol de
búsqueda de manera que se analicen 1º las situaciones con mejores expectativas.
Permite por tanto resolver más rapidamente el problema.

CLASE08: MIERCOLES 02- Introducción a la complejidad.


Notación O.

Вам также может понравиться