You are on page 1of 53

Programacin 2

Curso 2011/2012

Recursividad
Los trminos
recurrencia, recursin o recursividad hacen referencia a
una tcnica de definicin de conceptos (o de diseo de
procesos) en la que el concepto definido (o el proceso
diseado) es usado en la propia definicin
(o diseo).
Un ejemplo paradigmtico sera el del
tringulo de Sierpinski en el que cada
tringulo est compuesto de otro ms
pequeos, compuestos s su vez de la
misma estructura recursiva (de hecho en este caso se trata
de una estructura fractal)
Otro caso de estructura recursiva son
las denominadas Matryoshkas (o
muecas rusas): donde cada mueca
esconde en su interior otra mueca,
que esconde en su interior otra
mueca que , hasta que se llega a
una mueca que ya no escode nada.
En nuestro caso nos preocuparemos de los mtodos
(funciones o acciones) recursivos: aqullos en los que,
dentro de las instrucciones que los forman, contienen una
llamada a s mismos.
Como siempre, la parte ms compleja no ser a nivel de
programacin, sino a nivel de diseo: dado un problema,
ser capaz de encontrar una solucin recursiva del mismo.
Por tanto, deberemos ser capaces de pensar
recursivamente.
Algunos de los problemas que veremos ya los sabis
resolver iterativamente y es bueno comparar las soluciones
recursivas que veremos con las iterativas que podis
realizar por vuestra cuenta.

J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

1. Llamadas a funciones
Antes de empezar con las llamadas recursivas,
recordaremos brevemente cmo funcionan las llamadas
entre funciones y cmo stas modifican el flujo de
ejecucin.
Consideremos el siguiente ejemplo, que ya vimos en el
tema anterior:
1 /*
1 * File: SquareRoot.java
2 * --------------------3 * This program calculates the square root of a
4 * given positive integer
5 */
6
7 import acm.program.ConsoleProgram;
8
9 public class SquareRoot extends ConsoleProgram {
10
11 public int squareRoot(int n) {
12
int lower = 0;
13
while ((lower + 1) * (lower + 1) <= n) {
14
lower = lower + 1;
15
}
16
return lower;
17 }
18
19 public void run() {
20
int n = readInt("Enter a natural number: ");
21
int root = squareRoot(n);
22
println("The root is " + root);
23 }
24 }

Lo que vamos a considerar ahora es cmo se ejecutan las


lneas, en funcin de las llamadas entre funciones:
La ejecucin comienza la lnea 21, que contiene la
llamada a la funcin readInt. Se congela la ejecucin
del mtodo run y se ejecuta el cdigo de readInt
Poco podemos decir de la ejecucin de readInt ya que
no disponemos de su cdigo, pero a grandes rasgos,
despus de escribir el mensaje y esperar la entrada
del usuario, una vez ste ha entrado un nmero
J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

entero, se devuelve la ejecucin a la lnea 21 (en la


que habamos congelado la ejecucin), asignando el
valor devuelto por readInt a n
La ejecucin pasa entonces a la lnea 22, dnde se
llama al mtodo squareRoot. Se vuelve a congelar la
ejecucin de run y se pasa a ejecutar la lnea 13
Despus de unas cuantas vueltas (dependiendo del
valor de n) , se sale del bucle y se ejecuta la lnea 17,
volviendo al punto dnde nos habamos congelado la
ejecucin de run.

Qu pasara si, desde una funcin, llamramos a la propia
funcin? Pues que el punto de ejecucin pasara a la
primera instruccin de la funcin y que, cuando dicha
llamada retornase, continuaramos la ejecucin en el punto
en el que nos hubiramos quedado.
El diagrama de llamadas que se producen es:
run
n: 15
root: 3
"Enter a "
15

readInt

21

squareRoot

"The root is "

22

println

23

n: 15
lower: 0 1 2 3

En este diagrama, cada llamada viene representada por:


en la parte superior izquierda tenemos el nombre de la
funcin llamada
en la parte superior derecha tenemos la lnea a la que
regresaremos cuando salgamos de la funcin (dicha
lnea pertenece a la funcin llamante)
una flecha de entrada en la que se muestran los
valores de los parmetros
una flecha de salida en la que se muestra el resultado
(o nada si la funcin es void)
dentro de la caja los valores de las variables locales de
la funcin (que incluyen los parmetros). En el caso de

J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

squareRoot hemos marcado los sucesivos valores que


tiene la variable lower

J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

2. Pensar recursivamente: Los


textos palndromos
Una palabra (o texto) es palndroma si se lee igual de
izquierda a derecha que de derecha a izquierda (en el caso
de un texto no tomaremos en cuenta los posibles espacios
que separen las palabras).
Por ejemplo: Dbale arroz a la zorra el abad es, tal y como
podis comprobar, un texto palndromo1
Lo que queremos ser un programa tal que, dado un texto,
nos diga si es palndromo o no.
El programa principal bsicamente consistir en:
Pedir los datos al usuario. Como se tratar de un texto,
la forma natural de hacerlo ser con el mtodo
readLine
Eliminar los espacios de la cadena de entrada. Para
ello crearamos un mtodo removeSpaces tal que,
dado un String, devuelva otro, con los espacios
borrados2.
Llamar a la funcin que comprueba si el texto entrado
es palndromo. Llamaremos a esta funcin
isPalindrome, y ser una funcin que recibir como
parmetro un String y devolver un boolean.
Finalmente, dependiendo del valor devuelto por la
funcin anterior, se indicar si el texto es palndromo o
no.
Si escribimos esto en Java, tendremos:

1 Para simplificar, al introducir el texto obviaremos los


posibles acentos ortogrficos que pudieran tener las
palabras.
2 Para programarlo os podis inspirar en el mtodo
removeVocals del tema anterior.
J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

1 /* CheckPalindrome.java
2 * -------------------3 * Checks whether the entered text is palindrome.
4 */
5
6 import acm.program.ConsoleProgram;
7
8 public class CheckPalindrome extends ConsoleProgram {
9
10 public String removeSpaces(String text) {
11
// Ejercicio
12 }
13
14 public boolean isPalindrome(String text) {
15
// Se detallar ms adelante
16 }
17
18 public void run() {
19
String text = readLine(Enter text to check: );
20
text = removeSpaces(text);
21
if ( isPalindrome(text) ) {
22
println(Text is palindrome.);
23
} else {
24
println(Text is not palindrome.);
25
}
26 }
27 }

Una solucin iterativa


Antes de intentar solucionar el problema de forma
recursiva, vamos a ver cmo procederamos a hacerlo con
los conocimientos que tenemos, es decir, mediante una
solucin iterativa.
Qu es lo que hemos de hacer? Bsicamente comprobar
que:
el primer carcter de la cadena (posicin 0) coincide
con el ltimo (posicin longitud-1), y
el segundo (posicin 1), coincide con el penltimo
(posicin longitud-2), y
hasta llegar a la mitad de la cadena3
Para ello haremos un bucle que vaya generando las parejas
a comparar. En el momento de encontrar dos caracteres
3 Por qu solamente hasta la mitad? Considerad los casos
de textos de longitud par e impar al justificarlo.
J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

diferentes, ya podemos dar por acabada la comprobacin


(ya que sabemos que no lo es). Si al final no hemos
encontrado ninguna diferente, sabemos que se trata de un
palndromo. Es decir, se trata de un esquema de bsqueda.
1 public boolean isPalindrome(String text) {
2 int i = 0;
3 int length = text.length();
4 int maxPair = length / 2;
5 while ( i < maxPair &&
6
text.charAt(i) == text.charAt(length1-i) ) {
7
i = i + 1;
8 }
9 // If not found, isPalindrome is true
10 return ( i == maxPair );
11 }

Si comparamos la descomposicin del problema en


subproblemas tenemos:
problema inicial: ver si un texto es palndromo
una serie de problemas ms sencillos: comparar
parejas de caracteres.

Pensando una solucin recursiva


