CONTENIDO DE LA LECCIÓN 11

Documentos relacionados
UNIVERSIDAD DE LOS ANDES NUCLEO UNIVERSITARIO RAFAEL RANGEL (NURR) DEPARTAMENTO DE FISICA Y MATEMATICA AREA COMPUTACION TRUJILLO EDO.

Programación II Recursividad Dr. Mario Rossainz López

4.1 Definición. Se dice que algo es recursivo si se define en función de sí mismo o a sí mismo. Un objeto (problemas, estructuras de datos) es

UNIVERSIDAD AUTÓNOMA CHAPINGO DPTO. DE PREPARATORIA AGRÍCOLA ÁREA DE FÍSICA RECURSIÓN. Guillermo Becerra Córdova

Tema 7: Recursividad

Recursividad. Definición de Recursividad: Técnica de programación muy potente que puede ser usada en lugar de la iteración.

UAA Sistemas Electrónicos Estructura de Datos Muñoz / Serna

Introducción Algoritmos de tipo dividir para vencer Algoritmos de rastreo Inverso. Recursividad. Programación Avanzada. 8 de septiembre de 2017

TEMA 5: Subprogramas, programación modular

Práctica 5.- Recursividad

ESTRUCTURA DE DATOS: Tema 3. Recursividad

ESTRUCTURA DE DATOS: Tema 3. Recursividad

Tema 06: Recursividad

M.C. Yolanada Moyao Martínez

UNIVERSIDAD DON BOSCO FACULTAD DE ESTUDIOS TECNOLÓGICOS ESCUELA DE COMPUTACION

Complejidad de algoritmos recursivos

Fundamentos de la programación

Unidad 2 Recursividad. 2.1 Definición 2.2 Procedimientos Recursivos 2.3 Ejemplos de Casos Recursivos

Pilas Motivación

Tema: Programación Dinámica.

Unidad 2 Recursividad. 2.1 Definición 2.2 Procedimientos Recursivos 2.3 Ejemplos de Casos Recursivos

Recursividad Definición

Análisis de algoritmos. Recursividad

La recursividad forma parte del repertorio para resolver problemas en Computación y es de los métodos más poderosos y usados.

Tema: Programación Dinámica.

Estructura de Datos. Recursividad. Primer Semestre, Indice

Qué es la recursividad?

Recursividad. Facultad de Ciencias de la Computación. Juan Carlos Conde R. Object-Oriented Programming I

Funciones Tipos de funciones y Recursividad

Concepto de Recursión. Características de algoritmos recursivos. Ejemplos

Programación 1 Tema 5. Instrucciones simples y estructuradas

Recursión. Capítulo 4

PROGRAMACION ESTRUCTURADA: Tema 3. Funciones

Funciones: Pasos por Referencia Recursividad

Cursosindustriales. Curso de C / C++ Por Deimos_hack

Semana Lenguajes 7de programación Tipos de lenguajes de programación

Programación I Recursividad.

n! = 1 2 n 0! = 1 (n+1)! = (n + 1) n!

PILAS. Prof. Ing. M.Sc. Fulbia Torres

CAPITULO 6: FUNCIONES

Funciones II. Fundamentos de Programación Fundamentos de Programación I

Tema: Funciones, Procedimientos y Recursividad en C#.

INSTITUTO NACIONAL SUPERIOR DEL PROFESORADO TÉCNICO - TÉCNICO SUPERIOR EN INFORMÁTICA APLICADA - PROGRAMACIÓN I

UNIDAD 7 Recursividad Concepto. Algoritmos recursivos. Seguimiento de la recursión. Algunos métodos recursivos de búsqueda y ordenación: M-Sort y

Paradigmas de lenguajes de programación. Introducción a la programación imperativa. Lenguaje C. Programación imperativa

Sentencias de Procesamiento Iterativo: while y do-while

Paso de parámetros. Universidad Europea de Madrid. Todos los derechos reservados.

Programación 2. Lección 3. Introducción a la recursividad

APUNTADORES. Un apuntador es un objeto que apunta a otro objeto. Es decir, una variable cuyo valor es la dirección de memoria de otra variable.

RECURSIVIDAD. Prof. Ing. M.Sc. Fulbia Torres

Estructuración del programa en partes más pequeñas y sencillas

INFORMATICA TECNICATURA DE NIVEL SUPERIOR ALGUNOS EJERCICIOS DE SELECCIÓN E ITERACION

Introducción a c++ Introducción a la programación EIS Informática III

CONTENIDO DE LA LECCIÓN 22

TECNICAS DE PROGRAMACION Universidad Católica Los Angeles de Chimbote RECURSIVIDAD Y SOBRECARGA DE METODOS

Tema: Funciones, Procedimientos y Recursividad en C#.

Tema: Funciones, Procedimientos y Recursividad en C#.

Diseño y Análisis de Algoritmos

Recursividad. 1.1 Concepto de Recursividad. Objetivos. Metodología de la Programación II Entender el concepto de recursividad.

Tema: Funciones, Procedimientos y Recursividad en C#.

Secuencias Calculadas

Algoritmos. Medios de expresión de un algoritmo. Diagrama de flujo

1. Una pila funciona según el método LIFO (Last In First Out ). Se define la clase Pila de la siguiente forma:

Fundamentos de la POO 1

1. Los objetos conocidos, es decir, aquellos objetos de los cuales poseemos información total o parcial útil en la búsqueda de los objetos desconocido

Una clasificación de los tipos de datos existentes en los diferentes lenguajes de programación se presenta a continuación:

Trabajo Práctico Nº 06

Procedimientos y funciones

Manual de referencia de C++ Parte IV Variables Punteros. Preparado por Prof. Luis A. Ortiz Ortiz

Fundamentos de Programación. Resolución de Problemas y Diseño de Programas. Fundamentos de Programación. Página 0 de 27

ALGORITMICA Y PROGRAMACION POR OBJETOS I

Programación 1. Tema II. Diseño de programas elementales. Lección 7. Diseño modular y descendente de programas

Tema 3. Estructuras de control

Recursividad... un análisis posterior. Jaime Gutiérrez Alfaro Introducción a la programación

Programación Unidad 5. Recursividad. Programación TIG - TUP 1. Sede Regional Orán UNIVERSIDAD NACIONAL DE SALTA

Objetivos. 1. Realizar exitosamente programas que involucren procesos que requieran iteraciones. Antecedentes

Laboratorio 5 Tema 7. Tipos de Datos Estructurados: Arreglos, Registros y Archivos

Estructuras dinámicas lineales (i)

Programación 1. Tema II. Diseño de los primeros programas. Lección 4. Diseño de algunos programas elementales

Informática PRÀCTICA 2 Curs

Este método de diseño de algoritmos en etapas, yendo de los conceptos generales a los de detalle, se conoce como método descendente (top-down).

Técnicas de prueba y corrección de algoritmos

Ejercicios sobre recursividad

U.A.B.C. Facultad de Ingeniería Programación Estructurada UNIDAD III

UNIVERSIDAD DON BOSCO FACULTAD DE ESTUDIOS TECNOLÓGICOS ESCUELA DE COMPUTACION

Lenguaje C, tercer bloque: Funciones

Metodología de la Programación II. Recursividad

UNIVERSIDAD AUTÓNOMA DEL ESTADO DE MÉXICO CENTRO UNIVERSITARIO UAEM ATLACOMULCO INGENIERÍA EN COMPUTACIÓN

Trabajo avanzado con consultas

FRACCION GENERATRIZ. Pasar de decimal exacto a fracción

PARTE II: ALGORÍTMICA

Algoritmos y Estructuras de Datos Curso 06/07. Ejercicios

Tema 07: Backtraking. M. en C. Edgardo Adrián Franco Martínez edgardoadrianfrancom

Programación Dinámica

