Backtracking explora un árbol de decisiones. En cada nodo:
elige una opción;
modifica el estado;
explora recursivamente;
deshace la elección;
prueba la siguiente opción.
Texto
choose → explore → unchoose
No es simplemente “usar recursión”. La idea central es construir soluciones incrementales y restaurar el estado para compartir una misma estructura entre ramas.
current contiene un subconjunto cuyos índices son estrictamente crecientes y todos son menores que start para la próxima elección. Por eso no genera el mismo conjunto en órdenes diferentes.
En una permutación el orden importa. Cada nivel elige un elemento todavía no usado.
TypeScript
functionpermutations<T>(values:readonlyT[]):T[][]{const result:T[][]=[];const current:T[]=[];const used =Array(values.length).fill(false);functionvisit():void{if(current.length === values.length){
result.push([...current]);return;}for(let index =0; index < values.length; index +=1){if(used[index])continue;
used[index]=true;
current.push(values[index]);visit();
current.pop();
used[index]=false;}}visit();return result;}
functioncombinationSum(
candidates:readonlynumber[],
target:number,):number[][]{const sorted =[...newSet(candidates)].filter((value)=> value >0).sort((a, b)=> a - b);const result:number[][]=[];const current:number[]=[];functionvisit(start:number, remaining:number):void{if(remaining ===0){
result.push([...current]);return;}for(let index = start; index < sorted.length; index +=1){const value = sorted[index];if(value > remaining)break;
current.push(value);visit(index, remaining - value);
current.pop();}}visit(0, target);return result;}
El break solo es seguro porque todos los candidatos son positivos y están ordenados. Con negativos, un valor mayor que remaining podría compensarse después.
Backtracking distingue caminos. DP combina caminos que llegan al mismo estado futuro.
Pregunta:
Si dos historias llegan a la misma información relevante, ¿las decisiones futuras son idénticas?
Si sí, memoization puede evitar repetir el subárbol. Si necesitas enumerar cada solución completa, fusionar estados puede requerir guardar cómo reconstruirlas.
Backtracking suele usar DFS sobre un árbol de decisiones implícito. No todo DFS es backtracking: recorrer un árbol existente sin tomar y deshacer decisiones no necesariamente lo es.
¿Por qué result.push(current) suele ser un bug en una implementación con push y pop?
Respuesta
Porque guarda la misma referencia mutable en todos los resultados. Las modificaciones posteriores cambian lo que ven todas las entradas. Debe guardarse una copia como [...current].