Apuntes DAM

Torres de Hanói

Mover una torre de discos de un poste a otro sin poner nunca uno grande sobre uno pequeño. El ejemplo perfecto de recursividad: tres líneas lo resuelven en 2ⁿ − 1 movimientos.

nivel intermedioTambién: Hanoi, torres de Hanoi, tower of Hanoi

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Torres de Hanói

Elige cuántos discos: la función recursiva aparta los de encima, mueve el grande y vuelve a poner los de encima, con 2ⁿ − 1 movimientos.

De 1 a 6

Paso 1

Hay que llevar los 3 discos de A a C, de uno en uno y sin poner nunca un disco sobre otro más pequeño. B sirve de apoyo.

1static void hanoi(int n, char origen, char destino, char auxiliar) {
2    if (n == 0) return;
3    hanoi(n - 1, origen, auxiliar, destino);
4    System.out.println("disco " + n + ": " + origen + " → " + destino);
5    hanoi(n - 1, auxiliar, destino, origen);
6}

Variables

n
3
movimientos
0 de 7

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

Hay tres postes y n discos de tamaños distintos apilados en el primero, del más grande abajo al más pequeño arriba. Hay que llevarlos todos al tercero moviendo un disco cada vez y sin poner nunca un disco sobre otro más pequeño. Con 3 discos se puede pensar a mano; con 8 ya cuesta, y aun así la solución cabe en tres líneas.

La clave es mirar solo el disco más grande. Para moverlo de A a C, todos los demás tienen que estar fuera de en medio, en B. Así que: primero se llevan los n − 1 de encima de A a B (usando C de apoyo), después se mueve el grande de A a C, y por último se llevan los n − 1 de B a C (usando A de apoyo). Mover n − 1 discos es el mismo problema con uno menos: se resuelve con la misma función. El caso base, 0 discos, no hace nada.

Es el ejemplo perfecto del «salto de fe» de la recursividad: no hace falta imaginar los cientos de movimientos, basta con confiar en que la llamada con n − 1 discos hace bien su trabajo. Cada llamada solo decide su parte, y la pila de llamadas guarda en qué punto de cada nivel se iba.

El número de movimientos cumple T(n) = 2 · T(n − 1) + 1, que da 2ⁿ − 1, y se puede demostrar que no se puede hacer con menos. Crece muy deprisa: la leyenda de los 64 discos de oro necesitaría más de 18 trillones de movimientos. También existe una solución sin recursividad: el disco pequeño se mueve en los turnos impares, siempre en el mismo sentido, y en los pares se hace el único movimiento posible que no lo toca.

Cuándo usarlo

  • Para aprender y explicar recursividad: el caso base, el paso recursivo y la pila de llamadas se ven con claridad.
  • Como modelo de problemas que se resuelven reduciendo el tamaño en uno y combinando: muchos algoritmos de divide y vencerás siguen el mismo esquema.
  • Para estudiar el crecimiento exponencial con un ejemplo que se puede contar a mano.
  • En pruebas de rendimiento de la recursividad y de la pila de un lenguaje.

Cuándo no

  • Para simular con muchos discos: con 30 ya son más de mil millones de movimientos; si solo interesa un movimiento concreto, se calcula directamente.
  • Como patrón para problemas que no se reducen a copias más pequeñas de sí mismos: no todo problema recursivo se parece a Hanói.
  • Con más de tres postes: el problema cambia por completo (el de Reve, con cuatro postes, tiene otra solución).

Paso a paso

  1. Caso base. Si no hay discos que mover (n == 0), no se hace nada.
  2. Apartar. Llevar los n − 1 discos de encima del origen al poste auxiliar, con la misma función.
  3. Mover el grande. Mover el disco n, que ya está solo, del origen al destino.
  4. Volver a poner. Llevar los n − 1 discos del auxiliar al destino, encima del grande, con la misma función.

El código

Hanói recursivo con contador

Los 7 movimientos de 3 discos y la cuenta para 10 y 16 discos, que coincide con 2ⁿ − 1.

Java
1public class Main {
2    static int movimientos = 0;
3
4    static void hanoi(int n, char origen, char destino, char auxiliar, boolean escribir) {
5        if (n == 0) return;
6        hanoi(n - 1, origen, auxiliar, destino, escribir);    // aparta los n − 1 de encima
7        movimientos++;
8        if (escribir) System.out.println(movimientos + ". disco " + n + ": " + origen + " → " + destino);
9        hanoi(n - 1, auxiliar, destino, origen, escribir);    // y los vuelve a poner encima
10    }
11
12    public static void main(String[] args) {
13        hanoi(3, 'A', 'C', 'B', true);
14        for (int n : new int[]{10, 16}) {
15            movimientos = 0;
16            hanoi(n, 'A', 'C', 'B', false);
17            System.out.println(n + " discos: " + movimientos + " movimientos (2^" + n + " − 1 = " + ((1 << n) - 1) + ")");
18        }
19    }
20}