SUBPROGRAMAS. Los subprogramas pueden ser invocados varias veces desde diferentes partes del programa.

Capítulo 4. Control de flujo. Continuar

Análisis de algoritmos

Transcripción:

CONTENIDO DE LA LECCIÓN 11 RECURSIVIDAD 1. Introducción 2 2. Ejemplos de recursividad 3 2.1 Ejemplos 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 3 3. Funcionamiento interno de la recursividad 11 3.1 Ejemplos 11.7, 11.8, 11.9, 11 4. Soluciones no recursivas (iterativas) 16 4.1 Ejemplos 11.10, 11.11, 11.12 16 5. El problema de las Torres de Hanoi 20 5.1 Ejemplo 11.13 22 6. Uso de las pilas para simular recursividad 23 7. El problema de las ocho reinas 26 7.1 Ejemplo 11.14 30 8. Casos interesantes 32 8.1 Ejemplo 11.15: Información genealógica 32 8.2 Ejemplo 11.16: Árboles similares 34 8.3 Ejemplo 11.17: Árboles no similares 36 9. Retroceso 36 10. Programación no determinista 36 11. Retroceso cronológico 37 12. Retroceso de dependencia dirigida 37 13. Uso de la recursividad 38 13.1 Examen breve 30 43 14. Lo que necesita saber 39 15. Preguntas y problemas 40 15.1 Preguntas 40 15.2 Problemas 40 11-1

LECCIÓN 11 RECURSIVIDAD INTRODUCCIÓN Una función que se llama a sí mismo se dice que es recursiva. La recursividad en las funciones puede darse de dos maneras diferentes: a. Directa. La función se llama directamente a sí misma. Por ejemplo, observe la figura 11.1, funciona() es una función y en alguna parte de ella aparece una llamada a sí misma: funciona() Llamada a funciona() Figura 11.1. Recursividad directa b. Indirecta. La función llama a otra función y esta a su vez llama a la primera. Por ejemplo, en la figura 11.2a la función funciona() llama a la función funcionb() y esta a su vez invoca a la primera; es decir el control regresa a funciona(). funciona() Llamada a funcionb() funcionb() Llamada a funciona() funciona() Llamada a funcionb() funcionb() Llamada a funcionc() funcionc() a) b) Figura 11.2. Recursividad indirecta También se da la recursividad indirecta cuando una función llama a otra y ésta a una tercera. Una vez ejecutada, el control regresa a la función que la llamó, lo mismo sucede en todos los niveles. Por ejemplo, en la figura 11.2b la función funciona() llama a la función funcionb(), y esta llama a la función funcionc() Cuando funcionc() concluye, el control regresa a funcionb(); y al terminar ésta, el control se transfiere a funciona() 11-2

En toda definición recursiva de un problema se debe establecer un estado básico (estado primitivo, estado de salida o estado de terminación) Es decir, un estado en el cual la solución no se presente de manera recursiva sino directamente. Además la entrada (datos) del problema debe ir acercándose al estado básico. Si dada la definición de un problema es posible determinar el estado básico y el acercamiento paulatino al mismo, entonces se puede llegar a una solución. En otro caso se estaría en presencia de una definición circular del problema. Es importante comprender que cada instanciación (copia activa) de una función recursiva es totalmente peculiar y tiene sus propios argumentos, variables locales, direcciones de retorno, etc. Objetivos de esta lección: Comprender y manejar el concepto de recursividad. Aprender como se escriben y utilizan funciones que se llaman a sí mismas. Resolver problemas utilizando recursividad. Los ejemplos que aparecen a continuación, a pesar de que pueden resolverse de manera no recursiva permiten ilustrar claramente el concepto de recursividad. EJEMPLOS DE RECURSIVIDAD Ejemplo 11.1 Solución Escriba una función recursiva C++ para encontrar la suma de todos los enteros desde 1 hasta N. Piense en esta operación por un minuto. No es una operación recursiva clásica? Para encontrar la suma de enteros de 1 a 5, podría adicionar 5 a la suma de enteros de 1 a 4? Entonces, para encontrar la suma de 1 a 4, adiciona 4 a la suma de enteros de 1 a 3, y así sucesivamente, correcto? Expresada en símbolos: sumanumeros(5) = 5 + sumanumeros(4) sumanumeros(4) = 4 + sumanumeros(3) sumanumeros(3) = 3 + sumanumeros(2) sumanumeros(2) = 2 + sumanumeros(1) sumanumeros(1) = 1 Observe que sumanumeros(1) es el estado básico, porque su valor es conocido. Ahora, traduciendo este proceso a una función recursiva, se obtiene: sumanumero s( n) 1 n + sumanumeros( n 1) Si Si n = n > 1 1 Esta expresión se puede expresar en seudocódigo como sigue: 11-3

Ejemplo 11.2 sumanumeros(n) si (n == 1) entonces regresar (1). si no regresar ( n + sumanumeros(n-1)). La función C++ se codifica entonces directamente desde el algoritmo como: /************************************************************* Esta función recursiva calculará la suma de los enteros entre 1 hasta N *************************************************************/ int sumanumeros(int n) if(n == 1) return (1) else return (n + sumanumeros(n 1)) // Fin de sumanumeros() Algoritmo 11.1. sumanumeros El siguiente programa: SUMAENT.CPP, muestra el uso del algoritmo 11.1 /* Este programa: SUMAENT.CPP, calcula la suma de los números enteros positivos comprendidos entre 1 y el número dado. */ #include <iostream.h> //Para cin y cout int sumanumeros(int n); void main(void) int n; /* Tener en cuenta que el valor devuelto es de la clase int. Por lo tanto la suma esta limitada a un máximo de 32767, en caso contrario se producirá un sobreflujo. */ cout << "Dame un número entero mayor que cero: "; cin >> n; cout << "\nla suma de los números enteros comprendidos entre 1 y " << n << " es: " << sumanumeros(n) << endl; int sumanumeros(int n) if(n == 1) return(1); else return(n + sumanumeros(n-1)); 11-4

Ejemplo 11.3 Factorial de un número: La notación n! se lee factorial de n e indica el producto de los enteros positivos desde 1 hasta n. Por ejemplo: 3! = 1 2 3 4! = 1 2 3 4 5! = 1 2 3 4 5 n! = 1 2 3... (n-2) (n-1) (n) También definimos 1! = 1 y 0! = 1. Si se le pide que desarrolle una función que calcule el factorial de un número, cómo le haría? Si invertimos la definición de la fórmula tenemos: n! = n (n-1) (n-2)... 3 2 1 Por lo tanto, 4! = 4 3 2 1. Observe que 3 2 1 es 3!; entonces, podemos definir 4! de manera recursiva como: En general, podemos definir n! como: 4! = 4 3! n! = n (n-1)! (n-1)! = (n-1) (n-2)! (n-2)! = (n-2) (n-3)!... Llegamos a definir entonces, el factorial de un número n en términos del factorial del número (n-1) de la siguiente manera: 1 n! = n ( n 1)! Si Si n = 0 n > 1 o n = 1 En la definición recursiva del factorial se da un estado básico (si n = 0 o n = 1, entonces el factorial vale 1), en el cual el problema ya no se define en términos de sí mismo sino que se asigna un valor directamente. En el paso recursivo de la fórmula se utiliza el concepto de factorial aplicado a (n-1) Por ser un número entero positivo será (n-1) un valor más cercano al estado básico (n = 0 o n = 1) Una vez establecida la definición recursiva de los números factoriales, podemos comenzar a formular un algoritmo recursivo. Considere el siguiente seudocódigo: factorial(n) INICIO x = n -1. calcular x!. // (n-1)! regresar(n x!). // n! = n (n -1)! FIN. La función factorial() estima el valor de n! calculando el valor de (n-1)! y luego multiplicando el resultado por n. Sin embargo, como ya habrá notado, la segunda instrucción no está definida de modo correcto: tenemos que encontrar una forma de calcular el valor de x!. Si lo piensa bien, verá que ya tenemos una: factorial() Esta función calcula el factorial de un número. Apliquemos este conocimiento y volvamos a escribir la rutina como: 11-5

