Backtracking en Java: algoritmos de vuelta atrás con ejemplos

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 20 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🔙 ¿Qué es backtracking?

Backtracking (vuelta atrás) es una técnica algorítmica que resuelve problemas explorando todas las posibilidades de forma sistemática, pero con una ventaja crucial sobre la fuerza bruta: cuando detecta que un camino no puede llevar a una solución válida, retrocede (backtracks) y prueba la siguiente alternativa sin explorar el resto de ese camino.

Imagina que estás en un laberinto. En cada bifurcación eliges un camino. Si llegas a un callejón sin salida, vuelves a la última bifurcación y tomas otro camino. No necesitas explorar todo el callejón sin salida: en cuanto ves la pared, retrocedes. Eso es exactamente lo que hace backtracking con los problemas computacionales.

📘 Definición formal: Backtracking es un método de búsqueda exhaustiva que construye soluciones candidatas incrementalmente, abandona cada candidato parcial (backtracks) tan pronto como determina que no puede completarse hasta una solución válida, y retoma la exploración desde el último punto de decisión con alternativas pendientes.

La técnica se formalizó en los años 1960, aunque sus ideas se remontan a los trabajos de Derrick Henry Lehmer sobre problemas combinatorios. Hoy es fundamental en la resolución de puzzles (Sudoku, N-Reinas), satisfacción de restricciones, compiladores, inteligencia artificial y optimización combinatoria.

🌳 Cómo funciona: el árbol de decisiones

Todo problema de backtracking puede visualizarse como un árbol de decisiones (también llamado árbol de búsqueda o espacio de estados). Cada nodo del árbol representa una decisión parcial, y cada rama representa una opción disponible en ese punto:

Árbol de decisiones — Ejemplo: comprar leche
                    [Inicio: necesito leche]
                     /                    \
            [Tienda esquina]         [Supermercado]
                  |                    /          \
           [¡Cerrado! ✗]      [Refrigerador]   [Pasillo alimentación]
            ← BACKTRACK            |                    |
                            [No hay leche ✗]    [¡Leche encontrada! ✓]
                             ← BACKTRACK             SOLUCIÓN

Recorrido: Tienda → ✗ → Backtrack → Supermercado → Refrigerador → ✗ →
           Backtrack → Pasillo → ✓ ¡Solución!

El algoritmo recorre este árbol en profundidad (DFS, Depth-First Search). Cada vez que encuentra un callejón sin salida, retrocede al nodo padre y explora la siguiente rama. Los cuatro componentes esenciales son:

Componente Descripción Ejemplo (N-Reinas)
Estado Representación de una solución parcial en un momento dado. Las posiciones de las reinas ya colocadas en el tablero.
Opciones (candidatos) Las decisiones posibles desde el estado actual. Las columnas disponibles para la siguiente reina.
Restricciones Condiciones que debe cumplir una solución válida. Ninguna reina amenaza a otra (fila, columna, diagonal).
Criterio de parada Condición que indica que se ha encontrado una solución completa. Se han colocado las N reinas sin conflictos.

📋 Plantilla genérica en Java

Todos los algoritmos de backtracking siguen la misma estructura. Esta plantilla te servirá como punto de partida para cualquier problema:

Plantilla genérica — Backtracking
/**
 * Plantilla genérica de backtracking.
 * @param solucion  la solución parcial que se está construyendo
 * @param paso      el nivel de decisión actual (profundidad en el árbol)
 */