Una solucin recursiva del problema consistira en una
descomposicin en la que, para comprobar si una texto es
palndromo, nos surja como subproblema la necesidad de
saber si una parte de l lo es.
Recordad las muecas rusas: tenemos una mueca (saber
si un texto es palndromo) que al abrirla (descomponer el
problema) contiene dentro otra mueca ms pequea
(saber si una parte del texto es un palndromo).
En este caso, la descomposicin que podemos hacer es la
siguiente:para comprobar si un texto es palndromo o no:
miramos si los caracteres de los extremos (primero y
ltimo) son iguales
comprobamos si el texto formado por los caracteres no
extremos es palndromo
Como puede observarse, en la nueva descomposicin, uno
de los subproblemas que nos aparece es el mismo que el
original (pero sobre un texto ms pequeo).

J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

La necesidad de los casos simples

Si la nica posibilidad de solucionar un problema fuera


aplicar la regla recursiva, nunca acabaramos de
resolverlo!, ya que siempre nos quedara un subproblema
que resolver (al que aplicaramos la regla recursiva, que nos
dara otro subproblema, que nos dara , hasta el infinito).
Es por ello es necesario que existan casos que no
necesiten de aplicar la regla recursiva, sino que se pueden
resolver directamente. Son como la muequita rusa ms
pequea, que ya no contiene ms muequitas dentro. A
estos casos que pueden resolverse directamente se les
conoce como casos simples, en contraposicin de los
otros que se denominan casos recursivos.
En este caso, cundo podemos considerar que un texto es
palndromo sin necesidad de comprobar nada ms?
si un texto es vaco, podemos considerar que es
palndromo
si un texto consiste en solamente una letra, tambin
Juntando todo, ya tenemos todos los ingredientes que
necesitamos para programar nuestra versin recursiva de la
funcin:
1 public boolean isPalindrome(String text) {
2 if ( text.length() <= 1) {
3
return true;
4 } else {
5
char first = text.charAt(0);
6
char last = text.charAt(text.length()-1);
7
String inner = removeExtrems(text);
8
return (first == last) && isPalindrome(inner);
9}
10
11 public String removeExtrems(String text) {
12 // Ejercicio para el lector
13 }

Es cierto que en esta versin recursiva, en las sucesivas


llamadas recursivas dentro del mtodo removeExtrems,
creamos muchas copias de trozos de la cadena. Cuando
ms adelante veamos ms detalles de la clase String,
veremos que hay formas de evitar dichas copias 4. Tened
4 En la siguiente seccin veremos el uso de ndices que
permitira delimitar la parte de String a considerar sin
J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

presente que todava nos queda mucho por avanzar y que


no podemos pretender tener en cuenta todos los detalles.
La ejecucin del programa usando esta versin recursiva de
isPalindrome genera el siguiente diagrama de llamadas:
run

"Enter text "


readLine

19

"abcba"
"abcba"
removeExtrems

text: "abcba"

"abcba"
"abcba"

"Text is pal"

true

isPalindrome

21

println

22

text: "abcba"
rst: 'a'
last: 'a'
inner: "bcb"

20

true

"bcb"
isPalindrome

8
text: "bcb"
rst: 'b'
last: 'b'
inner: "c"

"c"

true

isPalindrome

8
text: "c"

Resumiendo
Intentemos resumir en una tabla las intuiciones que
obtenemos pensando en la imagen de las muecas rusas y
su equivalente en cuanto a la solucin recursiva de un
problema5:
Muecas rusas
Solucin recursiva
Una mueca puede abrirse
Un problema de
para ver qu es lo que hay
descompone en varios
en su interior.
subproblemas .
necesidad de ir generando un String diferente para cada
llamada recursiva.
5 Como toda metfora la coincidencia no es exacta, pero
puede ayudarnos a tener intuiciones.
J.M. Gimeno y J.L. Gonzlez

Programacin 2

Curso 2011/2012

Al abrir una mueca grande


encontramos muecas ms
pequeas en su interior

Al descomponer un
problema grande (casos
recursivos) encontramos
subproblemas que tienen la
misma estructura que el
problema inicial y trabajan
sobre datos ms pequeos
La mueca ms pequea ya Existen casos simples cuya
no contiene otras muecas
solucin no requiere
descomponerlos ms.
Slo hay dos tipos de
Entre los casos simples y los
muecas (las que contienen recursivos tengo todas las
otras en su interior y las ms posibilidades cubiertas
pequeas que no las
contienen).

J.M. Gimeno y J.L. Gonzlez

10

Programacin 2

Curso 2011/2012

3. Recursividad usando ndices


Una forma de no tener que ir generando mltiples copias de
partes del String en el caso del texto capica, consiste en
plantear la recursividad no sobre el String, sino sobre unos
ndices que nos indiquen el subconjunto de elementos sobre
los que estamos trabajando.
Como tanto los vectores como los Strings permiten acceder
directamente a una posicin, plantaeremos este ejemplo
sobre vectores y quedar como ejercicio hacer los cambios
necesarios para que funcione sobre Strings.

Cmo referirse a un subvector?

Supongamos que el String text, que ya no tiene espacios,


almacena el texto del que queremos ver si es palndromo, y
textChars el char[] correspondiente, es decir:
char[] textChars = text.toCharArray();
Adems denominaremos L al nmero de elementos del
vector, es decir:
int L = textChars.length;
Por tanto, el vector est formado por las posiciones 0 a L-1.
Para referirnos a un subvector de elementos consecutivos,
uno podra usar dos ndices: uno para el primer elemento
del segmento y otro para el ltimo, es decir 6:

rst

last

Pero la experiencia ha demostrado que elegir los ndices de


esta manera complica algunos algoritmos. Por ejemplo,
para representar un subvector vaco hemos de hacer que
ltimo sea ms pequeo que primero (last <first), el
nmero de elementos del segmento es last-first+1, lo que
es algo incmodo.
Existen dos formas tpicas para referirnos a un subvector:

6 Fijaos en que la posicin L cae fuera del vector. Por cierto,


L lo usaremos en los diagramas como alias de la longitud
del vector.
J.M. Gimeno y J.L. Gonzlez

11

Programacin 2

Curso 2011/2012

Dos ndices, uno que se refiera al primer elemento de


subvector (inf) y otro que se refiera al primer elemento
fuera del subvector (sup).
Un ndice que indique el primer elemento del
subvector (begin) y el nmero de elementos a
considerar (len)7.
Es decir,
len

begin

end

En este caso se cumple:


len == end begin
el subvector vaco: len == 0 y begin == end
el subvector total: len == L y begin == 0 y end == L
0 <= begin <= end <= L

Vectores palndromos
La funcin recursiva que realizaremos tendr la siguiente
forma:
1 public boolean isPalindromeArray(char[] textChars,
2
int begin,
3
int end) {
4
5 // Checks whether the subarray from begin to
6 // end-1 of textChars is palindrome
7}

Cul ser la descomposicin en este caso? Para ver si los


caracteres desde begin a end-1 de textChars forman un
palndromo hemos de:
comprobar si los caracteres extremos son iguales, es
decir hemos de comparar textChars[begin] y
textChars[end-1]
comprobar que el subvector sin los extremos, es
palndromo, es decir, hemos de hacer la llamada
7 Si recordis, esta es la forma que se utiliza para indicar
los elementos de un char[] a tener en cuenta cuando
construimos un vector.
J.M. Gimeno y J.L. Gonzlez

12

Programacin 2

Curso 2011/2012

recursiva con los parmetros textChars, begin+1 y


end-1
Como ya vimos anteriormente dicha descomposicin
solamente es posible si el subvector tiene longitud al menos
dos. En los casos de longitudes cero (vaco) o uno (un solo
carcter) sabemos que la funcin ha de retornar cierto.
Si lo juntamos todo, nos queda:
1 public boolean isPalindromeArray(char[] textChars,
8
int begin,
9
int end) {
10
11 // Checks whether the subarray from begin to
12 // end-1 of textChars is palindrome
13
14 if ( begin == end || begin == end-1) {
15
return true;
16 else {
17
return textChars[begin] == textChars[end-1] &&
18
isPalindromeArray(textChars, begin+1, end-1);
19 }
20 }

Teniendo en cuenta que end-begin da el nmero de


elementos en el segmento, la condicin del caso simple
puede expresarse tambin como ( end-begin <= 1). Fijaos
que en la llamada recursiva el tamao del intervalo es ms
pequeo.
Con esta nueva versin, el mtodo run quedar ahora
como:

J.M. Gimeno y J.L. Gonzlez

13

Programacin 2

Curso 2011/2012

1 public void run() {


28 String text = readLine(Enter text to check: );
29 text = removeSpaces(text);
30 boolean isPal = isPalindromeArray(text.toCharArray(),
31
0,
32
text.length());
33 if (isPal) {
34
println(Text is palindrome.);
35 } else {
36
println(Text is not palindrome.);
37 }
38 }

Se deja como ejercicio la implementacin usando como


parmetros begin y len, es decir, el ndice del primer
elemento del subvector y su tamao.
Observad que la llamada que hacemos, en la que begin es
0 y end es text.length(), comprueba si los caracteres de
todo el vector forman un palndromo, es decir:

L
end

0
begin

Descomposicin cuando uno de los


extremos est fijo

Cuando uno de los extremos, ya sea el de la izquierda como


el de la derecha, est fijo, solamente deberemos usar un
ndice. En estos casos, la descomposicin del vector ser:
Si el extremo izquierdo queda fijo:

pos

pos

Si es el extremo derecho:

Fijaos que en ambos casos hemos seguido la convencin de


que el extremo izquierdo forma parte del intervalo y, en

J.M. Gimeno y J.L. Gonzlez

14

Programacin 2

Curso 2011/2012

cambio, el extremo derecho es el primer elemento que ya


no forma parte de ese intervalo.

J.M. Gimeno y J.L. Gonzlez

15

Programacin 2

Curso 2011/2012

4. Un ejemplo sobre nmeros: la


exponenciacin
Vamos a aplicar nuestra idea de buscar recurrencias a aun
problema, esta vez sobre nmeros: dados dos nmeros a0
y b0, disear una funcin que calcule la exponenciacin
ab.

Solucin iterativa

Dado que ab no es ms que realizar b multiplicaciones de a,


hacemos un bucle tal que, a cada vuelta acumule
(multiplicando) sobre una variable (que inicialmente valdr
0) el producto de esa variable por a. Al final de b iteraciones
habremos multiplicado b veces por a y, por tanto,
tendremos la exponenciacin buscada.

Vamos a buscar una solucin recursiva


Recordad, la estrategia consiste en encontrar una
descomposicin de la exponenciacin que, a su vez, incluya
una exponenciacin. Una vez encontrada, deberemos
buscar tambin como mnimo un caso en que se pueda dar
el resultado sin necesidad de aplicar la recurrencia (si no, el
programa nunca acabara). Si con esos casos tenemos
cubiertas todas las posibilidades, hemos acabado.
En nuestro caso podemos ver que:
ab =aab 1 , si b 1
cuando b=0, a0=1
Es decir,
1 public long exp(long a, long b) {
2 if ( b == 0 ) {
3
return 1;
4 } else {
5
return a * exp(a, b-1);
6 }
7}

Hemos usado long en vez de int pues la exponenciacin


genera nmeros potencialmente grandes y con int
podramos tener problemas de precisin.
Esta solucin funciona pero, cuando b es grande, es
bastante ineficiente: a cada llamada se reduce b en una
J.M. Gimeno y J.L. Gonzlez

16

Programacin 2

Curso 2011/2012

unidad, por lo que para calcular la exponenciacin hemos


de hacer tantas llamadas como valor tiene el exponente 8.
Podemos hacerlo mejor?

Restar es lento, dividir es ms rpido


(para llegar a cero)
Si en vez de restar uno al exponente, lo dividimos por dos
(usando divisin entera), tendremos que hacer menos
llamadas hasta legar al caso simple. Por ejemplo, si
empezamos con b=10, tendremos:
Restando: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
Dividiendo: 10, 5, 2, 1, 0
Incluso en este caso (con un valor bajo de b) la diferencia
es significativa. Pensad que si duplico el valor de b, en el
primer caso se duplica el nmero de vueltas, mientras que
en el segundo solamente se aade una vuelta ms 9.

Podemos encontrar una recursin


dividiendo?
Como queremos dividir10, tendremos que encontrar una
relacin entre el ab original y Eb div 2 de la llamada
recursiva (dnde el E es una expresin que debemos
determinar).
Una posible forma de proceder es buscar una relacin entre
b y b div 2) , y luego aplicar algo de aritmtica.

Al dividir por dos, tenemos dos posibilidades, dependiendo


de si b es par o impar11:
Si b es par, la divisin entera es exacta, por lo que
b div 2)
tenemos que b es exactamente
2