factorial(n) INICIO x = factorial(n-1). // (n-1)! regresar(n x). // n! = n (n -1)! FIN. Ahora bien, cuando calcule el valor de n!, la función se llamará a sí misma de modo recursivo para estimar el valor de (n-1)!. Cabe preguntarnos si la función está completa. Mirémosla con detenimiento y analicemos su ejecución cuando calcula 2!. El procesamiento se inicia cuando se llama a la función con un argumento de 2. Entonces ésta calcula 2! recursivamente llamándose a sí misma con un argumento de 1; para estimar 1!, vuelve a llamarse a sí misma otra vez con un argumento de 0. La tercera copia (instancia) de la función se llamará a sí misma con un argumento de 1, la siguiente con 2, y así sucesivamente. El problema está más claro ahora: la función es recursiva hasta el infinito. Todas las funciones recursivas necesitan una forma de detenerse, a la que hemos denominado estado básico o primitivo. Se coloca, por lo común, en la parte superior de una función recursiva, y contiene instrucciones que al final terminan la recursividad y comienzan a desapilar todas las llamadas anidadas. Si se le omite o está incorrecta, como acabamos de ver, el funcionamiento puede convertirse en infinitamente recursivo. Volviendo a nuestro ejemplo, identifiquemos un estado básico para la función factorial(). Por definición, sabemos que 0! = 1 y que 1! = 1. Por lo tanto, podemos añadir revisiones para estos valores en la parte superior del procedimiento, de este modo: factorial(n) INICIO si(n == 0 o n == 1) regresar(1). FIN. regresar(n * factorial(n-1)). Ahora, cuando se le llame con un argumento de 0 o 1, la función devolverá un valor explícito en lugar de realizar otra llamada recursiva. (Observe que hemos eliminado la innecesaria variable temporal x de nuestro algoritmo) Pero aún tenemos otro problema. La función puede ser llamada al comienzo con un argumento negativo. Por lo tanto, debemos añadir una revisión más para asegurar que la función ha sido llamada de modo apropiado: factorial(n) INICIO si(n < 0) /* Revisa Argumentos erróneos */ regresar(-1). si(n == 0 n == 1) regresar(1). regresar(n * factorial(n-1)). FIN. A continuación se muestra la versión final escrita en C++. int factorialrecurrente(int n) if(n < 0) /* Revisa argumentos erróneos */ return(-1); 11-6

if(n == 0 n == 1) return(1); return(n * factorialrecurrente(n-1)); Algoritmo 11.2: factorialrecurrente Ejemplo 11.4 Serie de Fibonacci: Otro caso clásico de problemas definidos recursivamente es el cálculo de la serie de Fibonacci: 0, 1, 1, 2, 3, 5, 8, 13, 21,..., etc. Recuérdese que el Fibonacci de un número se obtiene de la suma de los dos números Fibonacci anteriores. A continuación se ilustrará el concepto de números Fibonacci: Por definición: Fibonacci(0) = 0 Fibonacci(1) = 1 Los demás números de la serie en base a la definición son: Fibonacci(2) = Fibonacci(1) + Fibonacci(0) = 1 + 0 = 1 Fibonacci(3) = Fibonacci(2) + Fibonacci(1) = 1 + 1 = 2 Fibonacci(4) = Fibonacci(3) + Fibonacci(2) = 2 + 1 = 3 La definición en forma recursiva de la serie de Fibonacci será: n Fibonacci ( n) = fibonacci( n 1) + fibonacci( n 2) Si Sí ( n = 0) n > 1 o ( n = 1) En este ejemplo el estado básico se presenta cuando n es cero o es uno. En el paso recursivo de la fórmula se utiliza el concepto de Fibonacci aplicado a (n-1) y (n-2) Por ser n un número entero positivo serán (n-1) y (n-2) valores más cercanos al estado básico. Empecemos ahora a construir una solución recursiva: fibonaccirecursivo(n) INICIO si(n < 0) regresar(-1). /* Argumento erróneo */ si((n = 0) o (n == 1)) regresar(n). FIN. regresar(fibonaccirecursivo(n-1) + fibonaccirecursivo(n-2)). Esta solución tiene un error, la verificación n < 0, que busca un argumento erróneo, solo es válida la primera vez, en llamadas subsecuentes a la función no tiene sentido. Por lo tanto se hace necesario realizar algunas modificaciones al algoritmo anterior: fibonaccirecursivoinicial(n) INICIO si(n < 0) regresar(-1). /* Argumento erróneo */ 11-7

FIN. regresar(fibonaccirecursivo(n)). fibonaccirecursivo(n) INICIO si((n == 0) o (n == 1)) regresar(n). FIN. regresar(fibonaccirecursivo(n-1) + fibonaccirecursivo(n-2)). La versión en C++ del algoritmo anterior es: int fibonaccirecursivoinicial(n) if(n < 0) return(-1); return(fibonaccirecursivo(n)); int fibonaccirecursivo(n) if((n == 0) (n == 1)) return(n); return(fibonaccirecursivo(n - 1) + fibonaccirecursivo(n - 2)); Algoritmo 11.3: fibonaccirecursivo Ejemplo 11.5 El siguiente programa: FIBONA1.CPP, ilustra el uso del algoritmo 11.3. /* El siguiente programa: FIBONA1.CPP, calcula los n términos de la serie de Fibonacci */ #include <iostream.h> //Para cout y cin int fibonaccirecursivoinicial(int n); int fibonaccirecursivo(int n); void main(void) int n; cout << "Dame el número de términos a calcular: "; cin >> n; cout << endl; while(n > 0) cout << "El valor del término " << n << " de la serie de Fibonacci es: " << fibonaccirecursivoinicial(n - 1) << endl; n = n - 1; 11-8

int fibonaccirecursivoinicial(n) if(n < 0) return(-1); return(fibonaccirecursivo(n)); int fibonaccirecursivo(n) if((n == 0) (n == 1)) return(n); return(fibonaccirecursivo(n - 1) + fibonaccirecursivo(n - 2)); Ejemplo 11.6 En este ejemplo presentamos el caso de una inversión en una institución bancaria. Se ha depositado un capital m por el cual se recibe un x% de interés anual. El problema consiste en determinar el capital que se tendrá al cabo de n años. Supóngase que m = $5000.00 y x = 10%, entonces: C 0 = 5000 C 1 = C 0 + C 0 * 10% C 2 = C 1 + C 1 * 10% [Capital Inicial] [Capital al finalizar el primer año de inversión] [Capital al finalizar el segundo año de inversión] En general, al cabo de n años se tendrá que: C n = C n-1 + C n-1 * 10% o C n = 1.10 * C n-1 Se llega a una definición recursiva del cálculo de inversiones. C n m = (1 + x)* C Sí n = 0 n 1 Sí n 0 > Nuevamente se cuenta con un estado básico, para n = 0, y un acercamiento a él en la otra expresión de la fórmula. El siguiente algoritmo presenta una solución recursiva del cálculo de una inversión: balance(n, m, x) FIN. INICIO si(n = 0) entonces regresar(m). //Estado básico sino regresar((1+x) * balance((n-1), m, x). Para simplificar el ejemplo: sí definimos el capital inicial m y la tasa x como variables globales; y la recapitalización es mensualmente tendremos: float balance(int m) if (n == 0) 11-9

