Apuntes DAM

Permutaciones (backtracking)

Generar todas las ordenaciones de unos elementos eligiendo uno de los libres en cada posición y deshaciendo la elección al volver. Hay n! y el backtracking las recorre sin repetir ninguna.

nivel intermedioTambién: generar permutaciones, anagramas, todas las ordenaciones, next permutation

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.

Permutaciones

Escribe unas letras distintas: se eligen de una en una entre las libres, y al completar una ordenación se deshace la última elección para probar otra.

De 2 a 4 letras distintas
  • libre

Paso 1

Para cada posición se prueba, por orden, cada letra que siga libre. Al llenar las 3 posiciones se apunta la permutación y se deshace la última elección para probar la siguiente: es backtracking.

1static void permutar(char[] letras, boolean[] usada, StringBuilder actual, List<String> res) {
2    if (actual.length() == letras.length) {
3        res.add(actual.toString());
4        return;
5    }
6    for (int i = 0; i < letras.length; i++) {
7        if (usada[i]) continue;
8        usada[i] = true;
9        actual.append(letras[i]);
10        permutar(letras, usada, actual, res);
11        actual.deleteCharAt(actual.length() - 1);
12        usada[i] = false;
13    }
14}

Variables

actual
—
encontradas
0
últimas
—

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

La idea

Una permutación es una forma de ordenar unos elementos. Con A, B y C hay 6: ABC, ACB, BAC, BCA, CAB y CBA. En general hay n! (n factorial): n opciones para la primera posición, n − 1 para la segunda, y así hasta 1. Crece muy deprisa: 10 elementos ya dan 3.628.800.

Para generarlas todas se usa el esquema del backtracking: elegir, explorar y deshacer. Se lleva la permutación que se está construyendo y un array de «usadas». En cada nivel se prueba, por orden, cada elemento libre: se marca, se añade y se llama a la función para la posición siguiente; al volver, se quita y se desmarca, para que el siguiente candidato encuentre todo como estaba. Cuando la permutación está completa, se apunta.

Si hay elementos repetidos (las letras de «AAB»), este esquema produciría permutaciones repetidas. La solución clásica es ordenar los elementos y, en cada posición, no elegir un elemento igual al anterior si el anterior está libre: esa letra ya se probó en esa posición. Así salen 3 permutaciones de «AAB» en vez de 6.

Hay otra forma, sin recursividad: el algoritmo de la siguiente permutación. A partir de una ordenación, calcula la que va justo después en orden alfabético con tres pasos sencillos, en O(n). Empezando por la ordenación de menor a mayor y repitiendo hasta que no haya siguiente, salen todas en orden. Es lo que hace std::next_permutation en C++.

Cuándo usarlo

  • Generar todas las ordenaciones: anagramas, rutas que visitan unas ciudades en distinto orden, turnos.
  • Búsquedas exhaustivas pequeñas: probar todas las asignaciones posibles (criptoaritmética, puzles) cuando n es pequeño.
  • Como esqueleto de backtracking con poda: se descartan las ramas que ya no pueden dar una solución válida.
  • Para pruebas: comprobar que un algoritmo funciona con cualquier orden de la entrada.

Cuándo no

  • Con muchos elementos: 15 elementos son más de un billón de permutaciones. Hay que podar mucho o cambiar de idea (programación dinámica, voraces, heurísticas).
  • Si solo hacen falta algunas al azar: se mezcla el array con Fisher-Yates en O(n).
  • Si el orden no importa (elegir 3 de 10): eso son combinaciones, no permutaciones, y hay muchas menos.

Paso a paso

  1. Caso base. Si la permutación en construcción tiene todos los elementos, se apunta y se vuelve.
  2. Elegir. Para cada elemento todavía libre: se marca como usado y se añade al final.
  3. Explorar. Se llama a la función para rellenar la siguiente posición.
  4. Deshacer. Al volver, se quita el elemento y se desmarca, para probar el siguiente candidato en esta posición.

El código

Permutaciones sin repetidas

El backtracking con la regla para letras repetidas: «AAB» da 3 y no 6. Con «ROMA» salen 24 (y entre ellas, AMOR).