8 Tcnicamente se dice que el coste temporal del algoritmo es lineal

respecto del valor del exponente. Ms en la asignatura de Algortmica


y Complejidad.

9 En este caso diremos que el coste temporal del algoritmo


es logartmico respecto del valor del exponente.
10 Cuando trabajamos con enteros usamos la divisin
entera a div b. En Java no existe un operador especial por
lo que cuando a y b son enteros, a/b es la divisin entera
entre a y b.
11 Recordad que estamos tratando con aritmtica de
enteros por lo que la divisin no siempre es exacta.
J.M. Gimeno y J.L. Gonzlez

17

Programacin 2

Curso 2011/2012

Si b es impar, la divisin entera pierde el valor del

b div 2)+1
2
Si aplicamos el anlisis anterior a la frmula ab , tenemos:
b div 2)

Si b es par,
2
a b=a
b div 2)+1

Si b es impar,
2
b

a =a
Es decir, en ambos casos, para calcular ab hemos de
calcular ( a2 ) b div 2 , tan slo que en caso de que b sea impar,

resto de la divisin, por tanto b es

deberamos multiplicar el resultado de la llamada recursiva


por a antes de devolverlo. Es decir:
1 public long exp2(long a, long b) {
2 if ( b == 0 ) {
3
return 1;
4 } else {
5
long result = exp2(a*a, b/2);
6
if ( b%2 == 1 ) {
7
result = a * result;
8
}
9
return result;
10 }
11 }

En este caso, es b quin decrece a cada llamada recursiva,


ya que cuando b>0 , se tiene que b/ 2<b (usando divisin
entera).

Trazando las llamadas


Aunque hayamos realizado un fino razonamiento sobre la
correccin de la funcin anterior, a veces necesitamos un
pequeo ejemplo de ejecucin para ver que, efectivamente,
funciona. Vamos a
ejecutar paso a paso el clculo de 713.
Observando el diagrama podemos observar que la
ejecucin de un algoritmo recursivo consiste en dos bucles:
uno que va desde la llamada inicial hasta el caso
simple y que va modificando los parmetros

J.M. Gimeno y J.L. Gonzlez

18

Programacin 2
uno que va desde la
solucin del caso simple
hasta regresar de la
llamada inicial y que va
propagando la solucin
Fijaos que conseguimos
estos dos bucles sin
explicitarlos de manera
alguna (con un while, o un
for).

Curso 2011/2012
7
13

7 * 13841287201
= 96889010407

exp2(7, 13)
7*7 = 49
13/2 = 6

Espacio usado por

13841287201

exp2(49, 6)
49*49 = 2401
6/2=3

2401 * 576801
= 13841287201
exp2(2401, 3)

un algoritmo
recursivo

2401 * 2401
= 5764801

5764801 * 1

Si miramos la traza de las


= 5764801
3/2
=
1
llamadas, veremos que, para
que la ejecucin de la
exp2(5764801, 1)
funcin pueda recorrer el
camino de vuelta, en algn
lugar se debern guardar los 5764801 * 5764801
valores de los parmetros y
= 33232930569601
1
de las variables locales de
1/2 = 0
las llamadas anteriores. Este
lugar es la pila de
exp2(33232930569601, 0)
ejecucin. Su nombre
responde a que, de la misma
manera que en una pila de
platos, solamente podemos colocar cosas encima de la pila,
y acceder al elemento de la cima (que es el ltimo que
hemos introducido).
Debido a que en la ejecucin de un mtodo recursivo
podemos tener que realizar muchas llamadas hasta
alcanzar los casos simples, puede darse la situacin de que