return (m); else return((1 + x / 12 / 100) * balance(n 1)); //Fin de balance() Algoritmo 11.4: balance Esto es todo lo que hay que hacer! Esta función calculará al final de cualquier mes n que pase por la función. Observe cómo la función se llama a si misma en la cláusula else. Cuando la computadora encuentra la llamada recursiva en la cláusula else, deberá temporalmente retrasar el cálculo para evaluar la llamada de la función recursiva justo como se hizo el cálculo de interés compuesto. Cuando encuentra la cláusula else por segunda vez, la función se llama a sí misma otra vez y se mantiene llamándose cada vez que se ejecuta la cláusula else hasta que alcanza el estado primitivo. Cuando esto sucede, se ejecuta la cláusula if (porque n es cero), y la llamada recursiva termina. Ahora, insertaremos esta función en un programa llamado CAPITAL.CPP, para calcular el interés compuesto, como sigue: // Este programa: CAPITAL.CPP, ilustra el cálculo del interés compuesto de un capital inicial determinado #include <iostream.h> // Para cin y cout // Funciones prototipo y variables globales float balance(int); // Regresa nuevo balance float deposito = 0.0; // Capital inicial float tasa = 0.0; // Tasa de interés anual void main(void) // Define la variable argumento de la función int meses = 0; // Número de meses después del depósito // inicial para determinar el balance // Obtención del capital inicial, número de meses y tasa de interés cout << "Escriba el depósito inicial: $"; cin >> deposito; cout << "Escriba el número de meses (periodo): "; cin >> meses; cout << "Escriba la tasa de interés anual: "; cin >> tasa; // Establece la salida ajustada y la precisión cout.setf(ios::fixed); cout.precision(2); // Muestra resultados, haciendo la llamada a la función recursiva cout << "\n\ncon un depósito inicial de $" << deposito << " y una tasa de interés de " << tasa << "%\nel balance al final de " << meses << " meses deberá ser de $" << balance(meses) << endl; // Fin de main() 11-10

/*********************************************************************** Esta función recursiva calculará un balance de cuenta con base en una tasa de interés compuesto mensual. ************************************************************************/ float balance(int n) if(n == 0) return deposito; else return(1 + tasa / 12 / 100) * balance(n - 1); //Fin de balance() El programa anterior pide al usuario que escriba el depósito inicial, número de meses a calcular y la tasa de interés anual. Las variables deposito y tasa se definen en forma global para propósitos de ejemplo para que podamos concentrarnos en la función recursiva. El número de meses a calcular se pasa a la función como un parámetro por valor. Realmente las variables deposito y tasa podrían definirse en forma local para main() y pasar a la función como parámetros por valor. Esto se dejará como un ejercicio. Una vez que el usuario escribe los valores necesarios, se hace la llamada recursiva y el programa escribe el balance final. Aquí está una muestra de la salida del programa: Escriba el depósito inicial: $1000 Escriba el número de meses para calcular: 4 Escriba la tasa de interés anual: 12 Con un depósito inicial de $1000 y una tasa de interés de 12% el balance al final de 4 meses deberá ser de $1040.60 FUNCIONAMIENTO INTERNO DE LA RECURSIVIDAD Internamente se utiliza una estructura tipo pila para guardar los valores de las variables y constantes locales de la función que efectúa la llamada. De esta manera, una vez concluida la ejecución, se puede seguir con los pasos que quedaron pendientes recuperando los valores necesarios de la pila. Con cada llamada recursiva se crea una copia de todas las variables y constantes que estén vigentes, y se guarda esa copia en la pila. Además se guarda una referencia a la siguiente instrucción a ejecutar. Al regresar se toma la imagen guardada en el tope de la pila y se continúa operando. Esta acción se repita hasta que la pila quede vacía. Se tratará de ilustrar estos conceptos con ayuda de los ejemplos anteriores. Ejemplo 11.7 Retomando el problema del cálculo del factorial de un número n (véase el algoritmo 11.2), para mostrar el funcionamiento interno de la recursión. En la tabla 11.1 se presentan los valores que van adquiriendo las variables y los valores que son guardados en la pila, en el transcurso del cálculo del factorial de un número n = 4. PASO n PILA FactorialRecurrente 0 4 1 4 4*, 2 3 4*, 3*, 3 2 4*, 3*, 2*, 4 2 4*, 3*, 2* 1 5 2 4*, 3* 2(2*1) 6 3 4* 6(3*2) 7 4 24(4*6) Tabla 11.1. Cálculo recursivo del factorial 11-11

En el primer paso n es igual a 4; por lo tanto, siguiendo el algoritmo 11.2 debe hacerse 4 * 3! Ante la imposibilidad de realizar esta operación (porque primero debe calcularse 3!) se guarda el valor de n en la pila y se continúa trabajando con n = 3. Con el nuevo valor de n procedemos de manera similar que con n = 4. La diferencia radica en que la entrada, n, está más cercana al estado básico. En consecuencia, en el paso 2 se agrega el valor 3 a la pila para multiplicarlo posteriormente por 2! En el paso 3 se agrega el valor 2 a la pila. Una vez alcanzado el estado básico, paso 4, se comienza a extraer los valores almacenados en la pila y a operar con ellos. En el paso 5 se extrae de la pila el valor 2 y se multiplica por el factorial de 1, obteniendo 2! Luego se extrae el 3 y se multiplica por el factorial de 2, para obtener 3! Se continúa así hasta que la pila quede vacía, lo cual ocurre en el paso 7 donde se calcula el factorial de 4. En la figura 11.3 se presenta gráficamente lo descrito anteriormente. factorialrecurrente(4) = 24 4 * factorialrecurrente(3) = 24 6 3 * factorialrecurrente(2) = 6 2 2 * factorialrecurrente(1) 1 = 2 1 1 Ejemplo 11.8 Figura 11.3. Representación gráfica de la recursividad Retomando ahora el problema del cálculo de un número Fibonacci n (véase el algoritmo 11.3), para mostrar nuevamente el funcionamiento interno de la recursividad. En la tabla 11.2 se presentan los valores que van adquiriendo las variables y los valores almacenados en la pila, durante el cálculo del número Fibonacci n = 4. PASO n PILA FibonacciRecursivo 0 4 1 4 fibonaccirecursivo(2) +, 2 3 fibonaccirecursivo(2) +, fibonaccirecursivo(1) +, 3 2 fibonaccirecursivo(2) +, fibonaccirecursivo(1) +, fibonaccirecursivo(0) +, 4 1 fibonaccirecursivo(2) +, fibonaccirecursivo(1) +, 1 fibonaccirecursivo(0) +, 5 0 fibonaccirecursivo(2) +, fibonaccirecursivo(1) +, 1(1+0) 6 1 fibonaccirecursivo(2) +, 2(1+1) 7 2 fibonaccirecursivo(0) +, 2 8 1 fibonaccirecursivo(0) +, 3(2+1) 9 0 3(3+0) Tabla 11.2. Cálculo recursivo de un número Fibonacci 11-12

En la misma podemos observar que en este ejemplo se van guardando en la pila llamadas recursivas pendientes de ejecución. Para n igual a 4 se llama al proceso fibonaccirecursivo(3) y se guarda en la pila fibonaccirecursivo(2), paso 1. En el paso 2 se calcula fibonaccirecursivo(3), llamando al proceso fibonaccirecursivo(2) y almacenando en la pila fibonaccirecursivo(1) (una vez calculados, serán sumados) Se continúa de esta manera para los diferentes valores de n, hasta que éste sea 1 o 0. Cuando n es igual a 1, paso 4, se asigna directamente un valor a fibonaccirecursivo(1), ya que éste es un estado básico. Como se llegó a un estado básico, se comienzan a extraer sucesivamente de la pila los elementos dejados en ella. En el paso 5 se extrae de la pila a fibonaccirecursivo(0), el cual también tiene solución directa por ser un estado básico y se suma al número fibonacci obtenido en el paso anterior. Lo mismo sucede en el paso 6 al extraer fibonaccirecursivo(1) En el paso 7, al extraer fibonaccirecursivo(2) se procede de igual manera que en el paso 3. Es decir se llama a fibonaccirecursivo(1) y se almacena en la pila a fibonaccirecursivo(0) El proceso continúa hasta que la pila quede vacía, paso 9. En la figura 11.4 se presenta gráficamente lo descrito con anterioridad. 11-13