Java
1import java.util.*;
2
3public class Main {
4    /** Backtracking que no repite permutaciones aunque haya letras repetidas: las letras van ordenadas y una
5        letra igual a la anterior solo se elige si la anterior ya está usada en esta rama. */
6    static void permutar(char[] letras, boolean[] usada, StringBuilder actual, List<String> res) {
7        if (actual.length() == letras.length) {
8            res.add(actual.toString());
9            return;
10        }
11        for (int i = 0; i < letras.length; i++) {
12            if (usada[i]) continue;
13            if (i > 0 && letras[i] == letras[i - 1] && !usada[i - 1]) continue;   // esa letra ya se probó aquí
14            usada[i] = true;
15            actual.append(letras[i]);
16            permutar(letras, usada, actual, res);
17            actual.deleteCharAt(actual.length() - 1);
18            usada[i] = false;
19        }
20    }
21
22    static List<String> permutaciones(String s) {
23        char[] letras = s.toCharArray();
24        Arrays.sort(letras);
25        List<String> res = new ArrayList<>();
26        permutar(letras, new boolean[letras.length], new StringBuilder(), res);
27        return res;
28    }
29
30    public static void main(String[] args) {
31        for (String s : List.of("ABC", "AAB", "ROMA")) {
32            List<String> p = permutaciones(s);
33            String lista = p.size() <= 6 ? String.join(" ", p) : String.join(" ", p.subList(0, 6)) + " …";
34            System.out.println(s + ": " + p.size() + " → " + lista);
35        }
36    }
37}

Salida al ejecutarlo (la misma en los 5 lenguajes)

ABC: 6 → ABC ACB BAC BCA CAB CBA
AAB: 3 → AAB ABA BAA
ROMA: 24 → AMOR AMRO AOMR AORM ARMO AROM …

La siguiente permutación, sin recursividad

Tres pasos convierten una ordenación en la siguiente en orden alfabético. Repitiéndolos desde 1 2 3 salen las 6.

Java
1import java.util.*;
2
3public class Main {
4    /** Convierte a en la siguiente permutación en orden lexicográfico; false si ya era la última. */
5    static boolean siguiente(int[] a) {
6        int i = a.length - 2;
7        while (i >= 0 && a[i] >= a[i + 1]) i--;               // 1. el último que tiene a su derecha uno mayor
8        if (i < 0) return false;
9        int j = a.length - 1;
10        while (a[j] <= a[i]) j--;                             // 2. el menor de su derecha que le supera
11        int t = a[i];
12        a[i] = a[j];
13        a[j] = t;
14        for (int x = i + 1, y = a.length - 1; x < y; x++, y--) {   // 3. su derecha, de menor a mayor
15            t = a[x];
16            a[x] = a[y];
17            a[y] = t;
18        }
19        return true;
20    }
21
22    static String texto(int[] a, String sep) {
23        StringJoiner sj = new StringJoiner(sep);
24        for (int x : a) sj.add(String.valueOf(x));
25        return sj.toString();
26    }
27
28    public static void main(String[] args) {
29        int[] a = {1, 2, 3};
30        StringJoiner todas = new StringJoiner(" ");
31        do todas.add(texto(a, "")); while (siguiente(a));
32        System.out.println("Todas, en orden: " + todas);
33        int[] b = {1, 3, 5, 4, 2};
34        String antes = texto(b, " ");
35        siguiente(b);
36        System.out.println("Después de " + antes + " va " + texto(b, " "));
37        int[] c = {3, 2, 1};
38        System.out.println("¿Hay alguna después de 3 2 1? " + (siguiente(c) ? "sí" : "no: es la última"));
39    }
40}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Todas, en orden: 123 132 213 231 312 321
Después de 1 3 5 4 2 va 1 4 2 3 5
¿Hay alguna después de 3 2 1? no: es la última

Traza: El backtracking con A, B y C

ConstruidaLibresQué pasa
AB Celige la A
ABCelige la B
ABC—completa: se apunta
ACBelige la C
ACB—completa: se apunta
BA Celige la B
BACelige la A
BAC—completa: se apunta
BCAelige la C
BCA—completa: se apunta
CA Belige la C
CABelige la A
CAB—completa: se apunta
CBAelige la B
CBA—completa: se apunta

Tras cada permutación completa se deshacen elecciones hasta el último nivel que aún tiene letras sin probar.

Complejidad

OperaciónCoste
Generar todasO(n · n!): n! permutaciones de n elementos
Con repetidos (a letras iguales, b iguales…)n! / (a! · b! · …) permutaciones distintas
Siguiente permutaciónO(n)
Mezclar al azar (Fisher-Yates)O(n)