public void backtrack(Solucion solucion, int paso) {

    // 1. ¿Es una solución completa?
    if (esCompleta(solucion)) {
        procesarSolucion(solucion);  // Imprimir, guardar, etc.
        return;
    }

    // 2. Generar candidatos para el paso actual
    List<Opcion> candidatos = generarCandidatos(solucion, paso);

    // 3. Probar cada candidato
    for (Opcion candidato : candidatos) {

        // 4. ¿El candidato es válido? (PODA)
        if (esValido(solucion, candidato)) {

            // 5. ELEGIR: añadir el candidato a la solución
            solucion.agregar(candidato);

            // 6. EXPLORAR: avanzar al siguiente paso
            backtrack(solucion, paso + 1);

            // 7. DESHACER (backtrack): quitar el candidato
            solucion.quitar(candidato);
        }
    }
}
✅ Patrón «elegir → explorar → deshacer»: Este es el corazón de todo backtracking. Cada decisión se toma, se explora su consecuencia recursivamente y luego se deshace para probar la siguiente opción. Es crucial que el paso «deshacer» restaure el estado exactamente como estaba antes.

🔢 Ejemplo 1: generar permutaciones

El primer ejemplo clásico de backtracking es generar todas las permutaciones de un conjunto. Para {1, 2, 3} queremos obtener: [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1].

Permutaciones.java
import java.util.ArrayList;
import java.util.List;

public class Permutaciones {

    private final List<List<Integer>> resultados = new ArrayList<>();

    public List<List<Integer>> permutar(int[] nums) {
        List<Integer> actual = new ArrayList<>();
        boolean[] usado = new boolean[nums.length];
        backtrack(nums, actual, usado);
        return resultados;
    }

    private void backtrack(int[] nums, List<Integer> actual, boolean[] usado) {
        // Solución completa: tenemos N elementos
        if (actual.size() == nums.length) {
            resultados.add(new ArrayList<>(actual)); // Copiar (¡importante!)
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (usado[i]) continue; // Poda: ya está en la permutación actual

            // ELEGIR
            actual.add(nums[i]);
            usado[i] = true;

            // EXPLORAR
            backtrack(nums, actual, usado);

            // DESHACER
            actual.remove(actual.size() - 1);
            usado[i] = false;
        }
    }

    public static void main(String[] args) {
        Permutaciones p = new Permutaciones();
        List<List<Integer>> resultado = p.permutar(new int[]{1, 2, 3});
        System.out.println("Permutaciones de {1, 2, 3}:");
        for (List<Integer> perm : resultado) {
            System.out.println("  " + perm);
        }
        System.out.println("Total: " + resultado.size()); // 6 = 3!
    }
}
Salida en consola
Permutaciones de {1, 2, 3}:
  [1, 2, 3]
  [1, 3, 2]
  [2, 1, 3]
  [2, 3, 1]
  [3, 1, 2]
  [3, 2, 1]
Total: 6

♛ Ejemplo 2: el problema de las N-Reinas

El problema de las N-Reinas es el ejemplo más famoso de backtracking: colocar N reinas en un tablero N×N de modo que ninguna amenace a otra (ni en la misma fila, columna o diagonal). Fue planteado por primera vez en 1848 por el ajedrecista Max Bezzel para N=8.

NReinas.java — Solución completa
public class NReinas {

    private final int n;
    private final int[] columnas; // columnas[fila] = columna de la reina en esa fila
    private int soluciones = 0;

    public NReinas(int n) {
        this.n = n;
        this.columnas = new int[n];
    }

    public void resolver() {
        backtrack(0);
        System.out.println("Total de soluciones para " + n + " reinas: " + soluciones);
    }

    private void backtrack(int fila) {
        // Solución completa: todas las filas tienen reina
        if (fila == n) {
            soluciones++;
            imprimirTablero();
            return;
        }

        // Probar cada columna en esta fila
        for (int col = 0; col < n; col++) {
            if (esSeguro(fila, col)) {
                // ELEGIR: colocar reina
                columnas[fila] = col;

                // EXPLORAR: pasar a la siguiente fila
                backtrack(fila + 1);

                // DESHACER: implícito (se sobrescribe en el siguiente ciclo)
            }
        }
    }