fibonaccirecursivo(4) = fibonaccirecursivo(3) + fibonaccirecursivo(2) = 3 MIGUEL Á. TOLEDO MARTÍNEZ fibonaccirecursivo(3) = fibonaccirecursivo(2) + fibonaccirecursivo(1) = 2 fibonaccirecursivo(2) = fibonaccirecursivo(1) + fibonaccirecursivo(0) = 1 fibonaccirecursivo(1) = 1 1 1 fibonaccirecursivo(0) = 0 0 0 fibonaccirecursivo(1) = 1 1 1 fibonaccirecursivo(2) = fibonaccirecursivo(1) + fibonaccirecursivo(0) = 1 fibonaccirecursivo(1) = 1 1 1 fibonaccirecursivo(0) = 0 0 0 Figura 11.4. Representación gráfica de la recursividad 11-14

Ejemplo 11.9 Para concluir esta serie de ejemplos sobre el funcionamiento interno de la recursividad, retomemos el problema del cálculo de una inversión en una institución bancaria (véase el algoritmo 11.4). En la tabla 11.3 se presentan los valores que van adquiriendo las variables y los valores almacenados en la pila, en el transcurso del cálculo de una inversión durante 4 años, con un capital inicial de $5000.00 y a un interés anual del 10%.(Nuevamente, para simplificar supondremos que los intereses se acumulan anualmente). PASO n PILA balance 0 4 1 4 1.10 *, 2 3 1.10 *, 1.10 *, 3 2 1.10 *, 1.10 *, 1.10 *, 4 1 1.10 *, 1.10 *, 1.10 *, 1.10 *, 5 0 1.10 *, 1.10 *, 1.10 *, 1.10 *, 5000 6 1 1.10 *, 1.10 *, 1.10 *, 5500(5000 * 1.10) 7 2 1.10 *, 1.10 *, 6050(5500 * 1.10) 8 3 1.10 *, 6655(6050 * 1.10) 9 4 7320.5(6655 * 1.10) Tabla 11.3. Cálculo recursivo de una inversión bancaria En el primer paso n es igual a 4, por lo tanto siguiendo el algoritmo debe hacerse 1.10 por el capital obtenido durante la inversión de los 3 años anteriores. Como no se dispone de este valor, se almacena 1.10 en la pila para operar con él posteriormente y se continúa con el cálculo de balance(3), paso 2. Como no se tiene aún el estado básico se procede de manera similar al paso anterior, es decir se almacena 1.10 en la pila y se llama a balance(2). Se continúa así hasta llegar al estado básico, paso 5, en el cual el capital a utilizar en el cálculo de la inversión es el inicial, y por lo tanto puede ser asignado directamente. En el paso 6 se extrae de la pila el valor 1.10 y se multiplica por el capital, calculando así la inversión obtenida al cabo de un año. Se continúa realizando esta operación mientras haya elementos en la pila. En el paso 9 la pila queda vacía y así se obtiene, finalmente, el monto total de la inversión al cabo de 4 años. En la figura 11.5 se presenta gráficamente lo antes descrito. En esta sección se ha presentado el funcionamiento interno de la recursividad. Aunque es un proceso transparente al usuario, es muy importante conocerlo para comprender debidamente la herramienta que se está utilizando. También se ha mencionado en los párrafos anteriores que la recursividad es un instrumento poderoso para definir objetos en términos de sí mismos. Usando recursividad es posible escribir un código más compacto y, en ciertos casos, de más fácil comprensión. Sin embargo, debe quedar claro que la recursividad no da mayor rapidez a la ejecución del código y tampoco permite ahorrar espacio de memoria. Con respecto a este último punto debe tenerse en cuenta que la recursividad, para su funcionamiento utiliza una pila interna la cual consume memoria. En general, es recomendable usar recursividad cuando el problema por su naturaleza así lo exige. Es decir, cuando la solución simplifica y facilita de modo notable usando esta herramienta. En otro caso es más conveniente, computacionalmente hablando, utilizar alguna técnica iterativa para resolver el problema. Los casos que se presentaron antes, resueltos por algoritmos recursivos, son problemas que pueden resolverse fácilmente de manera iterativa. Ahora es preciso ejemplificar cada uno de estos problemas, con su correspondiente solución iterativa. 11-15

balance(4) = 7320.50 7320.50 1.10 * balance(3) = 7320.50 6655.00 1.10 * balance(2) = 6655.00 6050.00 1.10 * balance(1) = 5500.00 1.10 * balance(0) = 5500.00 5000.00 5000.00 Figura 11.5. Representación gráfica de la recursividad SOLUCIONES NO RECURSIVAS (ITERATIVAS) Ejemplo 11.10 Basándose en el algoritmo 11.2: factorialrecurrente antes diseñado, elaboraremos un algoritmo no recursivo. factorialiterativo(n) /* Este algoritmo calcula el factorial de un número n donde n es un valor numérico entero, positivo o nulo. */ INICIO si(n < 0) entonces regresar(-1). // Valida datos erróneos. factorial = 1. mientras (n > 0) hacer // Ciclo para calcular n! Inicio factorial = n * factorial. n = n-1. Fin. FIN. regresar(factorial). 11-16

Ejemplo 11.11 La tabla 11.4 representa los valores que van adquiriendo las variables, durante el cálculo del factorial de un número n = 4. n factorial 4 1 3 4 2 12 1 24 0 24 Tabla 11.4. Cálculo iterativo del factorial A continuación se presenta un algoritmo no recursivo para el cálculo de los números de Fibonacci. fibonacciiterativo(n) /* Este algoritmo calcula el número Fibonacci correspondiente a n, donde n es un valor numérico entero, positivo o nulo. */ INICIO si((n = 0) o (n == 1)) entonces regresar(n).. si no Inicio fiba = 0. fibb = 1. i = 2. Fin. mientras(i <=n) Inicio Fin. fibo = fiba + fibb. fiba = fibb. fibb = fibo. i = i + 1. regresar(fibo). FIN. La tabla 11.5 representa los valores que van adquiriendo las variables, durante el cálculo del número Fibonacci n = 4. n fibo fiba fibb i 4 0 1 2 1 1 1 3 2 1 2 4 3 2 3 5 Tabla 11.5. Cálculo iterativo de un número Fibonacci A continuación presentamos un algoritmo con algunas variaciones respecto a la solución antes propuesta. Nuestro objetivo ahora es diseñar primero una función iterativa que acepte un número entero que no sea negativo como el argumento n y devuelva el valor F(n) La primera versión de nuestro seudocódigo puede ser así: 11-17

fibonacci(n) INICIO si (n = 0) regresar(0). si(n = 1) regresar(1). para i = 2 hasta n Inicio elementosiguiente = elementoanteanterior + elementoanterior. actualizar elementoanteanterior y elementoanterior. Fin. FIN. regresar(elementosiguiente). Observe que nuestro seudocódigo carece de algunos detalles relevantes: los valores iniciales de las variables, los incrementos para las variables que iteran (ciclos) y una prueba para detectar los argumentos válidos. Después de agregar esas instrucciones, el algoritmo queda así: fibonacci(n) INICIO si(n < 0) regresr(-1). si (n = 0) regresar(0). si(n = 1) regresar(1). elementoanteanterior = 0. elementoanterior = 1. para i = 2 hasta n Inicio elementosiguiente = elementoanteanterior + elementoanterior. ElementoAnteAnterior = elementoanterior. elementoanterior = elementosiguiente. Fin. FIN. regresar(elementosiguiente). Observe que hemos establecido la convención de devolver 1 para indicar un argumento erróneo. También observe que ya inicializamos y actualizamos las dos variables: elementoanteanterior y elementoanterior. Finalmente escribiremos un programa en C++ que llame a esta función. El programa se llamará FIBONA2.CPP. 11-18