J.M. Gimeno y J.L. Gonzlez

19

Programacin 2

Curso 2011/2012

se llena la pila de ejecucin y se produce el error de


desbordamiento de pila (Stack Overflow).

J.M. Gimeno y J.L. Gonzlez

20

Programacin 2

Curso 2011/2012

5. Raz cuadrada (esta vez exacta)


Vamos a realizar el diseo de una funcin para calcular la
raz cuadrada de un nmero x 0 , pero usando nmeros
en coma flotante12.
Una primera cuestin a tener en cuenta es que, debido a
que hay errores de precisin, ya que el nmero de
decimales que se toman en cuenta es finito, no podemos
comprobar si el resultado es igual a la raz cuadrada sino
que es una aproximacin suficientemente buena.
Relacionado con lo anterior, un algoritmo del tipo:
comenzamos en 0.0 y vamos incrementado poco a poco
(por ejemplo en incrementos de 0.00001) hasta encontrar
la raz cuadrada no son aplicables13. Adems, estamos
buscando una solucin recursiva. Qu hacemos?

Reformular el problema

A veces, cuando un problema se nos


resiste, la solucin consiste en
replantear el problema en funcin de
otro que s sabremos resolver.
Desgraciadamente no existe una regla
que podamos seguir para encontrar la
reformulacin que nos garantice
encontrar el aj! adecuado para cada
problema. Lo nico que podemos hacer
es estudiar soluciones,
meditar sobre ellas, intentar generalizarlas y aplicarlas a
otras situaciones.
En este caso el aj! consiste en considerar lo siguiente 14:
tal y como se ha comentado antes, no podemos
buscar exactitud, sino una buena aproximacin
12 En el primer tema vimos el clculo de la raz cuadrada
entera, es decir, del nmero ms alto que, elevado al
cuadrado, es menor que el nmero dado. Es decir, la raz
cuadrada entera r de un nmero n cumple r 2 n<(r +1)2
13 No porque no logren la solucin, sino porque el tiempo
que emplearan en ello sera astronmico.
14 Tambin podramos habernos acordado teorema de
Bolzano sobre funciones continuas.
J.M. Gimeno y J.L. Gonzlez

21

Programacin 2

Curso 2011/2012

una buena aproximacin quiere decir que lo que


encontraremos est suficientemente cerca
que est suficientemente cerca quiere decir que est
dentro de un intervalo que incluye la solucin
intervalo que incluye la solucin,
ummm, intervalo, intervalo! , claro!, aja!
Vamos a intentar formalizar un poco estos conceptos para
ver si podemos esbozar el diseo que existe detrs de la
solucin. Comenzaremos con la idea de que el valor que
retornamos est suficientemente cerca de la solucin.

Formalizando suficientemente bueno

Formalmente, decir que la aproximacin r es


suficientemente buena quiere decir que r est
suficientemente cerca de x , es decir
| xr|<
para un valor de > 0 suficientemente pequeo (y que, por
tanto, define la precisin del resultado).
Aplicando la definicin de valor absoluto
| xr|< xr < si x r
r x < si x <r

Por la primera rama:


xr < x <r que, junto con la condicin de estar en este
caso ( x r ), hace que se deba cumplir

x <r x r
Anlogamente por la segunda rama
r x < r< x +
que, junto con la condicin de estar en este caso ( x<r ),
hace en este caso se deba cumplir:
x<r < x + r ( x , x+ )
Juntando las condiciones de ambas ramas, se tiene:
r ( x , x +)
Grficamente, para que r se considere un resultado
suficientemente bueno ha de estar en el siguiente intervalo:

Es decir, el objetivo es: dados x y encontrar un valor r


que est ah dentro. Por tanto la funcin que queremos
disear tendr la forma:
J.M. Gimeno y J.L. Gonzlez

22

Programacin 2

Curso 2011/2012

1 public double squareRoot(double x, double epsilon) {


2 ?
3}

Si solamente podemos variar estos dos parmetros no es


fcil encontrar una solucin recursiva. Por ello intentaremos
reformular el problema en otro que sea ms simple, y que
nos permita encontrar un valor suficientemente bueno para
la raz.
En concreto, para simplificar el problema, utilizaremos una
estrategia que tambin funciona en la vida real:
si el problema es muy complicado, pediremos alguna
pista adicional para simplificarlo.

Reformulemos el problema
Supongamos que nos dan como pista un intervalo,
delimitado por a y b , en el que nos dicen que ah
dentro est x . Es decir,
x [ a , b ]
Dicha condicin es equivalente a:
2

x [a , b ]

Por tanto, ahora, la funcin a disear ser 15:


1 public double squareRoot(double x,
2
double epsilon,
3
double inf,
4
double sup) {
5 ?
6}

Dentro de la funcin al lmite inferior ( a ) le llamamos inf y


al superior ( b ) sup.
Podemos usar esta informacin adicional en nuestro favor
para as encontrar una solucin recursiva al problema?

El intervalo es pequeo

Si el tamao del intervalo fuera ms pequeo que ,


cualquier valor del intervalo sera una aproximacin
adecuada de x .
Es decir:
15 Quedar por ver si podemos usar esta funcin para
calcular la que realmente nos interesa, en otras palabras, la
llamada inicial.
J.M. Gimeno y J.L. Gonzlez

23

Programacin 2

Curso 2011/2012

Como x est dentro de ese intervalo, cualquier valor del


intervalo est a una distancia menor que y, por tanto,
cualquiera nos sirve como aproximacin de la raz
cuadrada.
Ya hemos encontrado el caso simple !!

El intervalo es grande
Si el intervalo es grande, la idea es descomponerlo en
intervalos ms pequeos y preguntarnos en cual de ellos
estar x para que sea la llamada recursiva quin nos
resuelva el problema (recordad que solamente podemos
llamar a la funcin usando un intervalo que contenga el
valor de raz de x).

Cmo sabemos en qu lado est x ? Pues comparando


en valor de m2 con el de x y, si es menor, hemos de
seguir buscando por la derecha y, en caso contrario, por la
izquierda.
La funcin en Java quedara, por tanto:
1 public double squareRoot(double x,
7
double epsilon,
8
double inf,
9
double sup) {
10
11 double mid = (inf + sup) / 2;
12
13 if ( sup - inf < epsilon ) {
14
return mid;
15 } else if ( mid * mid < x ) {
16
return squareRoot(x, epsilon, mid, sup);
17 } else {
18
return squareRoot(x, epsilon, inf, mid);
19 }
20 }

Dos cosas a mencionar en el cdigo anterior:


J.M. Gimeno y J.L. Gonzlez

24

Programacin 2

Curso 2011/2012

El uso de la funcin Math.abs para calcular el valor


absoluto. La clase Math, del paquete java.lang,
contiene algunas funciones matemticas de uso
comn, como la que permite calcular el valor absoluto.
Como todas las dems clases del paquete java.lang no
hace falta hacer un import para usarlas (lo mismo
sucede con la clase String).
Fijaos en la forma de alinear los if, aunque el else de la
lnea 12 se corresponde con el if de la 10 se alinea con
el de la lnea 8. De esta manera visualmente vemos
que la estructura de control se corresponde en el
fondo con tres posibilidades (dos con la condicin
explicitada y una que es la que se toma cuando ambas
fallan).

La llamada inicial

Hasta el momento no ha habido demasiados problemas


para encontrar los parmetros a pasar en la primera
llamada. Para esta funcin, tendremos que pensar un peln
ms.
De las cuatro parmetros de la funcin, dos estn claros: x
y psilon. El problema viene para inf y para , ya que se
ha de cumplir
2
2
inf x .
para inf no hay demasiado problema dado que x 0
, lo que garantiza que podemos coger siempre inf =0
para est un peln ms complicado. Una posible
idea sera coger x , ya que normalmente x 2 x , pero
ello solamente es cierto si x 1 . En caso de no serlo,
podemos coger el mismo 1.0 como un valor mayor que
x

