Backtracking
Introdução
Backtracking foi introduzido por D. R. Knuth em 1975 e é uma técnica de resolução de problemas que explora todas as soluções possíveis para encontrar a solução correta. Foi popularizado por Donald Knuth em seu livro "The Art of Computer Programming". O termo "backtracking" refere-se ao processo de retroceder para uma solução anterior quando uma solução parcial não leva a uma solução completa. Essa técnica é amplamente utilizada em problemas de busca, como quebra-cabeças, jogos e problemas de otimização.
O algoritmo funciona explorando todas as soluções possíveis, seguindo um caminho até encontrar uma solução completa ou até que não haja mais opções a serem exploradas. O algoritmo pode ser descrito como uma busca em profundidade, onde o algoritmo tenta construir uma solução parcial e, se essa solução não for válida, retrocede e tenta outra opção.
Principais problemas resolvidos com Backtracking
- Permutação: Gerar todas as permutações de um conjunto de números.
- Exemplo: Gerar todas as permutações do conjunto {1, 2, 3} resulta em {1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1}.
- Combinação: Gerar todas as combinações de um conjunto de números.
- Exemplo: Gerar todas as combinações do conjunto {1, 2, 3} resulta em {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}.
- Subconjunto: Gerar todos os subconjuntos de um conjunto de números.
- Exemplo: Gerar todos os subconjuntos do conjunto {1, 2, 3} resulta em {}, {1}, {2}, {3}, {1, 2}, {1, 3}, {2, 3}, {1, 2, 3}.
- Problema das N-rainhas: Colocar N rainhas em um tabuleiro de xadrez de forma que nenhuma rainha ataque outra.
- Exemplo: Para N=4, uma solução é colocar as rainhas nas posições (0, 1), (1, 3), (2, 0), (3, 2).
- Problema do Sudoku: Resolver um quebra-cabeça de Sudoku preenchendo os números de forma que cada linha, coluna e subgrade 3x3 contenha todos os números de 1 a 9.
- Exemplo: Resolver um quebra-cabeça de Sudoku 9x9 preenchendo os números de forma que cada linha, coluna e subgrade 3x3 contenha todos os números de 1 a 9.
Backtracking na prática em problemas reais
- Jogos: Resolver quebra-cabeças, como Sudoku, e jogos de tabuleiro, como o problema das N-rainhas.
- Otimização: Encontrar a melhor solução para problemas de otimização, como o problema do caixeiro viajante.
- Inteligência Artificial: Resolver problemas de busca em inteligência artificial, como jogos de tabuleiro e quebra-cabeças.
- Análise Combinatória: Gerar combinações e permutações de conjuntos de dados.
Conceitos importantes
- Solução parcial: Uma solução que não é completa, mas pode ser estendida para uma solução completa.
- Solução completa: Uma solução que atende a todos os requisitos do problema.
- Espaço de busca: O conjunto de todas as soluções possíveis para o problema.
- Caminho: Uma sequência de decisões que leva a uma solução parcial ou completa.
- Corte: Uma decisão que não leva a uma solução válida e, portanto, não precisa ser explorada.
- Podas: Técnicas usadas para eliminar partes do espaço de busca que não precisam ser exploradas.
- Heurísticas: Estratégias usadas para guiar a busca em direção a soluções mais promissoras.
- Recursão: Uma técnica de programação onde uma função chama a si mesma para resolver subproblemas.
- Backtracking: O processo de voltar atrás e tentar outra abordagem quando uma solução não é válida ou não leva a uma solução completa.
Complexidade assintótica do Backtracking
A complexidade do algoritmo de Backtracking depende do problema específico e da implementação. No entanto, em geral, a complexidade pode ser expressa como:
O(b^d)
onde:
b
é o fator de ramificação (o número de opções disponíveis em cada nível da árvore de busca).d
é a profundidade da solução (o número máximo de níveis na árvore de busca).
Tabela com exemplos de complexidade, pior, média e melhor caso:
Problema | Pior Caso | Melhor Caso | Caso Médio |
---|---|---|---|
Permutação | O(n!) | O(1) | O(n!) |
Combinação | O(2^n) | O(1) | O(2^n) |
Subconjunto | O(2^n) | O(1) | O(2^n) |
Problema das N-rainhas | O(N!) | O(1) | O(N!) |
Problema do Sudoku | O(9^n) | O(1) | O(9^n) |
Como funciona o Algoritmo
O algoritmo de Backtracking funciona explorando todas as soluções possíveis para um problema, seguindo um caminho até encontrar uma solução completa ou até que não haja mais opções a serem exploradas. O algoritmo pode ser descrito em três etapas principais:
- Escolha: Seleciona uma opção do conjunto de soluções possíveis.
- Exploração: Verifica se a opção escolhida leva a uma solução válida. Se sim, continua explorando essa opção.
- Retrocesso: Se a opção escolhida não leva a uma solução válida, o algoritmo retrocede e tenta outra opção.
Passo a Passo Teórico
- Inicialização: Comece com uma solução parcial vazia.
- Escolha: Se a solução parcial não é completa, escolha uma opção do conjunto de soluções possíveis.
- Verificação: Verifique se a solução parcial é válida.
- Se for válida, adicione a opção à solução parcial e continue explorando.
- Se não for válida, retroceda e escolha outra opção.
- Solução Completa: Se a solução parcial é completa, armazene ou imprima a solução.
- Retrocesso: Se não houver mais opções a serem exploradas, retroceda para a última decisão e escolha outra opção.
- Repetição: Repita os passos 2 a 5 até que todas as soluções possíveis tenham sido exploradas.
- Finalização: Quando todas as opções tiverem sido exploradas, o algoritmo termina.
Pseudocódigo e Template
function backtrack(solution):
if isComplete(solution):
store(solution)
return
for option in options:
if isValid(option):
add(option, solution)
backtrack(solution)
remove(option, solution)
Implementação em JavaScript
Problema: Gerar todas as sub conjunto (subset) de um conjunto de números.
function backtrack(nums, start, path, result) {
result.push([...path]);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]); // Adiciona o número atual ao caminho
backtrack(nums, i + 1, path, result); // Chama recursivamente
path.pop(); // Remove o último número do caminho (retrocesso)
}
}
function generateSubSets(nums) {
const result = [];
backtrack(nums, 0, [], result);
return result;
}
const nums = [1, 2, 3];
const result = generateSubSets(nums);
console.log(result); // Exibe todas os subconjuntos
// Exibe: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]
Variações do Backtracking
- Backtracking com Podas: Utiliza técnicas de poda para eliminar partes do espaço de busca que não precisam ser exploradas.
- Backtracking com Heurísticas: Utiliza heurísticas para guiar a busca em direção a soluções mais promissoras.
- Backtracking com Recursão: Utiliza recursão para explorar soluções parciais e completas.
- Backtracking Iterativo: Utiliza uma abordagem iterativa em vez de recursiva para explorar soluções.
- Backtracking com Memorização: Utiliza memorização para armazenar soluções parciais já calculadas e evitar cálculos repetidos.
- Backtracking com Programação Dinâmica: Combina técnicas de backtracking e programação dinâmica para resolver problemas complexos.
- Backtracking com Algoritmos Genéticos: Utiliza técnicas de algoritmos genéticos para explorar soluções em um espaço de busca complexo.
- Backtracking com Algoritmos de Busca: Combina técnicas de backtracking com algoritmos de busca para explorar soluções em um espaço de busca complexo.
- Entre muitas outras variações.
Apendix
Qual a diferença entre Backtracking e Programação Dinâmica?
A principal diferença entre Backtracking e Programação Dinâmica é a abordagem que cada técnica usa para resolver problemas. O Backtracking explora todas as soluções possíveis, enquanto a Programação Dinâmica divide o problema em subproblemas menores e resolve cada um deles apenas uma vez, armazenando os resultados para evitar cálculos repetidos.
Qual a diferença entre Permutação, Combinação e Subconjunto?
- Permutação: Uma permutação é uma disposição de todos os elementos de um conjunto em uma ordem específica. Por exemplo, as permutações do conjunto {1, 2} são {1, 2} e {2, 1}.
- Combinação: Uma combinação é uma seleção de elementos de um conjunto, sem considerar a ordem. Por exemplo, as combinações do conjunto {1, 2} são {1, 2}.
- Subconjunto: Um subconjunto é qualquer conjunto que pode ser formado a partir de um conjunto original, incluindo o conjunto vazio e o próprio conjunto. Por exemplo, os subconjuntos do conjunto {1, 2} são {}, {1}, {2} e {1, 2}.
Em inglês:
- Permutacão: Permutation
- Combinação: Combination
- Subconjunto: Subset