/* Este programa: FIBONA2.CPP, resuelve en forma iterativa la serie de Fibonacci */ #include <iostream.h> const int MAXIMO = 10; //Para cout //Máximo n int fibonacci(int n); void main(void) for(int i = -1; i <= MAXIMO; i++) cout << "\ti: " << i << " fib(" << i << "): " << fibonacci(i) << endl; int fibonacci(int n) int i; int elementosiguiente, elementoanteanterior, elementoanterior; if(n < 0) return(-1); if(n == 0) return(0); if(n==1) return(1); elementosiguiente = 0; elementoanteanterior = 0; elementoanterior = 1; for(i = 2; i <= n; i++) elementosiguiente elementoanteanterior elementoanterior = elementoanterior + elementoanteanterior; = elementoanterior; = elementosiguiente; return(elementosiguiente); Ejemplo 11.12 Algoritmo no recursivo para el cálculo del capital obtenido en una inversión bancaria durante n años. balanceiterativo(n, m, x) /* Este algoritmo calcula el capital que se tendrá luego de invertir un capital inicial m, durante n años, con una tasa anual de x%. */ INICIO capital = m. i = 1. mientras(i <= n) hacer Inicio capital = capital + x * capital. i = i + 1. 11-19

Fin. FIN. regresar(capital). La tabla 11.6 presenta los valores que van adquiriendo las variables, en el transcurso del cálculo de una inversión durante 4 años con un capital inicial de $5000.00 a un interés anual del 10%. n m x capital i 4 5000 0.10 5000 1 5500 2 6050 3 6655 4 7320.50 5 Tabla 11.6. Cálculo iterativo de una inversión bancaria EL PROBLEMA DE LAS TORRES DE HANOI El problema de las Torres de Hanoi es un problema clásico de recursividad, ya que pertenece a la clase de problemas cuya solución se simplifica notablemente al utilizar recursividad. Tenemos tres torres: A, B y C, y un conjunto de cinco discos, todos de distintos tamaños. El enigma comienza con todos los discos colocados en la torre A de tal forma que ninguno de ellos cae sobre uno de menor tamaño; es decir, están apilados, uno sobre otro, con el más grande hasta abajo, encima de él el siguiente en tamaño y así sucesivamente (véase la figura 11.6, donde se muestra un ejemplo) El propósito del enigma es apilar los cinco discos, en el mismo orden, pero en la torre C. Durante la solución, puede colocar los discos en cualquier torre, pero debe apegarse a las siguientes reglas: Sólo puede mover el disco superior de cualesquiera de las torres. Un disco más grande nunca puede estar encima de uno más pequeño. Torre A Torre B Torre C Figura 11.6 Enigma de Las torres de Hanoi Intente resolver manualmente el enigma con una cantidad pequeña de discos, digamos cinco o seis, antes de proseguir con la solución algorítmica. El problema que enfrentamos es cómo escribir un programa que resuelva el enigma para cualquier cantidad de discos. Comencemos por considerar una solución general para n discos. Si tenemos la solución para n-1 discos, sería evidente que podemos resolver el problema para n discos: resolvemos el enigma para n-1 discos y luego movemos el disco n a la torre C. De modo 11-20

similar, si pudiéramos resolver para n-2 discos, el caso n-1 sería más sencillo. Podemos continuar de esta manera hasta llegar al caso trivial donde n = 1: simplemente movemos el disco de la torre A a la torre C. Aunque no sea obvio, lo que acabamos de describir es una solución recursiva para el problema, es decir, resolvimos el problema para una n dada en términos de n-1. Examinemos un ejemplo concreto y resolvamos el enigma con cinco discos. Supongamos que sabemos cómo resolverlo con cuatro discos, moviéndolos de la torre A a la torre B (utilizando C como torre auxiliar) Luego, para terminar la solución, sólo debemos mover el disco más grande de la torre A a la torre C y mover los cuatro discos de la torre B a la torre C (usando A como auxiliar). Podemos resumir la solución de un modo más preciso: 1. Si n = 1, movemos el disco de A a C y nos detenemos. 2. Movemos n-1 discos de A a B utilizando C como auxiliar. 3. Movemos el disco n de A a C. 4. Movemos n-1 discos de B a C usando A como auxiliar. Observe que los pasos 2 y 4 son recursivos porque nos sugieren que repitamos la solución para n-1 discos. También observe que las torres cambian su papel conforme avanza la solución. Ahora que comprendemos la solución, debemos convertir estas reglas en un algoritmo. Diseñaremos una función, llamada torres(), que mostrará los movimientos necesarios para resolver el enigma de acuerdo con un número dado de discos. Su salida estará constituida por comandos de este tipo: Mueve disco X de torre Y a la torre Z La función torres() necesita cuatro argumentos. El primero indica la cantidad de discos que se van a usar. Los otros tres determinan el papel que tendrán las tres torres: origen, destino y auxiliar. A continuación se muestra el seudocódigo que soluciona el enigma de Las torres de Hanoi. Observe cómo la rutina cambia el papel de cada torre con cada llamada recursiva. Sin embargo, la función carece de un detalle importante: no revisa valores erróneos que le fueron pasados como argumentos. Dejaremos esto como ejercicio. torres(entero n, carácter A, carácter B, carácter C) /* n: Cantidad de discos */ /* A: torre origen */ /* B: torre auxiliar */ /*C: torre destino */ INICIO si(n == 1) Inicio escribir( mueve disco, n, de la torre, A, a la torre, C). regresa. Fin. /* Mueve n-1 discos de la torre A a la torre B (C es e la torre auxiliar) */ torres(n-1, A, C, B). /* Mueve el enésimo aro de la torre A a la torre C */ escribir( mueve disco, n, de la torre, A, a la torre, C). 11-21

/* Mueve n-1 discos de la torre B a la torre C (A es la torre auxiliar) torres(n-1, B, A, C). FIN. regresar. Algoritmo 11.5: Torres El algoritmo recursivo anterior ofrece una solución clara y compacta al problema de las Torres de Hanoi. Es posible dar una solución iterativa a este problema, pero es conveniente mencionar que la misma es más complicada y extensa. Ejemplo 11.13 El siguiente programa: HANOI.CPP, ilustra el uso del algoritmo 11.5. Torres /* Este programa: HANOI.CPP, muestra el algoritmo para resolver el enigma de Las torres de Hanoi. */ #include <iostream.h> //Para cout y cin void torres(int n, char A, char B, char C); void main(void) int discos; char origen char auxiliar char destino = 'A'; = 'B'; = 'C'; cout << "Dame el número de discos: "; cin >> discos; cout << endl; torres(discos, origen, auxiliar, destino); void torres(int n, char A, char B, char C) /* n: Cantidad de discos */ /* A: torre origen */ /* B: torre auxiliar */ /*C: torre destino */ if(n == 1) cout << "Mueve disco " << n << " de la torre " << A << " a la torre " << C << endl; return; /* Mueve n-1 discos de la torre A a la torre B (C es la torre auxiliar) */ torres(n-1, A, C, B); /* Mueve el enésimo disco de la torre A a la torre C */ cout << "Mueve disco " << n << " de la torre " << A << " a la torre " << C << endl; /* Mueve n-1 discos de la torre B a la torre C (A es la torre auxiliar) */ torres(n-1, B, A, C); return; 11-22