Salida al ejecutarlo (la misma en los 5 lenguajes)

1. disco 1: A → C
2. disco 2: A → B
3. disco 1: C → B
4. disco 3: A → C
5. disco 1: B → A
6. disco 2: B → C
7. disco 1: A → C
10 discos: 1023 movimientos (2^10 − 1 = 1023)
16 discos: 65535 movimientos (2^16 − 1 = 65535)

Hanói sin recursividad

La versión iterativa con tres pilas: el disco 1 da vueltas en los movimientos impares y en los pares solo hay una jugada posible. Hace exactamente los mismos movimientos que la recursiva.

Java
1import java.util.*;
2
3public class Main {
4    /** Sin recursividad: en los movimientos impares el disco 1 avanza en círculo (A→C→B si n es impar,
5        A→B→C si es par); en los pares se hace el único movimiento posible que no lo toca. */
6    static List<String> iterativo(int n) {
7        List<Deque<Integer>> postes = List.of(new ArrayDeque<>(), new ArrayDeque<>(), new ArrayDeque<>());
8        for (int d = n; d >= 1; d--) postes.get(0).push(d);
9        int paso = n % 2 == 0 ? 1 : 2, chico = 0;          // chico: el poste donde está el disco 1
10        List<String> movs = new ArrayList<>();
11        for (int k = 1; k < (1 << n); k++) {
12            int de, a;
13            if (k % 2 == 1) {
14                de = chico;
15                a = (chico + paso) % 3;
16                chico = a;
17            } else {
18                int x = (chico + 1) % 3, y = (chico + 2) % 3;
19                boolean deX = !postes.get(x).isEmpty() && (postes.get(y).isEmpty() || postes.get(x).peek() < postes.get(y).peek());
20                de = deX ? x : y;
21                a = deX ? y : x;
22            }
23            int disco = postes.get(de).pop();
24            postes.get(a).push(disco);
25            movs.add("disco " + disco + ": " + "ABC".charAt(de) + " → " + "ABC".charAt(a));
26        }
27        return movs;
28    }
29
30    static void recursivo(int n, int o, int d, int x, List<String> movs) {
31        if (n == 0) return;
32        recursivo(n - 1, o, x, d, movs);
33        movs.add("disco " + n + ": " + "ABC".charAt(o) + " → " + "ABC".charAt(d));
34        recursivo(n - 1, x, d, o, movs);
35    }
36
37    public static void main(String[] args) {
38        List<String> it = iterativo(3);
39        for (int i = 0; i < it.size(); i++) System.out.println((i + 1) + ". " + it.get(i));
40        boolean iguales = true;
41        for (int n = 1; n <= 12; n++) {
42            List<String> r = new ArrayList<>();
43            recursivo(n, 0, 2, 1, r);
44            if (!r.equals(iterativo(n))) iguales = false;
45        }
46        System.out.println("Mismos movimientos que la versión recursiva para n = 1..12: " + (iguales ? "sí" : "no"));
47    }
48}

Salida al ejecutarlo (la misma en los 5 lenguajes)

1. disco 1: A → C
2. disco 2: A → B
3. disco 1: C → B
4. disco 3: A → C
5. disco 1: B → A
6. disco 2: B → C
7. disco 1: A → C
Mismos movimientos que la versión recursiva para n = 1..12: sí

Traza: Los 7 movimientos de 3 discos (de A a C)

MovimientoDiscoDeAPostes después (de abajo arriba)
11ACA: 3 2 · B: — · C: 1
22ABA: 3 · B: 2 · C: 1
31CBA: 3 · B: 2 1 · C: —
43ACA: — · B: 2 1 · C: 3
51BAA: 1 · B: 2 · C: 3
62BCA: 1 · B: — · C: 3 2
71ACA: — · B: — · C: 3 2 1

El disco 3 se mueve justo en el centro (movimiento 4): antes, los otros dos se apartan a B; después, vuelven encima.

Complejidad

MedidaCoste
Movimientos2ⁿ − 1 (el mínimo posible)
TiempoO(2ⁿ)
Memoria de la versión recursivaO(n): la profundidad de la pila de llamadas
Calcular solo el movimiento kO(n)

Cada disco más duplica el trabajo: 10 discos son 1.023 movimientos y 20 discos, más de un millón.