    /**
     * Verifica que la posición (fila, col) no entre en conflicto
     * con ninguna reina ya colocada en filas anteriores.
     */
    private boolean esSeguro(int fila, int col) {
        for (int i = 0; i < fila; i++) {
            // Misma columna
            if (columnas[i] == col) return false;
            // Misma diagonal (la diferencia de filas == diferencia de columnas)
            if (Math.abs(columnas[i] - col) == Math.abs(i - fila)) return false;
        }
        return true;
    }

    private void imprimirTablero() {
        System.out.println("--- Solución " + soluciones + " ---");
        for (int fila = 0; fila < n; fila++) {
            StringBuilder sb = new StringBuilder();
            for (int col = 0; col < n; col++) {
                sb.append(columnas[fila] == col ? " ♛ " : " · ");
            }
            System.out.println(sb);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        new NReinas(8).resolver();
    }
}
Salida en consola (primera solución de 92)
--- Solución 1 ---
 ♛  ·  ·  ·  ·  ·  ·  · 
 ·  ·  ·  ·  ♛  ·  ·  · 
 ·  ·  ·  ·  ·  ·  ·  ♛ 
 ·  ·  ·  ·  ·  ♛  ·  · 
 ·  ·  ♛  ·  ·  ·  ·  · 
 ·  ·  ·  ·  ·  ·  ♛  · 
 ·  ♛  ·  ·  ·  ·  ·  · 
 ·  ·  ·  ♛  ·  ·  ·  · 

...
Total de soluciones para 8 reinas: 92
📘 Dato: Para N=8 existen 92 soluciones (12 únicas, el resto son rotaciones y reflexiones). Sin poda, habría que probar 8⁸ = 16.777.216 configuraciones. Con backtracking, solo se exploran ~15.000 nodos.

🧩 Ejemplo 3: resolver un Sudoku

El Sudoku es uno de los problemas más populares resueltos con backtracking. Dado un tablero 9×9 parcialmente relleno, hay que completarlo con dígitos del 1 al 9 respetando tres restricciones: cada fila, cada columna y cada subcuadro 3×3 debe contener todos los dígitos del 1 al 9 sin repetir.

SudokuSolver.java
public class SudokuSolver {

    private static final int VACIO = 0;

    public boolean resolver(int[][] tablero) {
        // Buscar la siguiente celda vacía
        for (int fila = 0; fila < 9; fila++) {
            for (int col = 0; col < 9; col++) {
                if (tablero[fila][col] == VACIO) {

                    // Probar dígitos del 1 al 9
                    for (int digito = 1; digito <= 9; digito++) {
                        if (esValido(tablero, fila, col, digito)) {

                            // ELEGIR
                            tablero[fila][col] = digito;

                            // EXPLORAR
                            if (resolver(tablero)) {
                                return true; // ¡Solución encontrada!
                            }

                            // DESHACER
                            tablero[fila][col] = VACIO;
                        }
                    }
                    return false; // Ningún dígito funciona → backtrack
                }
            }
        }
        return true; // No hay celdas vacías → resuelto
    }

    private boolean esValido(int[][] tablero, int fila, int col, int digito) {
        // Verificar fila
        for (int c = 0; c < 9; c++) {
            if (tablero[fila][c] == digito) return false;
        }
        // Verificar columna
        for (int f = 0; f < 9; f++) {
            if (tablero[f][col] == digito) return false;
        }
        // Verificar subcuadro 3x3
        int inicioFila = (fila / 3) * 3;
        int inicioCol  = (col / 3) * 3;
        for (int f = inicioFila; f < inicioFila + 3; f++) {
            for (int c = inicioCol; c < inicioCol + 3; c++) {
                if (tablero[f][c] == digito) return false;
            }
        }
        return true;
    }