Luego de realizar algunas pruebas, para distintos valores de n, se puede concluir que, para cualquier n, el número de movimientos está dado por la siguiente fórmula: numeromovimientos = 2 n -1 En la siguiente sección se estudiará cómo pueden plantearse soluciones no recursivas a problemas definidos en forma recursiva, usando pilas. USO DE LAS PILAS PARA SIMULAR RECURSIVIDAD Se ha presentado a la recursividad como una herramienta poderosa para crear algoritmos claros y concisos para problemas que se definen en términos de sí mismos. También se ha mencionado que el uso de la recursividad puede ser costoso en lo que a espacio de memoria se refiere. Por todo lo dicho y además, en consideración de que muchos lenguajes de programación no cuentan con esta facilidad, resulta conveniente contar con algún procedimiento que permita simular la recursividad. Una función puede tener variables locales y parámetros (argumentos) Cuando se hace una llamada recursiva a la función, los valores actuales de las variables y parámetros deben conservarse. Igualmente debe conservarse la dirección a la cual debe regresar el control una vez ejecutado la función llamada. En la función que se propone para simular recursividad sólo se conservan los valores de las variables y parámetros. Para ello se utilizan pilas. Nuevamente aparece el concepto de pila relacionado con recursividad. Una aplicación de éstas se estudió cuando se analizó la operación interna de la recursividad. Otra aplicación importante es el centro de atención de esta sección: simulación de recursividad mediante el uso de pilas. A continuación se presenta el problema de la Torres de Hanoi, pero con un enfoque no recursivo. Se verá cómo se puede modificar el algoritmo antes presentado para obtener una versión no recursiva del mismo. El algoritmo 11.5: Torres, trabaja con cuatro parámetros: n, origen, destino, auxiliar (el número de discos y los nombres de las tres torres que intervienen en el problema) Si se quieren conservar los valores de estos parámetros en cada llama, se deberá definir una pila para cada uno de ellos. (Otra alternativa sería definir una pila única, en la que cada elemento de la misma fuera capaz de almacenar los cuatro parámetros) Aquí se definirán cuatro pilas de trabajo: pilan: para almacenar imágenes de n. pilao: para almacenar imágenes de origen. pilad: para almacenar imágenes de destino. pilax: para almacenar imágenes de auxiliar. También será necesario manejar un tope para cada una de las pilas. El algoritmo siguiente es una solución iterativa al problema de las Torres de Hanoi. hanoiiterativo(n, origen, destino, auxiliar) INICIO /* Este algoritmo resuelve el problema de las torres de Hanoi de manera no recursiva. origen, destino y auxiliar son variables que almacenan los nombres de las tres torres, n representa el número de discos. 11-23

pilan, pilao, pilad y pilax son estructuras de datos tipo pila. tope es una variable de tipo entero. varaux es una variable de tipo carácter. bandera es una variable de tipo booleano. */ tope = 0. bandera = falso. mientras(n > 0) Y (bandera = falso) Inicio mientras(n > 1) Inicio /* Guardar en las pilas los valores actuales de los parámetros. */ tope = tope + 1. pilan[tope] = n. pilao[tope] = origen. pilad[tope] = destino. pilax[tope] = auxiliar. /* Simular llamada a Hanoi con n-1, origen, auxiliar y destino. */ Fin. n = n 1. varaux = destino. destino = auxiliar. auxiliar = varaux. FIN. Fin. escribir( Mover un disco de, origen, a, destino). bandera = verdadero. si(tope > 0) entonces // Las pilas no están vacías. Inicio // Se extraen los elementos de tope de las pilas. n = pilan[tope]. origen = pilao[tope]. destino = pilad[tope]. auxiliar = pilax[tope]. tope = tope 1. Fin. escribir( Mover un disco de, origen, a, destino). /* Simular llamada a Hanoi con n 1, auxiliar, destino y origen. */ n = n 1. varaux = origen. origen = auxiliar. auxiliar = varaux. bandera = falso. Algoritmo 11.6: hanoiiterativo 11-24

A continuación se presenta un conjunto de tablas donde se analiza el algoritmo 11.6: hanoiiterativo para diferentes valores de n. Todas las tablas tienen las mismas columnas y éstas representan lo siguiente: C1: representa el parámetro origen. C2: representa el parámetro destino. C3: representa el parámetro auxiliar. C4: representa la variable tipo pilan. C5: representa la variable tipo pilao. C6: representa la variable tipo pilad. C7: representa la variable tipo pilax C8: representa los movimientos de disco entre dos torres. Los escribiremos [x:y], y se interpretarán como que un disco ha sido movido de la torre x a la torre y. C9: representa la variable de control bandera, la cual puede tomar los valores de v (verdadero) y f (falso). En la tabla 11.7 se expone un seguimiento del algoritmo para n = 1. Paso N C1 C2 C3 tope C4 C5 C6 C7 C8 C9 0 1 A B C 0 Falso 1 [A:B] Verdadero Tabla 11.7. Solución iterativa del problema de las torres de Hanoi. En el paso 1 se escribió el movimiento necesario para alcanzar el estado final (C8), y se asignó a la variable de control el valor verdadero (C9) Como una de las dos condiciones evaluadas en el algoritmo es falsa, hemos llegado a la solución del problema. En la tabla 11.8 se presenta un seguimiento del algoritmo 11.6: hanoiiterativo para n = 2. Paso n C1 C2 C3 tope C4 C5 C6 C7 C8 C9 0 2 A B C 0 Falso 1 1 2 A B C 2 1 A C B 3 [A:C] Verdadero 4 2 A B C 0 [A:B] 5 1 C B A Falso 6 [C:B] Verdadero Tabla 11.8. Solución iterativa del problema de las Torres de Hanoi En el primer paso se guarda en las pilas (C4, C5, C6 y C7) una imagen de los datos (n, origen, destino y auxiliar) Se preparan las variables para realizar el movimiento de los (n 1) discos superiores de la torre origen a la torre auxiliar (C1, C2 y C3), paso 2. En el paso 3 se escribe el movimiento de la torre origen a la torre destino (C8) En el paso 4 se extraen los elementos del tope de las pilas y se asignan a sus correspondientes variables (n, C1, C2 y C3), con lo que se recuperan los valores almacenados en ellas en el paso 1. Se escribe también el movimiento de la torre origen a la torre destino (C8) En el paso 5 se preparan las variables para realizar el movimiento de los (n 1) discos superiores de la torre auxiliar a la torre destino (C1, C2 y C3) En el paso 6 se realizó el último movimiento de disco, necesario para alcanzar el estado solución (C8) 11-25

En la tabla 11.9 se presenta un seguimiento del algoritmo 11.6: hanoiiterativo para n = 3. Paso n C1 C2 C3 tope C4 C5 C6 C7 C8 C9 0 3 A B C 0 Falso 1 1 3 A B C 2 2 A C B 3 2 2 A C B 4 1 A B C 5 [A:B] Verdadero 6 2 A C B 1 [A:C] 7 1 B C A Falso 8 [B:C] Verdadero 9 3 A B C 0 [A:B] 10 2 C A B Falso 11 1 2 C B A 12 1 C A B 13 [C:A] Verdadero 14 2 C B A 0 [C:B] 15 1 A B C Falso 16 [A:B] Verdadero Tabla 11.9. Solución iterativa del problema de las Torres de Hanoi Una simple comparación entre los métodos recursivos e iterativos permite concluir que la solución recursiva del problema de las Torres de Hanoi es mucho más compacta y más fácil de comprender que la solución no recursiva. De cualquier manera es importante contar con un formalismo para traducir procesos recursivos en procesos no recursivos, porque, como ya se mencionó más arriba, algunos lenguajes no tienen esta herramienta de programación y además en ciertos problemas, el costo de usar recursividad es significativo. EL PROBLEMA DE LAS OCHO REINAS Otro ejemplo clásico de programación recursiva es el enigma de Las ocho reinas (o damas) El problema es colocar ocho reinas en un tablero de ajedrez de tal forma que ninguna ataque a las demás. En ajedrez una reina puede comerse a cualquier pieza desplazándose cualquier cantidad de casillas (escaques) por una fila, una columna o en diagonal( véase la figura 11.7). Por lo tanto, el dilema es colocar las ocho reinas en un tablero 8 x 8 sin que compartan la misma fila, columna o diagonal. Intente resolver manualmente el enigma antes de continuar. Para comenzar nuestra solución, vamos a suponer que desarrollamos el procedimiento reina(), que intentará colocar una reina en una fila mediante su propio parámetro fila. De este modo la función analizará todos los escaques de la fila especificada y, tras localizar uno que no esté bajo ataque, colocará la reina ahí; luego ella misma se llamará de modo recursivo para colocar otra reina en la siguiente fila. Si puede colocar las ocho reinas en el tablero, la función devolverá el valor RESUELTO. Si todos los escaques de la fila indicada están bajo ataque, entonces devolverá FALLO. Comencemos a esbozar el algoritmo (observe que para facilitar la programación, las filas y las columnas se indexarán de 0 a 7) 11-26