0102030405015101520tamaño de la entrada (n)operacionesO(n!)O(2ⁿ)O(n²)O(n log n)O(1)O(log n)O(n)
  • Mejor caso: O(2ⁿ)
  • Caso medio: O(2ⁿ)
  • Peor caso: O(2ⁿ)

Siempre 2ⁿ − 1 movimientos, y es el mínimo posible: no hay forma más rápida de resolverlo. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Es el ejercicio de recursividad de casi todos los cursos y libros de programación desde hace décadas.
  • Las rotaciones de copias de seguridad «torre de Hanói» usan el mismo patrón para decidir qué cinta se reutiliza cada día.
  • Se usa en psicología y neurología como prueba de planificación (la torre de Londres es una variante).
  • El número de movimientos 2ⁿ − 1 aparece en otros problemas: el código Gray, los subconjuntos de un conjunto…

Errores típicos

  • Olvidar el caso base o ponerlo en n == 1 sin tratar n == 0: recursividad infinita con 0 discos.
  • Intercambiar mal los postes en las llamadas: en la primera, el auxiliar hace de destino; en la segunda, de origen.
  • Intentar «ver» todos los movimientos en vez de confiar en que la llamada con n − 1 está bien.
  • Usar int para contar movimientos con muchos discos: con 31 discos ya se pasa; hace falta long.
  • Simular millones de movimientos cuando solo se pide uno: se puede calcular directamente.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Simulador de Hanói

La entrada dice cuántos discos hay, en qué poste están y a cuál hay que llevarlos. El método mover ya mueve un disco, comprueba las reglas (lanza una excepción si se rompen) y escribe cómo quedan los postes. Completa hanoi para que resuelva el problema llamando a mover.

  • Entrada: una línea discos origen destino, como 3 A C (de 1 a 8 discos; postes A, B y C).
  • Salida: una línea por movimiento, 1. disco 1: A → C | A: 3 2 B: C: 1 (cada poste de abajo arriba), y al final Hecho en 7 movimientos.
  • Si se rompe una regla: Movimiento ilegal: …. Entrada incorrecta: Entrada no válida: «…» (se espera: discos origen destino, como «3 A C»).
JavaSimulador de HanóiMedio

Ejemplo

Entrada (lo que se escribe por teclado)
3 A C
Salida esperada
1. disco 1: A → C | A: 3 2 B: C: 1
2. disco 2: A → B | A: 3 B: 2 C: 1
3. disco 1: C → B | A: 3 B: 2 1 C:
4. disco 3: A → C | A: B: 2 1 C: 3
5. disco 1: B → A | A: 1 B: 2 C: 3
6. disco 2: B → C | A: 1 B: C: 3 2
7. disco 1: A → C | A: B: C: 3 2 1
Hecho en 7 movimientos
Test oculto #3
Test oculto #4
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    static final List<Deque<Integer>> postes = List.of(new ArrayDeque<>(), new ArrayDeque<>(), new ArrayDeque<>());
5    static int movimientos = 0;
6
7    /** Mueve el disco de arriba de un poste a otro comprobando las reglas, y escribe cómo quedan los postes. */
8    static void mover(int de, int a) {
9        Deque<Integer> origen = postes.get(de), destino = postes.get(a);
10        if (origen.isEmpty()) throw new IllegalStateException("el poste " + "ABC".charAt(de) + " está vacío");
11        if (!destino.isEmpty() && destino.peek() < origen.peek())
12            throw new IllegalStateException("el disco " + origen.peek() + " no puede ir sobre el " + destino.peek());
13        destino.push(origen.pop());
14        movimientos++;
15        StringBuilder sb = new StringBuilder(movimientos + ". disco " + destino.peek() + ": " + "ABC".charAt(de) + " → " + "ABC".charAt(a) + " |");
16        for (int t = 0; t < 3; t++) {
17            List<Integer> discos = new ArrayList<>(postes.get(t));
18            Collections.reverse(discos);                       // de abajo arriba
19            sb.append(" ").append("ABC".charAt(t)).append(":");
20            for (int d : discos) sb.append(" ").append(d);
21        }
22        System.out.println(sb);
23    }
24
25    /** Lleva n discos del poste o al d usando x de apoyo (los postes son 0, 1 y 2), llamando a mover. */
26    static void hanoi(int n, int o, int d, int x) {
27        if (n == 0) return;
28        hanoi(n - 1, o, x, d);
29        mover(o, d);
30        hanoi(n - 1, x, d, o);
31    }
32
33    public static void main(String[] args) {
34        Scanner sc = new Scanner(System.in);
35        String linea = sc.hasNextLine() ? sc.nextLine().trim() : "";
36        String[] p = linea.split("\\s+");
37        if (p.length != 3 || !p[0].matches("[1-8]") || !p[1].matches("[ABC]") || !p[2].matches("[ABC]") || p[1].equals(p[2])) {
38            System.out.println("Entrada no válida: «" + linea + "» (se espera: discos origen destino, como «3 A C»)");
39            return;
40        }
41        int n = Integer.parseInt(p[0]), o = p[1].charAt(0) - 'A', d = p[2].charAt(0) - 'A';
42        for (int k = n; k >= 1; k--) postes.get(o).push(k);
43        try {
44            hanoi(n, o, d, 3 - o - d);
45            System.out.println("Hecho en " + movimientos + " movimientos");
46        } catch (IllegalStateException e) {
47            System.out.println("Movimiento ilegal: " + e.getMessage());
48        }
49    }
50}