Ningún algoritmo que escriba todas las permutaciones puede bajar de n!: es lo que ocupa la salida.

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(n!)
  • Caso medio: O(n!)
  • Peor caso: O(n!)

Hay n! permutaciones y cada una cuesta n escribirla: O(n · n!). Ningún algoritmo que las liste todas puede bajar de ahí. Las curvas grises son las demás clases, para comparar.

En la práctica

  • Pruebas de software: ejecutar un caso con todos los órdenes posibles de unas operaciones para buscar fallos de concurrencia.
  • Problemas de rutas pequeñas (el viajante con pocas ciudades) se resuelven probando todas las ordenaciones.
  • Los juegos de palabras y los generadores de anagramas.
  • Criptografía clásica: las cifras de transposición reordenan las letras según una permutación.
  • En C++, std::next_permutation; en Python, itertools.permutations.

Errores típicos

  • Olvidar deshacer la elección al volver (quitar el elemento o desmarcarlo): las ramas siguientes ven un estado corrupto.
  • Guardar la permutación en curso por referencia en la lista de resultados: al final todas apuntan al mismo objeto vacío. Hay que guardar una copia (toString(), new ArrayList<>(actual)).
  • No tratar los repetidos y obtener permutaciones duplicadas.
  • Intentar generar todas con n grande: con 13 elementos son más de 6.000 millones.
  • Confundir permutaciones (importa el orden) con combinaciones (no importa).

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. Anagramas

Para cada palabra, escribe cuántos anagramas distintos tiene (ordenaciones de sus letras, incluida ella misma) y los primeros en orden alfabético. El main ya escribe el resultado: completa anagramas para que devuelva todos, sin repetidos y ordenados.

  • Entrada: una palabra por línea, de 1 a 7 letras de la a a la z (se pasan a minúsculas).
  • Salida: ola: 6 anagramas: alo aol lao loa oal ola. Si hay más de 12, los 12 primeros, … y el último.
  • Otra cosa: Palabra no válida: «…» (de 1 a 7 letras de la a a la z).
JavaAnagramasMedio

Ejemplo

Entrada (lo que se escribe por teclado)
ola
sol
Salida esperada
ola: 6 anagramas: alo aol lao loa oal ola
sol: 6 anagramas: los lso ols osl slo sol
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    /** Todas las ordenaciones distintas de las letras de la palabra, de menor a mayor. */
5    static List<String> anagramas(String palabra) {
6        char[] letras = palabra.toCharArray();
7        Arrays.sort(letras);
8        List<String> res = new ArrayList<>();
9        generar(letras, new boolean[letras.length], new StringBuilder(), res);
10        return res;
11    }
12
13    static void generar(char[] letras, boolean[] usada, StringBuilder actual, List<String> res) {
14        if (actual.length() == letras.length) {
15            res.add(actual.toString());
16            return;
17        }
18        for (int i = 0; i < letras.length; i++) {
19            if (usada[i] || (i > 0 && letras[i] == letras[i - 1] && !usada[i - 1])) continue;
20            usada[i] = true;
21            actual.append(letras[i]);
22            generar(letras, usada, actual, res);
23            actual.setLength(actual.length() - 1);
24            usada[i] = false;
25        }
26    }
27
28    public static void main(String[] args) {
29        Scanner sc = new Scanner(System.in);
30        while (sc.hasNextLine()) {
31            String palabra = sc.nextLine().trim().toLowerCase();
32            if (palabra.isEmpty()) continue;
33            if (!palabra.matches("[a-z]{1,7}")) {
34                System.out.println("Palabra no válida: «" + palabra + "» (de 1 a 7 letras de la a a la z)");
35                continue;
36            }
37            List<String> a = anagramas(palabra);
38            String lista = a.size() <= 12 ? String.join(" ", a) : String.join(" ", a.subList(0, 12)) + " … " + a.get(a.size() - 1);
39            System.out.println(palabra + ": " + a.size() + (a.size() == 1 ? " anagrama: " : " anagramas: ") + lista);
40        }
41    }
42}

«Mississippi» tendría 11! = 39.916.800 ordenaciones, pero solo 34.650 distintas: evitar los repetidos al generar ahorra muchísimo trabajo frente a generar todo y filtrar.

Como las letras empiezan ordenadas y se prueban en orden, las permutaciones salen ya ordenadas sin ordenar nada al final.

2. Sentar a los invitados

