Backtracking es una técnica para resolver problemas de búsqueda construyendo soluciones “de a poco”.
La idea central es:
Por eso también se lo conoce como prueba y error con retroceso.
Se usa cuando:
Problemas clásicos:
Dado un arreglo de enteros positivos v y un objetivo objetivo, queremos saber si existe un subconjunto cuya suma sea exactamente objetivo.
Ejemplo:
v = [3, 2, 5]objetivo = 7Una solución existe: 2 + 5 = 7.
En cada posición i tenemos dos decisiones:
v[i];v[i].Llevamos una suma parcial suma.
suma == objetivo, encontramos solución.suma > objetivo, no tiene sentido seguir por ese camino (poda).i == n y no llegamos al objetivo, ese camino falla.Recorrido breve del ejemplo:
i=0, suma=0.3 -> i=1, suma=3.2 -> i=2, suma=5.5 -> suma=10 (se poda: supera 7).5 -> fin de ese camino, falla.2 -> i=2, suma=3.5 -> suma=8 (se poda).5 -> falla.3 -> i=1, suma=0.2 -> i=2, suma=2.5 -> suma=7, éxito.Árbol de decisiones del mismo recorrido:
flowchart TD
A["i=0, suma=0"]
A -->|"incluir 3"| B["i=1, suma=3"]
A -->|"no incluir 3"| C["i=1, suma=0"]
B -->|"incluir 2"| D["i=2, suma=5"]
B -->|"no incluir 2"| E["i=2, suma=3"]
D -->|"incluir 5"| F["suma=10 (poda)"]
D -->|"no incluir 5"| G["i=3, suma=5 (falla)"]
E -->|"incluir 5"| H["suma=8 (poda)"]
E -->|"no incluir 5"| I["i=3, suma=3 (falla)"]
C -->|"incluir 2"| J["i=2, suma=2"]
C -->|"no incluir 2"| K["i=2, suma=0"]
J -->|"incluir 5"| L["suma=7 (exito)"]
J -->|"no incluir 5"| M["i=3, suma=2 (falla)"]
K -->|"incluir 5"| N["i=3, suma=5 (falla)"]
K -->|"no incluir 5"| O["i=3, suma=0 (falla)"]
classDef poda fill:#ffe8e8,stroke:#d64545,stroke-width:1.5px;
classDef exito fill:#e8ffe8,stroke:#2f9e44,stroke-width:2px;
class F,H poda;
class L exito;
#include <stdbool.h>
#include <stdio.h>
bool hay_subconjunto_suma_rec(const int v[], int n, int i, int suma, int objetivo) {
if (suma == objetivo) {
return true;
}
if (suma > objetivo || i == n) {
return false;
}
if (hay_subconjunto_suma_rec(v, n, i + 1, suma + v[i], objetivo)) {
return true;
}
return hay_subconjunto_suma_rec(v, n, i + 1, suma, objetivo);
}
bool hay_subconjunto_suma(const int v[], int n, int objetivo) {
return hay_subconjunto_suma_rec(v, n, 0, 0, objetivo);
}
int main(void) {
int v[] = {3, 2, 5};
int n = (int)(sizeof(v) / sizeof(v[0]));
int objetivo = 7;
if (hay_subconjunto_suma(v, n, objetivo)) {
printf("Existe un subconjunto con suma %d\n", objetivo);
} else {
printf("No existe un subconjunto con suma %d\n", objetivo);
}
return 0;
}
En el peor caso, en cada posición tomamos dos decisiones (incluir o no incluir). Eso genera un árbol de búsqueda de tamaño aproximado $2^n$.
Por eso:
La poda (por ejemplo, cortar cuando suma > objetivo) mejora muchos casos prácticos, pero no cambia el peor caso asintótico.
Ahora vemos un ejemplo en el que hay que buscar la salida en un laberinto.
Mapa (5x5):
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| 1 | I | . | . | . | # |
| 2 | # | # | . | . | # |
| 3 | . | . | . | # | # |
| 4 | # | . | . | S | # |
| 5 | # | # | # | # | # |
I: inicioS: salida#: bloqueada.: libreObjetivo: encontrar un camino de I a S moviéndonos en 4 direcciones.
En cada celda (f, c):
V.Arriba, Abajo, Derecha, Izquierda).Ese desmarcado es clave: permite que la celda pueda ser usada por otro camino alternativo.
Una traza posible sería:
I se prueba Arriba y falla (fuera de rango).#).S.#include <stdbool.h>
bool buscar_salida(
char **mapa,
int filas,
int columnas,
int f,
int c
) {
if (f < 0 || f >= filas || c < 0 || c >= columnas) {
return false;
}
if (mapa[f][c] == '#' || mapa[f][c] == 'V') {
return false;
}
if (mapa[f][c] == 'S') {
return true;
}
char anterior = mapa[f][c];
mapa[f][c] = 'V';
// Orden fijo de prueba: Arriba, Abajo, Derecha, Izquierda
if (buscar_salida(mapa, filas, columnas, f - 1, c) ||
buscar_salida(mapa, filas, columnas, f + 1, c) ||
buscar_salida(mapa, filas, columnas, f, c + 1) ||
buscar_salida(mapa, filas, columnas, f, c - 1)) {
mapa[f][c] = anterior;
return true;
}
mapa[f][c] = anterior;
return false;
}
Si la grilla tiene F filas y C columnas, llamando N = F * C:
En la práctica, las podas (bloqueos, límites, visitados y orden de movimientos) reducen mucho la exploración real.
Backtracking explora decisiones de forma sistemática, deshace cuando una rama no sirve y sigue con otra. Es muy útil para problemas combinatorios con restricciones, especialmente cuando podemos podar ramas temprano.