En resumen:
1 public double squareRoot(double x, double epsilon) {
25 return squareRoot(x, epsilon, 0.0, Math.max(1.0, x));
26 }

Dos comentarios:
el uso de la funcin Math.max, para decidir si usamos
un valor u otro para (tambin se podra usar un
condicional pero usar max o min en estos casos es
habitual).

J.M. Gimeno y J.L. Gonzlez

25

Programacin 2

Curso 2011/2012

el mtodo tiene el mismo nombre que el que tiene 4


parmetros. En Java es posible tener dos mtodos con
el mismo nombre, siempre que se puedan distinguir a
partir del nmero de parmetros o del tipo de los
mismos. A esto se le llama sobrecarga16.

16 Fijaos que esto mismo pasa con los mtodos print y


println, que se llaman igual independientemente de que lo
que escribamos sea un entero, un carcter, un String, etc.
J.M. Gimeno y J.L. Gonzlez

26

Programacin 2

Curso 2011/2012

6. Seno de un ngulo
Otro ejemplo de programa recursivo sobre nmeros en
coma flotante se puede plantear para el clculo del seno de
un ngulo a partir de:
1. Frmula del seno del ngulo triple

sin =3 sin 4 sin


3
3

( )

2. Aproximacin del seno para ngulos pequeos


lim

sin
=1

La segunda propiedad podemos reformularla de la siguiente


manera para que quede ms claro que nos permite resolver
el caso simple:
sin para 0

es decir, para ngulos suficientemente pequeos podemos


usar el propio ngulo como valor de su seno.
Observad que cuando hacemos la llamada recursiva con
/3 , lo hacemos sobre un valor menor que nos acerca al
caso simple.

J.M. Gimeno y J.L. Gonzlez

27

Programacin 2

Curso 2011/2012

1 public class SineProgram extends ConsoleProgram {


2
3 public double sine(double alpha) {
4
double epsilon = 0.00001;
5
if (Math.abs(alpha) <= epsilon) {
6
return alpha;
7
} else {
8
double sin3rd = sinus(alpha/3);
9
return sin3rd * (3 - 4 * sin3rd * sin3rd);
10
}
11 }
12
13 public void run() {
14
double[] angles = { 0.5, -2.0, 4.0, -5.0, 10000.0 };
15
for (int i = 0; i < angles.length; i++) {
16
double mySin = sine(angles[i]);
17
double javaSin = Math.sin(angles[i]);
18
println(" MySin: " + mySin);
19
println("JavaSin: " + javaSin);
20
println();
21
}
22 }
23 }

Dicho programa muestra:


MySin: 0.47942553860944637
JavaSin: 0.479425538604203
MySin: 0.9092974268237167
JavaSin: 0.9092974268256817
MySin: -0.7568024953326157
JavaSin: -0.7568024953079282
MySin: -0.9589242746422137
JavaSin: -0.9589242746631385
MySin: -0.30561450635509907
JavaSin: -0.30561438888825215
Fijaos en que la aproximacin, conforme aumenta el valor
del ngulo, es cada vez peor. Ello se debe a que esta forma

J.M. Gimeno y J.L. Gonzlez

28

Programacin 2

Curso 2011/2012

de calcular el seno del ngulo no es la mejor, desde el


punto de vista de la precisin.

J.M. Gimeno y J.L. Gonzlez

29

Programacin 2

Curso 2011/2012

7. Bsqueda binaria17 en un vector


ordenado
Intentemos aplicar esta idea de los intervalos que se van
dividiendo por la mitad a un problema parecido: la
bsqueda en un vector de enteros ordenado.
Similitudes:
al igual que los nmeros reales en el intervalo [inf,
sup], los elementos del vector estn ordenados.
buscamos un elemento dentro del intervalo
(inicialmente el intervalo es el vector desde las
posiciones 0 hasta la L-1).
Diferencias:
en el caso de la raz cuadrada sabemos con seguridad
que la solucin existe, es decir, existe un elemento
que es la raz cuadrada. En el caso de la bsqueda, el
elemento podra no existir dentro del vector.

Reformulacin del problema


De cara a tratar con el problema de qu devolver si el
elemento a buscar no se encuentra en el vector,
reformularemos el problema de la siguiente manera: dado
un vector de enteros ordenado crecientemente y un
elemento x , que puede o no pertenecer al vector,
encontrar la posicin pos del vector tal que marque la
separacin entre los que son x y los que son x .
Es decir:
x

>x

pos

Casos particulares:
si x pertenece al vector, ocupar la posicin pos
si x aparece varias veces en el vector, se dar la
posicin de ms a la derecha
si todos los elementos del vector son x , el valor
devuelto es -1
17 Tambin denominada bsqueda dicotmica.
J.M. Gimeno y J.L. Gonzlez

30

Programacin 2

Curso 2011/2012

si todos los elementos del vector son x , el valor


devuelto es L-1
si x no pertenece al vector, pero hay elementos x
y x , se devuelve una posicin del vector entre 0 y L1

Planteamiento de la solucin recursiva

La estrategia consistir en considerar que el vector est


dividido en tres zonas:
la izquierda, que contiene los elementos que sabemos
que son x
la central, que es la que desconocemos, y que
queremos hacer cada vez ms pequea
la derecha, que contiene los elementos que sabemos
que son x
Grficamente:
x

>x

left

right

Dos cosas a tener en cuenta:


fijaos en que los tres intervalos estn definidos de la
manera recomendada, es decir, la posicin del primer
elemento del intervalo y la posicin del primero que no
pertenece a l
cualquier zona puede estar vaca (por ejemplo
inicialmente las zonas de la derecha y de la izquierda
lo estn, ya que no sabemos nada)18.
se cumple: 0 L
A nivel de Java, la funcin recursiva tendr la forma:

18 Como ejercicio podis considerar los valores de left y


right para los casos particulares mencionados en el
apartado anterior.
J.M. Gimeno y J.L. Gonzlez

31

Programacin 2

Curso 2011/2012

1 public int binarySearch(int[] v,


27
int x,
28
int left,
29
int right) {
30 ?
31 }

Caso simple

Cundo podemos dar la respuesta sin tener que hacer


nada ms? Cuando la zona intermedia de los interrogantes
est vaca, es decir, cuando left=right, ya sabemos la
solucin left-1.
x

>x

pos

right
left

Caso recursivo

Ahora es cuando aplicaremos la estrategia que hemos


utilizado antes, es decir, dividiremos el intervalo por la
mirad y estudiaremos qu podemos hacer. Para calcular la
posicin media, haremos la misma divisin que antes, tan
solo que ahora se tratar de una divisin entera, es decir:

mid=()div 2 ;

Grficamente:
x

>x

left

mid

right

Lo que hemos de hacer depende de si el valor que hay en la


posicin mid es x o x .
Si v [mid ] x , podemos extender la zona izquierda
hasta incluir la posicin mid , es decir:
x

J.M. Gimeno y J.L. Gonzlez

mid

>x

left right

32

Programacin 2

Curso 2011/2012

Si v [ mid ] > x , ahora lo que podemos extender es la


zona de la derecha, es decir:
x

>x

left

mid
right

Dnde el ndice marcado en negrita es el que se pasa en la


llamada recursiva.
Una vez diseada la solucin, simplemente queda
programarla en Java e indicar los valores de la llamada
inicial. Al principio la zona desconocida ocupa todo el
vector, por lo que la llamada inicial se hace con left = 0 y
right = L

J.M. Gimeno y J.L. Gonzlez

33

Programacin 2

Curso 2011/2012