La función no comprueba nada: confía en que, si cada llamada respeta el esquema, ningún movimiento romperá las reglas. mover lo verifica de todos modos, y nunca salta.

Con 8 discos son 255 movimientos, 2⁸ − 1: compruébalo en la última línea.

2. El movimiento número k

Con n discos que van de A a C, ¿qué disco se mueve en el movimiento k, y de dónde a dónde? Con 60 discos hay más de un trillón de movimientos: no se puede simular. Pero la estructura recursiva lo permite calcular directamente. El main lee las consultas y valida los rangos: completa movimiento.

  • Entrada: una consulta por línea, n k (n de 1 a 62 discos, k desde 1).
  • Salida: Movimiento 4 de 7: disco 3 de A a C.
  • Errores: Línea no válida: «…», Número de discos fuera de rango (de 1 a 62): 70, Con 3 discos solo hay movimientos del 1 al 7.
JavaEl movimiento número kDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
3 1
3 4
3 7
Salida esperada
Movimiento 1 de 7: disco 1 de A a C
Movimiento 4 de 7: disco 3 de A a C
Movimiento 7 de 7: disco 1 de A a C
Test oculto #3
Test oculto #4
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2
3public class Main {
4    /** Qué pasa en el movimiento k (desde 1) al llevar n discos de o a d con x de apoyo: {disco, de, a}.
5        Sin simular: los 2^(n−1) − 1 primeros apartan n − 1 discos, el siguiente mueve el disco n y el
6        resto vuelve a poner los n − 1 encima. */
7    static int[] movimiento(int n, long k, int o, int d, int x) {
8        long mitad = 1L << (n - 1);
9        if (k == mitad) return new int[]{n, o, d};
10        if (k < mitad) return movimiento(n - 1, k, o, x, d);
11        return movimiento(n - 1, k - mitad, x, d, o);
12    }
13
14    public static void main(String[] args) {
15        Scanner sc = new Scanner(System.in);
16        while (sc.hasNextLine()) {
17            String linea = sc.nextLine().trim();
18            if (linea.isEmpty()) continue;
19            String[] p = linea.split("\\s+");
20            if (p.length != 2 || !p[0].matches("\\d{1,2}") || !p[1].matches("\\d{1,19}")) {
21                System.out.println("Línea no válida: «" + linea + "»");
22                continue;
23            }
24            int n = Integer.parseInt(p[0]);
25            if (n < 1 || n > 62) {
26                System.out.println("Número de discos fuera de rango (de 1 a 62): " + n);
27                continue;
28            }
29            long total = (1L << n) - 1, k;
30            try {
31                k = Long.parseLong(p[1]);
32            } catch (NumberFormatException e) {
33                k = -1;
34            }
35            if (k < 1 || k > total) {
36                System.out.println("Con " + n + " discos solo hay movimientos del 1 al " + total);
37                continue;
38            }
39            int[] m = movimiento(n, k, 0, 2, 1);
40            System.out.println("Movimiento " + k + " de " + total + ": disco " + m[0] + " de " + "ABC".charAt(m[1]) + " a " + "ABC".charAt(m[2]));
41        }
42    }
43}

Cada llamada descarta la mitad de los movimientos, como una búsqueda binaria: como mucho n llamadas, en vez de 2ⁿ − 1 movimientos simulados.

Curiosidad: el disco que se mueve en el paso k es uno más que el número de ceros al final de k en binario (Long.numberOfTrailingZeros(k) + 1).

Test

Test: Torres de Hanói

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿Cuántos movimientos hacen falta, como mínimo, para 5 discos?

  2. 2.En hanoi(n, A, C, B), ¿qué hace la primera llamada recursiva?

  3. 3.¿Qué memoria usa la versión recursiva con n discos?

  4. 4.¿Qué disco se mueve en el movimiento central (el 2^(n−1))?

  5. 5.¿Qué recurrencia cumple el número de movimientos?

Relacionado