Unos invitados se sientan en una fila de butacas, pero algunas parejas no se pueden sentar juntas. Cuenta de cuántas formas se pueden colocar y escribe la primera en orden alfabético. Con 9 invitados hay 362.880 ordenaciones, así que hay que podar: en cuanto un invitado queda al lado de un enemigo, esa rama se abandona. Completa sentar.

  • Primera línea: los nombres de los invitados (de 2 a 9, distintos). Después, una pareja de enemigos por línea: ana luis.
  • Salida: Formas posibles: 12 y Primera en orden alfabético: ana, carlos, luis, marta, o Imposible: no hay forma de sentarlos.
  • Pareja con un nombre desconocido o repetido: Línea no válida: «…».
JavaSentar a los invitadosDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
ana luis marta carlos
ana luis
marta carlos
Salida esperada
Formas posibles: 8
Primera en orden alfabético: ana, carlos, luis, marta
Test oculto #3
Test oculto #4
Test oculto #5
Test oculto #6
0/6 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 String[] nombres;             // en orden alfabético
5    static boolean[][] enemigos;         // enemigos[i][j]: i y j no se pueden sentar juntos
6    static long formas = 0;
7    static String primera = null;        // la primera ordenación válida que se encuentra
8
9    /** Prueba a sentar en la siguiente butaca a cada invitado libre que no sea enemigo del de su izquierda. */
10    static void sentar(List<Integer> actual, boolean[] sentado) {
11        if (actual.size() == nombres.length) {
12            formas++;
13            if (primera == null) {
14                StringJoiner sj = new StringJoiner(", ");
15                for (int i : actual) sj.add(nombres[i]);
16                primera = sj.toString();
17            }
18            return;
19        }
20        for (int i = 0; i < nombres.length; i++) {
21            if (sentado[i]) continue;
22            if (!actual.isEmpty() && enemigos[actual.get(actual.size() - 1)][i]) continue;   // poda: no puede ir aquí
23            sentado[i] = true;
24            actual.add(i);
25            sentar(actual, sentado);
26            actual.remove(actual.size() - 1);
27            sentado[i] = false;
28        }
29    }
30
31    public static void main(String[] args) {
32        Scanner sc = new Scanner(System.in);
33        String cabecera = sc.hasNextLine() ? sc.nextLine().trim() : "";
34        TreeSet<String> invitados = new TreeSet<>(Arrays.asList(cabecera.split("\\s+")));
35        invitados.remove("");
36        if (invitados.size() < 2 || invitados.size() > 9) {
37            System.out.println("Hacen falta de 2 a 9 invitados distintos en la primera línea");
38            return;
39        }
40        nombres = invitados.toArray(new String[0]);
41        List<String> lista = Arrays.asList(nombres);
42        enemigos = new boolean[nombres.length][nombres.length];
43        while (sc.hasNextLine()) {
44            String linea = sc.nextLine().trim();
45            if (linea.isEmpty()) continue;
46            String[] p = linea.split("\\s+");
47            if (p.length != 2 || !lista.contains(p[0]) || !lista.contains(p[1]) || p[0].equals(p[1])) {
48                System.out.println("Línea no válida: «" + linea + "»");
49                continue;
50            }
51            int a = lista.indexOf(p[0]), b = lista.indexOf(p[1]);
52            enemigos[a][b] = enemigos[b][a] = true;
53        }
54        sentar(new ArrayList<>(), new boolean[nombres.length]);
55        if (formas == 0) System.out.println("Imposible: no hay forma de sentarlos");
56        else {
57            System.out.println("Formas posibles: " + formas);
58            System.out.println("Primera en orden alfabético: " + primera);
59        }
60    }
61}

La poda es lo que convierte la fuerza bruta en backtracking: una rama con dos enemigos juntos se corta en cuanto aparece, sin generar las miles de filas que empezarían así.

Comprobar la fila al final también daría el resultado correcto, pero generando siempre las n! ordenaciones.

Test

Test: Permutaciones (backtracking)

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ántas permutaciones tiene un conjunto de 5 elementos distintos?

  2. 2.En el backtracking de permutaciones, ¿qué se hace al volver de la llamada recursiva?

  3. 3.¿Cuántas permutaciones distintas tiene «AAB»?

  4. 4.¿Qué permutación va justo después de 1 3 2 en orden lexicográfico?

  5. 5.¿Por qué se guarda una copia de la permutación al apuntarla?

Relacionado