1 public int binarySearch(int[] v,


32
int x,
33
int left,
34
int right) {
35 int pos;
36 if ( left == right ) {
37
pos = left-1;
38 } else {
39
int mid = (left + right) / 2;
40
if ( v[mid] <= x) {
41
pos = binarySearch(v, x, mid+1, right);
42
} else {
43
pos = binarySearch(v, x, left, mid);
44
}
45 }
46 return pos;
47 }
48
49 public int binarySearch(int[] v, int x) {
50 return binarySearch(v, x, 0, v.length);
51 }
52
53 public boolean contains(int[] v, int x) {
54 int pos = binarySearch(v, x);
55 return pos != -1 && v[pos] == x;
56 }

Las llamadas se hacen sobre intervalos


ms pequeos
Lo nico que nos queda es comprobar que cuando hacemos
la llamada recursiva, lo hacemos sobre un intervalo ms
pequeo. Lo primero a tener en cuenta es que en los casos
recursivos (ya que siempre se cumple que y
adems sabemos que .
Caso v[mid]<=x:
Queremos demostrar que

( mid+1 )
( mid+1 ) mid+ 1 2 Lo cual es cierto para

mid

>

ya

que

div 2

J.M. Gimeno y J.L. Gonzlez

34

Programacin 2

Curso 2011/2012

Caso v[mid]>x:
Ahora se trata de probar que

mid >mid 2 Lo que es cierto para


>

= 2> 2

>

ya que

Es decir, en ambos casos el intervalo es menor.

J.M. Gimeno y J.L. Gonzlez

35

Programacin 2

Curso 2011/2012

8. La ordenacin rpida (algoritmo


quicksort)
El algoritmo quicksort, desarrollado
en 1960 por Sir Charles Antony
Richard Hoare es, en la prctica, el
ms eficiente algoritmo de
ordenacin.
La idea detrs del algoritmo es la
siguiente: si se descompone el
vector en dos partes, tales que
todos los elementos del subvector
izquierdo sean menores que los del
subvector derecho, se pueden
ordenar separadamente (in situ), y el vector resultante ya
quedar ordenado.
Grficamente:
"pequeos"

"grandes"

dnde con pequeos queremos decir que todos los


elementos del subvector izquierdo son menores 19 que los
del subvector grandes.
En este caso, para ordenar todo el vector, puedo hacer dos
llamadas recursivas: una sobre el subvector izquierdo y otra
sobre el derecho.
El caso simple tambin ser fcil ya que un vector de 0 1
elemento siempre estar ordenado.
Como hemos de poder delimitar subvectores que tanto
varen su lmite por la izquierda como por la derecha,
aadiremos dos ndices left y right para delimitar los lmites
del subvector que estamos ordenando, es decir:

19 Durante la presentacin del algoritmo supondremos que


estamos ordenando los elementos del vector de forma
creciente.
J.M. Gimeno y J.L. Gonzlez

36

Programacin 2

Curso 2011/2012

left

right

Ya podemos hacer un esbozo del algoritmo:


1 public void quickSort(int[] v) {
2 quickSort(v, 0, v.length);
3}
4
5 public void quickSort(int[] v, int left, int right) {
6 // 0 <= left <= right <= v.length
7 if (right left > 1) {
8
9
// Particionar el vector y devolver la posicin
10
// de corte.
11
12
// Llamadas recursivas:
13
quickSort(v, left, ?);
14
quickSort(v, ?, right);
15 }
16 }

Por tanto, si logramos disear una funcin para hacer la


particin del vector, ya tendremos solucionado el problema
de la ordenacin.

El problema de la particin

Una posible idea para particionar el vector de la manera


deseada, es escoger un elemento del vector p (al que se le
denomina pivote) y restructurar el subvector dado hasta
que todos los elementos menores o iguales que p estn a la
izquierda, y los mayores que p a la derecha. Es decir:
>p

left

pos

right

Para disear esta funcin, dividiremos el subvector dado en


tres partes:
la de los que sabemos que son p
la de los que sabemos que son >p
la de los que an no sabemos nada.
J.M. Gimeno y J.L. Gonzlez

37

Programacin 2

Curso 2011/2012

Es decir:

left

>p

inf

sup

right

En la que
el caso simple ser cuando la zona intermedia est
vaca (inf==sup) y el valor a retornar ser inf
la llamada inicial se realizar con inf==left y
sup==right.
En el caso recursivo, la estrategia es reorganizar todo el
subvector de en medio en base a reorganizar un subvector
menor. Adems, para que la llamada recursiva se realice
sobre un subvector de menor tamao, o bien ha de crecer
la zona de la izquierda, o la de la derecha, o ambas.
Para que crezca la zona de la izquierda, ha de pasar
que v[inf] <=p. En este caso, puedo reorganizar el
vector con la llamada recursiva sobre inf+1 y sup.
Para que crezca la zona de la derecha, anlogamente,
se ha de cumplir que v[sup-1]>p. En este caso, puedo
reorganizar el vector con la llamada inf, sup-1.
Qu pasa si no se cumple ningn caso de los
anteriores? Fijaos en que esto se produce cuando
v[inf]>p && v[sup-1]<=p.
En este caso puedo intercambiar los elementos de las
posiciones inf y sup-1 del vector v y reorganizar
recursivamente el subvector delimitado por inf+1 y
sup-1.
En Java, quedara:

J.M. Gimeno y J.L. Gonzlez

38

Programacin 2

Curso 2011/2012

1 public int partition(int[] v,


2
int pivot,
3
int inf,
4
int sup) {
5 if ( inf == sup ) {
6 return sup;
7 } else if (v[inf] <= pivot) {
8 return partition(v, pivot, inf+1, sup);
9 } else if (v[sup-1] > pivot) {
10 return partition(v, pivot, inf, sup-1);
11 } else {
12 swap(v, inf, sup-1);
13 return partition(v, pivot, inf+1, sup-1);
14 }
15 }
16
17 public void swap(int[] v, int i, int j) {
18 int tmp = v[i];
19 v[i] = v[j];
20 v[j] = tmp;
21 }

Unindolo todo
Usando la funcin de particin dentro de quickSort y
suponiendo que existe una funcin que, dado un subvector,
nos selecciona un elemento de l para usar como pivote,
nos queda:

J.M. Gimeno y J.L. Gonzlez

39

Programacin 2

Curso 2011/2012

1 public void quickSort(int[] v, int left, int right) {


57 // 0 <= left <= right <= v.length
58 if (right left > 1) {
59
int pivotValue = choosePivot(v, left, right);
60
int pos = partition(v, pivotValue, left, right);
61
quickSort(v, left, pos);
62
quickSort(v, pos, right);
63 }
64 }
65
66 public int choosePivot(int[] v, int left, int right) {
67 // Returns any element of v whose position is
68 // >= left and < right
69 }

Pero tenemos un pequeo problema

Recordad que una de las cosas que hemos de garantizar es


que las llamadas recursivas se hacen sobre subvectores
ms pequeos. En nuestro caso necesitamos garantizar que
pos left < right left pos < right
right pos < right left pos > left
Y vemos que la primera de ellas no es cierta (ya que podra
ser que no existieran elementos mayores que el pivote).
Cmo lo arreglamos? El truco consiste en, una vez
escogido el pivote, sacarlo del vector, partir el vector sin
el pivote y luego reintegrar el pivote que se haba
eliminado, para poder hacer la llamada recursiva sobre la
parte izquierda del vector, con un elemento menos.
Vemoslo grficamente:
Intercambiamos el primero con el pivote (p)
p
left

right

Partimos el subvector (sin el primer elemento, que es


el pivote)
partition(v, p, left+1, right)
p
left

right

Despus de partir, se cumple:


J.M. Gimeno y J.L. Gonzlez

40

Programacin 2

Curso 2011/2012
>p

p
p
pos

left

right

Si ahora intercambiamos left y pos-1, queda:


>p

p
p
left

pos

right

Y podemos asegurar que el subvector de la izquierda, en el


peor de los casos, tendr un elemento menos.
En java:
1 public void quickSort(int[] v, int left, int right) {
70 // 0 <= left <= right <= v.length
71 if (right left > 1) {
72
int pivotPos = choosePivotPosition(v, left, right);
73
int pivotValue = v[pivotPos];
74
swap(v, left, pivotPos);
75
int pos = partition(v, pivotValue, left+1, right);
76
swap(v, left, pos-1);
77
quickSort(v, left, pos-1);
78
quickSort(v, pos, right);
79 }
80 }
81
82 public int choosePivotPosition(int[] v,
83
int left,
84
int right) {
85 // One possible pivit possition can is the
86 // middle of the subvector.
87
88 return left + (right - left) / 2;
89 }

Consideraciones finales
En nuestro caso hemos diseado la funcin partition
como una funcin recursiva. Por cuestiones de
eficiencia casi siempre la veris implementada
iterativamente.
J.M. Gimeno y J.L. Gonzlez

41

Programacin 2

Curso 2011/2012

La eleccin del pivote que hemos hecho, el punto


medio del subvector, es una de muchas posibles.
Otras posibilidades:
o Escoger una posicin al azar entre left y right-1
(ms adelante durante el curso veris cmo usar
un generador de nmeros aleatorios).
o Una mala eleccin sera escoger siempre left
como pivote, ya que en vectores casi-ordenados
el rendimiento sera psimo (todo esto lo veris
en la asignatura de Algortmica y Complejidad)
Hay muchsimas variaciones posibles en la manera de
partir el vector, en el lugar dnde apartamos el
pivote, etc, etc. por lo que hay que entender el porqu
de los pasos dados y ser capaces de rehacer el
algoritmo
o En otras palabras, no tiene sentido que intentis
aprenderos ste (o cualquier otro) algoritmo de
memoria.

J.M. Gimeno y J.L. Gonzlez

42

Programacin 2

Curso 2011/2012

9. Las torres de Hani

El puzle de las Torres de Hani consiste en hacer pasar los


discos, que inicialmente estn en la primera torre, a la
segunda, usando la tercera como auxiliar. Eso s,
cumpliendo dos reglas:
solamente puede moverse el disco superior de una
torre (el que est encima de todo)
no podemos poner un disco sobre otro de menor
tamao
Lo que se pide es una funcin que escriba los movimientos
a realizar para resolver un puzle con un nmero de discos
dado.
Para referirnos a un movimiento solo tendremos que indicar
las torres origen y destino, ya que solamente podr
moverse el disco superior de la torre origen y quedar
colocado sobre todos los que ya existen en la torre destino.

Planteamiento recursivo
Una forma de encontrar la solucin consiste en estudiar las
condiciones bajo las cuales puedo mover el disco ms
grande:

J.M. Gimeno y J.L. Gonzlez

43

Programacin 2

Curso 2011/2012

ha de ser el nico que haya en la torre origen


la torre destino sta ha de estar vaca
todos los discos han de estar en la torre auxiliar
Lo veis? Para poder mover el disco ms grande de los n
desde la torre origen a la destino, he de haber movido
previamente los n-1 discos desde la torre origen a la
auxiliar.
Una vez movidos los n-1 discos que lo obstruyen y movido
el ms grande a la torre destino, hemos de mover los n-1
discos que estn en la torre auxiliar a la torre destino,
usando como torre auxiliar la torre origen inicial.

Casos simples

Cundo puedo solucionar el problema directamente?


Cuando solamente he de mover un disco, no he de apartar
nada no usar la torre auxiliar: lo muevo directamente.

Escribiendo la solucin en Java


La funcin tendr tres parmetros:
numDisks, que ser el nmero de discos a mover
from, nombre de la torre de partida
to, nombre de la torre de destino
using, nombre de la torre auxilar

J.M. Gimeno y J.L. Gonzlez

44

Programacin 2

Curso 2011/2012

1 public void solve(int numDisks,


2
String from,
3
String to;
4
String using) {
5
6 if ( numDisks == 1 ) {
7
println(Move disk from + from + to + to);
8 } else {
9
solve(n-1, from, using, to);
10
println(Move disk from + from + to + to);
11
solve(n-1, using, to, from);
12 }
13 }

Visualizacin de las llamadas


Por ejemplo, para 3 discos que se quieren mover de la torre
A a la B usando C como auxiliar haramos la llamada
solve(3, A, B, C)
que escribira:
Move disk from A to B
Move disk from A to C
Move disk from B to C
Move disk from A to B
Move disk from C to A
Move disk from C to B
Move disk from A to B
Si hacemos un grfico de las llamadas que se producen nos
queda el siguiente rbol20:
3, "A", "B", "C"
A->B

2, "A", "C", "B"

2, "C", "B", "A"

A->C

C->B

1, "A", "B", "C"

1, "B", "C", "A"

1, "C", "A", "B"

1, "A", "B", "C"

A->B

B->C

C->A

A->B

20 En este caso no mostraremos los resultados ya que las


llamadas a la funcin no devuelven nada, simplemente se
escribe en la salida. Para que ocupe menos espacio,
solamente mostramos el valor de los tres parmetros.
J.M. Gimeno y J.L. Gonzlez

45

Programacin 2

Curso 2011/2012

Dnde adems de indicar dentro de los cuadrados los


parmetros que se pasan en la llamada, he indicado de la
forma Origen->Destino, fuera de las llamadas, lo que se
escribe en la pantalla.

Simplificando la solucin

De cara a pensar la solucin ha sido conveniente considerar


que el caso base es cuando numDisks es 1. Pero si miramos
la solucin obtenida vemos que podramos simplificar las
cosas considerando como caso ms simple cuando
numDisks es 0. En tal caso, no hay que hacer nada para
mover los 0 discos!
El cdigo de la funcin quedara ahora como:
1 public void solve2(int numDisks,
14
String from,
15
String to;
16
String using) {
17
18 if ( numDisks >= 1 ) {
19
solve2(n-1, from, using, to);
20
println(Move disk from + from + to + to);
21
solve2(n-1, using, to, from);
22 }
23 }

Recursividad mltiple

Fijaos que en este caso la descomposicin del caso no


simple ha dado lugar a dos llamadas recursivas. No hay
ningn problema. Mientras las llamadas se hagan sobre
datos ms pequeos, no hay limitacin alguna en su
cantidad.
Una cuestin importante en este caso es que, a diferencia
de los anteriores ejemplos, no existe una aproximacin
iterativa evidente al problema (y, una vez vista, la solucin
recursiva es muy clara).
Otro aspecto interesante de este problema es que muestra
la potencia de la recursividad: el hecho de que podamos
usar llamadas recursivas, hace que en la solucin podamos
utilizar operaciones mucho ms potentes que las
disponibles inicialmente. En este caso, no tan solo
disponemos de la posibilidad de mover un disco (operacin
J.M. Gimeno y J.L. Gonzlez

46

Programacin 2

Curso 2011/2012

bsica), sino que podemos usar una operacin que permite


mover varios discos a la vez (propia funcin que estamos
diseando).

Como curiosidad final

Un aspecto curioso, que de alguna manera conecta de


forma palindrmica con el inicio del tema es que si
dibujamos las configuraciones posibles de los discos como
vrtices de un grafo y los unimos cuando es posible pasar
de una a otra a travs de un movimiento vlido, obtenemos
el tringulo de Sierpinski con el que comenzbamos el
tema.
Para un disco (caso simple):

Para dos discos (caso recursivo):

J.M. Gimeno y J.L. Gonzlez

47

Programacin 2

Curso 2011/2012

10.
Nmero de particiones de un
nmero natural
Un conjunto de nmeros naturales >0 es una particin de
un nmero dado n, si la suma de dichos nmeros es n. Es
decir:

{a1 , a2 , , ak } Particiones ( n ) i ai >0 ai=n


i

Lo que se desea es disear e implementar una funcin tal


que, dado un nmero n, calcule el nmero de particiones
distintas de dicho nmero.
Es decir,
1 public int numPartitions(int nat) {
2 ?
3}

El problema ser encontrar una solucin recursiva del


mismo.

Intentado buscar una recursin


Una estrategia que a veces funciona es la de intentar
calcular manualmente los valores que ha de buscar la
funcin y ver si podemos basar nuestro diseo en ese
mtodo de clculo.
Otra estrategia que podemos aplicar cuando nos pidan
calcular el nmero de veces que algo se puede hacer, es
generar ese algo a contar de forma organizada.
Intentemos buscar las particiones de varios nmeros, de la
manera ms organizada posible, ya que querremos poder
luego programarla (y teniendo en cuenta que querremos
usar una estrategia recursiva, es decir, una en la que el
nmero der particiones de un nmero se calcule en base al
nmero de particiones de otros nmeros).
Empecemos21:
#(1) = #{{1}}=1
#(2) = #{{2},{1+1}} = 2

21 Para simplificar la notacin usaremos #(n) como el


nmero de particiones del nmero n y #{c} como la
cardinalidad del conjunto c.
J.M. Gimeno y J.L. Gonzlez

48

Programacin 2

Curso 2011/2012

Podemos intentar encontrar aqu una posible regla? La


respuesta es que s, las formas de sumar 2 se pueden
dividir en dos categoras:
Una en la que solamente usamos un nmero, es decir,
{2}
Otra en la que usamos varios nmeros, en este caso,
{1,1}
Fijaos en que en ste ltimo caso, los nmeros que
aparecen, son menores que 2. Podemos intentar encontrar
una regla para generarlos?
Primera aproximacin
Una posible idea consiste en pensar en que si tenemos que
n = p+q, cualquier combinacin que sume p, sumada a una
combinacin que sume q, es una combinacin que suma n.
Por ejemplo, como 10 = 4 + 6, una combinacin que sume
4, por ejemplo {2,2} junto con una combinacin que sume
6, por ejemplo {4,1,1}, forman una combinacin
{2,2,4,1,1} que suma 10.
As que, podemos conjeturar que la recursin ser:
n1

(n)=1+ (k )(nk )n>1


k=1

Probamos?
#(2) = 1 + #(1)*#(1) = 1+1*1 = 2
#(3) = 1 + #(1)*#(2) + #(2)*#(1) = 1 + 1*2 + 2*1 =
5
Pero 3 se descompone como {3}, {2,1}, {1,1,1}, es decir,
de 3 formas diferentes.
Segunda aproximacin
Para evitar repeticiones, si ya hemos considerado la
descomposicin de 3 como 2+1 ya no consideraremos la
1+2, es decir:
n2

(n)=1+ (k )(nk )n>1


k=1

Probamos?
#(2) = 1 + #(1)*#(1) = 1 + 1*1 = 2
#(3) = 1 + #(1)*#(2) = 1 + 1*2 = 3
#(4) = 1 + #(1)*#(3) + #(2)*#(2) = 1*3 + 2*2 = 7
Pero 4 se descompone en, {4}, {3,1},{2,2}, {2,1,1},
{1,1,1,1}. El problema es que la {1,1,1,1} y {2,2} se
cuentan dos veces.
J.M. Gimeno y J.L. Gonzlez

49

Programacin 2

Curso 2011/2012

En resumen, nuestro problema consisten en encontrar


formas de contar que eviten considerar varias veces la
misma descomposicin (en otras palabras,
descomposiciones del problema que, por construccin, sean
independientes entre s). Cuando esto sucede, una
estrategia comn es aadir algn parmetro que permita
distinguir independizar unos subproblemas de otros.
Posibilidad 1: distinguiendo particiones por tamao
Una forma de sistematizar el conteo de particiones es por
su tamao (ya que una misma particin no puede tener dos
tamaos diferentes). Si llamamos #(n,k) al nmero de
particiones de n de tamao k, tenemos que:
n

( n )= (n , k )
k=1

Por lo que ahora el problema consistir en buscar una


recurrencia que nos permita calcular #(n,k) que es el
nmero de particiones del nmero n usando k nmeros
positivos.
Como siempre debemos buscar casos simples y casos
recursivos.
#(n,k)=0 cuando k>n
#(n,k)=1 cuando k=n
Por lo que nos quedan los casos en los que k<n.
Propiedad 1: Particiones de menor tamao
Cmo se obtienen particiones de tamao k a partir de
particiones de tamao k-1? Sumando un nmero.
Supongamos que tengo una particin de tamao k-1 del
nmero n, si le sumo el nmero dk obtengo una particin de
tamao k del nmero n+dk.
Es decir,
n= {d 1 , ,d k 1 } n+d k = { d 1 , , d k }

o tambin

n= {d 1 , ,d k } nd k = { d 1 , , d k1 }

Propiedad 2: Particiones con sumandos menores


Otra manera de manipular las particiones es cambiar
alguno de los sumandos que aparecen en la
descomposicin. Es decir:

J.M. Gimeno y J.L. Gonzlez

50

Programacin 2

Curso 2011/2012

d
( k + d)
d1 , ,

n=

o tambin
d
( k d)
d1, ,

n= {d 1 , ,d k } nd=

siempre y cuando d k >d .


Si todos los sumandos son mayores que d podemos aplicar
varias veces la propiedad anterior y obtenemos
d
d
( kd )
( 1d) , ,

n= { d 1 , ,d k } nkd=

Aplicacin al caso recursivo


Podemos aprovecharnos de esta propiedad para encontrar
una recurrencia? (En este punto conviene recordar que
queremos obtener subproblemas que no contengan
particiones en comn, ya que no queremos volver a caer en
el error de sumar una misma particin varias veces).
Cuntas descomposiciones contienen al menos un 1?
Por la propiedad 1 tantas como formas de obtener n-1
usando k-1 nmeros, es decir, #(n-1, k-1).
Y cuantas no lo contienen? Si no contienen un 1,
quiere decir que todos los sumandos son >1, por lo
que por la propiedad 1 podemos restar un 1 a cada
uno de ellos. Eso quiere decir que de stas hay tantas
como #(n-k, k).
Hay alguna otra posibilidad? No. O bien una particin
contiene al menos un 1, o no lo contiene. Ya hemos
acabado!!
Recapitulando
Si agrupamos todo lo que hemos desarrollado tenemos que:
J.M. Gimeno y J.L. Gonzlez

51

Programacin 2
( n , k )=

Curso 2011/2012

0 k >n
1 k =n
(n1, k 1)+ ( nk , k ) k < n

Y expresando todo en Java quedara:


1 public int numPartitions(int sum) {
2 // Entrada: sum > 0
3 int count = 0;
4 for(int numParts=1; i<=total; ++numParts) {
5
count += numPartitions(sum, numParts);
6 }
7 return count;
8}
9
10 public int numPartitions(int sum, int numParts) {
11 // Entrada: sum > 0, numParts > 0
12 if ( numParts > sum ) {
13
return 0;
14 } else if ( numParts == sum ) {
15
return 1;
16 } else {
17
return numPartitions(sum-1, numParts-1) +
18
numPartitions(sum-numParts, numParts);
19 }
20 }

Fijaos en que, en esta solucin, el tamao del problema


puede definirse como sum+numParts, por lo que en ambas
llamadas recursivas los tamaos de los subproblemas son
menores.
Posibilidad 2: limitando el mnimo sumando de una
particin
Otra forma de sistematizar el conteo, y evitar repeticiones,
es fijar el valor mnimo que puede tener un sumando en
una particin. Es decir,
$(n, k) = nmero de formas de sumar n con sumandos que
son >=k.
En este caso el problema original #(n) es exactamente $
(n,1), ya que por definicin, todos los sumandos sern >=1.
Queda como ejercicio buscar la recursin en este caso.

J.M. Gimeno y J.L. Gonzlez

52

Programacin 2

11.

Curso 2011/2012

Bibliografa

Para la parte de recursividad, Programacin


Metdica, de J.L.Balczar, Ed. McGraw-Hill (2001).
Para el funcionamiento de las llamadas a funcin,
captulo 5 del libro The Art and Science of Java
(Preliminary Draft) de Eric S. Roberts. Lo tenis
disponible en sakai.

J.M. Gimeno y J.L. Gonzlez

53