    public void imprimir(int[][] tablero) {
        for (int f = 0; f < 9; f++) {
            if (f % 3 == 0 && f != 0) System.out.println("------+-------+------");
            for (int c = 0; c < 9; c++) {
                if (c % 3 == 0 && c != 0) System.out.print("| ");
                System.out.print(tablero[f][c] + " ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int[][] sudoku = {
            {5,3,0, 0,7,0, 0,0,0},
            {6,0,0, 1,9,5, 0,0,0},
            {0,9,8, 0,0,0, 0,6,0},

            {8,0,0, 0,6,0, 0,0,3},
            {4,0,0, 8,0,3, 0,0,1},
            {7,0,0, 0,2,0, 0,0,6},

            {0,6,0, 0,0,0, 2,8,0},
            {0,0,0, 4,1,9, 0,0,5},
            {0,0,0, 0,8,0, 0,7,9}
        };

        SudokuSolver solver = new SudokuSolver();
        System.out.println("Sudoku original:");
        solver.imprimir(sudoku);

        if (solver.resolver(sudoku)) {
            System.out.println("\nSudoku resuelto:");
            solver.imprimir(sudoku);
        } else {
            System.out.println("No tiene solución.");
        }
    }
}

🏰 Ejemplo 4: encontrar la salida de un laberinto

Un laberinto representado como una matriz donde 0 es camino libre y 1 es pared. El objetivo es encontrar un camino desde la esquina superior izquierda (0,0) hasta la esquina inferior derecha (n-1, n-1).

Laberinto.java
public class Laberinto {

    // Movimientos: arriba, abajo, izquierda, derecha
    private static final int[][] MOVIMIENTOS = {{-1,0},{1,0},{0,-1},{0,1}};
    private static final String[] DIRS = {"↑", "↓", "←", "→"};

    private final int[][] mapa;
    private final int n;
    private final boolean[][] visitado;

    public Laberinto(int[][] mapa) {
        this.mapa = mapa;
        this.n = mapa.length;
        this.visitado = new boolean[n][n];
    }

    public boolean resolver() {
        return backtrack(0, 0);
    }

    private boolean backtrack(int fila, int col) {
        // ¿Hemos llegado a la meta?
        if (fila == n - 1 && col == n - 1) {
            visitado[fila][col] = true;
            return true;
        }

        // ¿Es válida esta posición?
        if (fila < 0 || fila >= n || col < 0 || col >= n) return false;
        if (mapa[fila][col] == 1) return false;  // Pared
        if (visitado[fila][col]) return false;    // Ya visitado

        // ELEGIR: marcar como visitado
        visitado[fila][col] = true;

        // EXPLORAR: probar los 4 movimientos
        for (int[] mov : MOVIMIENTOS) {
            if (backtrack(fila + mov[0], col + mov[1])) {
                return true; // Camino encontrado
            }
        }

        // DESHACER: desmarcar (no forma parte del camino)
        visitado[fila][col] = false;
        return false;
    }

    public void imprimirCamino() {
        for (int f = 0; f < n; f++) {
            for (int c = 0; c < n; c++) {
                if (visitado[f][c])      System.out.print(" ★ ");
                else if (mapa[f][c] == 1) System.out.print(" ▓ ");
                else                      System.out.print(" · ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        int[][] mapa = {
            {0, 1, 0, 0, 0},
            {0, 0, 0, 1, 0},
            {1, 1, 0, 1, 0},
            {0, 0, 0, 0, 1},
            {0, 1, 1, 0, 0}
        };

        Laberinto lab = new Laberinto(mapa);
        if (lab.resolver()) {
            System.out.println("¡Camino encontrado!");
            lab.imprimirCamino();
        } else {
            System.out.println("No hay camino posible.");
        }
    }
}
Salida en consola
¡Camino encontrado!
 ★  ▓  ·  ·  · 
 ★  ★  ★  ▓  · 
 ▓  ▓  ★  ▓  · 
 ·  ·  ★  ★  ▓ 
 ·  ▓  ▓  ★  ★ 

✂️ Técnicas de poda: hacer backtracking eficiente

Sin poda, backtracking degenera en fuerza bruta. Las técnicas de poda (pruning) son lo que lo hacen práctico. La idea es descartar ramas lo antes posible:

Técnica Descripción Ejemplo
Poda de factibilidad Descartar opciones que ya violan una restricción, sin necesidad de seguir explorando. En N-Reinas, no probar columnas donde ya hay conflicto de diagonal.
Poda de optimalidad (Branch & Bound) Descartar ramas que no pueden mejorar la mejor solución encontrada hasta ahora. En el problema de la mochila, descartar ramas cuyo beneficio máximo potencial es menor que el beneficio de la mejor solución actual.
Propagación de restricciones Deducir valores antes de elegir, reduciendo candidatos. Usado en solucionadores CSP. En Sudoku, si una celda solo puede contener un valor, asignarlo directamente sin probar.
Ordenación de candidatos Probar primero los candidatos más prometedores para encontrar soluciones antes. En el problema de coloración de grafos, empezar por el nodo con más conexiones.
⚠️ Regla práctica: Una buena poda puede reducir el espacio explorado en varios órdenes de magnitud. En N-Reinas con N=8, sin poda se explorarían ~16.8 millones de nodos; con poda se exploran ~15.000 — una reducción del 99,9%.

⚖️ Backtracking vs. otros paradigmas algorítmicos

Paradigma Enfoque Cuándo usarlo en vez de backtracking
Fuerza bruta Genera TODAS las soluciones y las comprueba. Solo cuando el espacio de búsqueda es muy pequeño y la implementación más simple justifica la ineficiencia.
Divide y vencerás Divide el problema en subproblemas independientes. Cuando los subproblemas no comparten restricciones y pueden resolverse por separado (MergeSort, QuickSort).
Programación dinámica Subproblemas solapados con subestructura óptima. Cuando los mismos subproblemas se recalculan muchas veces (Fibonacci, mochila, caminos).
Algoritmos voraces Decisiones locales óptimas sin retroceder. Cuando se puede demostrar que la decisión localmente óptima lleva al óptimo global (Dijkstra, Kruskal).

🎯 Cuándo usar y cuándo evitar backtracking

✅ Usa backtracking cuando:

Necesitas encontrar todas las soluciones (o una que cumpla restricciones específicas) y no existe un algoritmo polinomial conocido para el problema.

El espacio de búsqueda tiene estructura de árbol y las restricciones permiten podar ramas tempranamente, reduciendo el espacio explorado en la práctica.

El problema es de satisfacción de restricciones (CSP): Sudoku, coloración de grafos, planificación de horarios, configuración de componentes.

❌ Evita backtracking cuando:

Existe un algoritmo polinomial. Si puedes resolver el problema con divide y vencerás, programación dinámica o un algoritmo greedy en tiempo polinomial, backtracking es innecesariamente lento.

El espacio de búsqueda es enorme sin posibilidad de poda. Si no hay restricciones que permitan descartar ramas tempranamente, backtracking degenera en fuerza bruta exponencial.

Los subproblemas se solapan masivamente. Si descubres que estás resolviendo los mismos subproblemas una y otra vez, necesitas memoización → programación dinámica.

🐛 Errores comunes al implementar backtracking

Error Consecuencia Solución
Olvidar el paso «deshacer» Las decisiones se acumulan → soluciones incorrectas o bucles infinitos. Siempre restaurar el estado tras la llamada recursiva. Si agregas, quita. Si marcas, desmarca.
No copiar la solución al guardarla Todas las soluciones guardadas apuntan al mismo objeto → al final todas son iguales (la última). Hacer new ArrayList<>(actual) al almacenar una solución en una lista de resultados.
Poda insuficiente Rendimiento inaceptable en tamaños moderados. Verificar restricciones lo antes posible, antes de la llamada recursiva, no después.
Modificar la lista mientras se itera ConcurrentModificationException en Java. Usar índices numéricos o un array boolean[] usado en lugar de modificar la colección de candidatos.
Confundir «encontrar una solución» con «encontrar todas» El algoritmo sigue buscando después de encontrar la primera, o para demasiado pronto. Para una sola solución: return true en cuanto la encuentres. Para todas: no hacer return, seguir con el bucle.

📝 Ejercicios resueltos

🏋️ Ejercicio 1: Generar todos los subconjuntos

Enunciado: Dado un conjunto de enteros sin duplicados, genera todos los subconjuntos posibles (el conjunto potencia). Por ejemplo, para {1, 2, 3} el resultado debe incluir: [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3].

Solución: Subconjuntos
import java.util.ArrayList;
import java.util.List;

public class Subconjuntos {

    public static List<List<Integer>> generar(int[] nums) {
        List<List<Integer>> resultado = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), resultado);
        return resultado;
    }

    private static void backtrack(int[] nums, int inicio,
                                  List<Integer> actual,
                                  List<List<Integer>> resultado) {
        // Cada estado parcial es un subconjunto válido
        resultado.add(new ArrayList<>(actual));

        for (int i = inicio; i < nums.length; i++) {
            actual.add(nums[i]);             // Elegir
            backtrack(nums, i + 1, actual, resultado); // Explorar
            actual.remove(actual.size() - 1); // Deshacer
        }
    }

    public static void main(String[] args) {
        List<List<Integer>> subs = generar(new int[]{1, 2, 3});
        System.out.println("Subconjuntos: " + subs);
        System.out.println("Total: " + subs.size()); // 8 = 2³
    }
}
// Salida: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

🏋️ Ejercicio 2: Suma de combinaciones (Combination Sum)

Enunciado: Dado un array de candidatos sin duplicados y un valor objetivo (target), encuentra todas las combinaciones únicas de candidatos donde los números sumen el target. Un mismo candidato puede usarse ilimitadas veces. Por ejemplo, para candidatos=[2,3,6,7] y target=7: resultado = [[2,2,3], [7]].

Solución: Combination Sum
import java.util.*;

public class CombinationSum {

    public static List<List<Integer>> combinationSum(int[] candidatos, int target) {
        List<List<Integer>> resultado = new ArrayList<>();
        Arrays.sort(candidatos); // Ordenar para poda
        backtrack(candidatos, target, 0, new ArrayList<>(), resultado);
        return resultado;
    }

    private static void backtrack(int[] candidatos, int restante, int inicio,
                                  List<Integer> actual,
                                  List<List<Integer>> resultado) {
        if (restante == 0) {
            resultado.add(new ArrayList<>(actual)); // Solución encontrada
            return;
        }

        for (int i = inicio; i < candidatos.length; i++) {
            // PODA: si el candidato es mayor que el restante, no seguir
            if (candidatos[i] > restante) break;

            actual.add(candidatos[i]);                              // Elegir
            backtrack(candidatos, restante - candidatos[i], i, actual, resultado); // Explorar (i, no i+1: se puede repetir)
            actual.remove(actual.size() - 1);                       // Deshacer
        }
    }

    public static void main(String[] args) {
        System.out.println(combinationSum(new int[]{2,3,6,7}, 7));
        // [[2, 2, 3], [7]]
    }
}

🏋️ Ejercicio 3: Verificar si un camino del caballo existe

Enunciado: En un tablero 5×5, determina si un caballo de ajedrez puede visitar todas las casillas exactamente una vez partiendo de la posición (0,0). Imprime el tablero con el orden de visitas. Este es el famoso Knight's Tour problem.

Solución: Knight's Tour 5×5
public class CaminoCaballo {

    private static final int N = 5;
    // Los 8 movimientos posibles del caballo
    private static final int[] DX = {2,1,-1,-2,-2,-1, 1, 2};
    private static final int[] DY = {1,2, 2, 1,-1,-2,-2,-1};

    private final int[][] tablero = new int[N][N];

    public boolean resolver() {
        // Inicializar tablero con -1 (no visitado)
        for (int[] fila : tablero)
            java.util.Arrays.fill(fila, -1);

        // El caballo empieza en (0, 0) como movimiento 0
        tablero[0][0] = 0;
        return backtrack(0, 0, 1);
    }

    private boolean backtrack(int x, int y, int movimiento) {
        // ¿Hemos visitado todas las casillas?
        if (movimiento == N * N) return true;

        // Probar los 8 movimientos del caballo
        for (int i = 0; i < 8; i++) {
            int nx = x + DX[i];
            int ny = y + DY[i];

            if (esValido(nx, ny)) {
                tablero[nx][ny] = movimiento;     // Elegir

                if (backtrack(nx, ny, movimiento + 1)) {
                    return true;                   // Explorar
                }

                tablero[nx][ny] = -1;             // Deshacer
            }
        }
        return false;
    }

    private boolean esValido(int x, int y) {
        return x >= 0 && x < N && y >= 0 && y < N && tablero[x][y] == -1;
    }

    public void imprimir() {
        for (int[] fila : tablero) {
            for (int casilla : fila) {
                System.out.printf("%3d", casilla);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        CaminoCaballo cab = new CaminoCaballo();
        if (cab.resolver()) {
            System.out.println("Camino del caballo (5×5):");
            cab.imprimir();
        } else {
            System.out.println("No existe solución.");
        }
    }
}

❓ Preguntas frecuentes sobre Backtracking en Java: algoritmos de vuelta atrás con ejemplos

Las dudas más comunes respondidas de forma clara y directa.

Fuerza bruta genera todas las combinaciones posibles y comprueba cada una. Backtracking es más inteligente: va construyendo soluciones incrementalmente y descarta ramas enteras del árbol de búsqueda en cuanto detecta que no pueden conducir a una solución válida (poda). Esto reduce drásticamente el espacio de búsqueda en la práctica, aunque en el peor caso ambos pueden tener la misma complejidad.
La recursión es la forma más natural e intuitiva de implementar backtracking, pero no es la única. Se puede implementar con una pila explícita (iterativamente), lo que evita posibles StackOverflowError en problemas con recursión muy profunda. Sin embargo, la versión recursiva es más legible y es la que se usa en la mayoría de contextos académicos y profesionales.
En el peor caso, backtracking tiene complejidad exponencial: O(b^d) donde b es el factor de ramificación (opciones en cada paso) y d la profundidad máxima. Por ejemplo, N-Reinas tiene O(N!) en el peor caso. Sin embargo, las técnicas de poda pueden reducir enormemente el espacio explorado en la práctica. La complejidad real depende mucho del problema y de la calidad de la poda.
La poda (pruning) consiste en detectar lo antes posible que una solución parcial no puede completarse de forma válida, para abandonar esa rama sin explorarla. Hay dos tipos principales: poda de factibilidad (descartar opciones que violan restricciones) y poda de optimalidad (descartar opciones que no pueden mejorar la mejor solución encontrada, llamada branch and bound). Una buena poda puede convertir un problema intratable en uno resoluble.
Backtracking se usa en resolución de puzzles (Sudoku, crucigramas), planificación de rutas, coloración de grafos, satisfacción de restricciones (CSP), compiladores (análisis sintáctico), inteligencia artificial (juegos como ajedrez), generación de combinaciones y permutaciones, y problemas de optimización combinatoria. También es la base de los solucionadores SAT que verifican circuitos electrónicos.
Backtracking explora el espacio de soluciones construyendo candidatos incrementalmente y retrocediendo cuando detecta callejones sin salida. Programación dinámica descompone el problema en subproblemas solapados y almacena sus resultados para reutilizarlos. Backtracking se usa cuando necesitas encontrar soluciones que cumplan restricciones; programación dinámica cuando necesitas optimizar un valor y los subproblemas se repiten.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Backtracking en Java: algoritmos de vuelta atrás con ejemplos? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!