reina(fila) INICIO para i = 0 hasta i < 8 i incrementado en 1 //Revisa cada columna si (asalvo(fila, i) //Prueba escaque Inicio Colocar la reina en el tablero: fila, i. si(reina(fila+1) == RESUELTO) regresa RESUELTO. FIN. Fin. regresa(fallo). //Todos los escaques están bajo ataque R Figura 11.7 Filas, columnas y diagonales de ataque de la reina. Es evidente que la descripción está incompleta. Primero, necesitamos una condición para detener la recursión. Pensemos en esto unos instantes. Sabemos que, por definición, hay un máximo de ocho reinas en el enigma. Por lo tanto, podemos revisar fila > 7 desde el comienzo de la función; Pero reflexionemos un momento en la importancia del valor que contiene el argumento fila. Si hacemos una llamada recursiva a reina() con fila equivalente a un valor n, significa que hemos resuelto de la fila 0 a la fila n-1. Por lo tanto, si debemos llamar a reina() cuando fila = 8, significa que todas las demás reinas (filas 0 a 7) ya fueron colocadas y la función debe devolver el valor RESUELTO. Después necesitamos una forma de registrar la posición de las reinas a medida que procede la función. Para hacerlo, utilizaremos un arreglo de caracteres de 8 x 8, que hará las veces de tablero. En cada posición, guardaremos (con propósito de mostrarlo) uno de los siguientes caracteres: - (guión) para indicar un escaque vacío; * (asterisco) para señalar un escaque donde se encuentra una reina. Por último, debemos definir la función asalvo(), que determina si un escaque específico está bajo ataque. Pero pospongamos el análisis de asalvo() hasta que no terminemos de definir reina() Incorporemos los cambios que hemos sugerido y veamos cómo toma forma nuestra función: 11-27

reina(fila) INICIO si(fila > 7) regresa(resuelto). //Estado de salida FIN. //Revisa cada columna: de 0 a 7 para i = 0 hasta i < 8 en incrementos de uno en uno de i //Verifica si el escaque esta o no bajo ataque si(asalvo(fila, i)) Inicio //Coloca la reina en el tablero tablero[fila][i] = REINA. //Prueba la fila siguiente si(reina(fila + 1) = RESUELTO regresa(resuelto). si no tablero[fila][i] = VACIA. Fin. regresa(fallo). Algoritmo 11.7: reina Observe que hemos añadido la instrucción: tablero[fila][i] = VACIA. porque si alguna llamada recursiva a reina() falla en encontrar una solución (con una reina ubicada en esa posición), esta instrucción restaura el tablero a su estado anterior; Luego la función queda en libertad para revisar el siguiente escaque disponible. Nuestro algoritmo empieza a tomar forma, por lo que ahora debemos analizar cómo implementar la función asalvo() El problema que debemos plantear es cómo determinará la función si un escaque específico está bajo ataque de una reina colocada con anterioridad. Primero, observe que, en realidad, no es necesario revisar posibles ataques a lo largo de las filas. Gracias a nuestra implementación, podemos tener la certeza de que la única reina que puede permanecer en una fila es la que intentamos colocar. Además, la revisión de ataques desde las columnas puede hacerse de modo directo, burdamente, indexando el tablero junto con la columna que vamos a revisar. Los ataques diagonales son los más difíciles de resolver. Como se ilustra en la figura 11.8, una reina colocada en cualesquiera de los escaques rojos sería atacada por la reina colocada en el escaque [3,3] Qué tan fácil podemos determinar si un escaque está bajo ataque a lo largo de sus dos diagonales (ascendente y descendente)? 11-28

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 R Figura 11.8 Ataques diagonales Diagonal ascendente Diagonal descendente Si mira con atención el tablero de la figura 11.8, observará que cada diagonal puede identificarse de modo único como una función de sus índices. Por ejemplo, examinemos la figura 11.9a. La suma de los índices (fila + columna) de cada escape en la diagonal ascendente es igual a 6. Por lo tanto, cualquier reina que haya sido colocada antes en un escaque cuyos índices sumen 6 estará bajo el ataque de la casilla [3,3] De modo similar, podemos derivar un valor único para las diagonales descendentes (véase la figura 11.9b.) restando los índices (columna fila). Observe que cada una de las 15 diagonales ascendentes tienen un intervalo de valores que va de 0 a 14, y las diagonales descendentes uno de 7 a 7. 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 6 0 0 6 1 0 6 9 2-2 0 R 9 3-2 0 6 9 4-2 0 6 9 5-2 0 6 9 6-2 0 9 7-2 0 Figura 11.9a Diagonal Ascendente Figura 11.9b Diagonal descendente Podemos integrar este concepto en nuestra función asalvo() La idea es que cada vez que coloquemos una reina en el tablero actualicemos dos arreglos: uno que registre la diagonal ascendente y otro la descendente. El índice de cada arreglo será el valor de índice de cada diagonal (por comodidad para programar, sumaremos 7 al valor del índice de la diagonal descendente) Así, asalvo() sólo necesita revisar el arreglo de escaques apropiado para determinar si uno de éstos está bajo ataque desde alguna de sus diagonales. Debemos extender esta idea para rastrear ataques a lo largo de las columnas. En este caso, sólo usamos el valor de la columna como índice en el tercer arreglo. 11-29

Ejemplo 11.14 A continuación se muestra la solución completa del enigma, mediante el programa REINAS.CPP. Este programa conduce la rutina; inicializa el tablero y los arreglos de las banderas; luego llama a reinasiguiente() para que resuelva el enigma. Si esta última función devuelve RESUELTO, REINAS.CPP también llama a muestratablero() para que imprima la solución. /* Estre programa: REINAS.CPP, resuelve el problema de ubicar 8 reinas dentro de un tablero de ajedrez, sin que se ataquen la una con la otra. */ #include <iostream.h> #define FALLO 0 #define RESUELTO 1 #define VACIO '-' #define REINA '*' /* Banderas para revisar filas y diagonales */ char columnas[8]; char diagonalascendente[15]; char diagonaldescendente[15]; char tablero[8][8]; int reinasiguiente(int fila); void ponerbanderas(int fila, int columna); void restablecerbanderas(int fila, int columna); int asalvo(int fila, int columna); void muestratablero(); void main(void) int i, j; /* Inicializa tablero y banderas */ for(i = 0; i < 8; i++) for(j = 0; j < 8; j++) tablero[i][j] = VACIO; for(i = 0; i < 15; i++) diagonalascendente[i] = VACIO; diagonaldescendente[i] = VACIO; for(i = 0; i < 8; i++) columnas[i] = VACIO; /* Intenta resolver el problema de las ocho reinas */ if(reinasiguiente(0) == RESUELTO) muestratablero(); else cout << " No se hallaron soluciones!" << endl; 11-30