Qual é a diferença entre retrocesso e primeira pesquisa em profundidade?


106

Qual é a diferença entre retrocesso e primeira pesquisa em profundidade?

Respostas:


98

Backtracking é um algoritmo de propósito mais geral.

A pesquisa em profundidade é uma forma específica de retrocesso relacionada à pesquisa de estruturas de árvore. Da Wikipedia:

Um começa na raiz (selecionando algum nó como a raiz no caso do gráfico) e explora o máximo possível ao longo de cada ramificação antes de retroceder.

Ele usa o retrocesso como parte de seu meio de trabalhar com uma árvore, mas está limitado a uma estrutura de árvore.

O retrocesso, entretanto, pode ser usado em qualquer tipo de estrutura onde partes do domínio podem ser eliminadas - seja ou não uma árvore lógica. O exemplo do Wiki usa um tabuleiro de xadrez e um problema específico - você pode olhar para um movimento específico e eliminá-lo, depois voltar para o próximo movimento possível, eliminá-lo etc.


13
Respondendo a uma postagem antiga aqui. Boa resposta, mas ... o problema do tabuleiro de xadrez também não poderia ser representado como uma árvore? :-) De qualquer posição em um tabuleiro de xadrez para uma determinada peça, não existe uma árvore de movimentos possíveis estendendo-se para o futuro? Parte de mim sente que qualquer caso em que o retrocesso poderia ser usado, também poderia ser modelado como uma árvore, mas não tenho certeza se estou correto nessa intuição.
The111 de

4
@ The111: Na verdade, qualquer problema de pesquisa pode ser representado como uma árvore - você tem um nó para cada solução parcial possível e uma borda de cada nó para uma ou mais opções alternativas possíveis que poderiam ser feitas neste estado. Acho que a resposta do LCN, que retrocesso geralmente significa DFS na árvore de pesquisa (geralmente implícita) gerada durante a recursão, chega mais perto da verdade.
j_random_hacker

5
@j_random_hacker Portanto, um DFS é uma maneira de explorar uma árvore (ou gráfico de forma mais geral), enquanto o retrocesso é uma maneira de resolver um problema (que emprega um DFS junto com a poda). :-)
The111

De maneira confusa, a Wikipedia descreve o retrocesso como um algoritmo de busca em profundidade , aparentemente combinando esses dois conceitos.
Anderson Green

29

Acho que essa resposta a outra pergunta relacionada oferece mais insights.

Para mim, a diferença entre o retrocesso e o DFS é que o retrocesso lida com uma árvore implícita e o DFS com uma explícita. Isso parece trivial, mas significa muito. Quando o espaço de busca de um problema é visitado por retrocesso, a árvore implícita é percorrida e podada no meio dele. Ainda assim, para o DFS, a árvore / gráfico com que ele lida é explicitamente construída e casos inaceitáveis ​​já foram descartados, ou seja, podados, antes que qualquer pesquisa seja feita.

Portanto, retroceder é DFS para árvore implícita, enquanto DFS retrocede sem podar.


Acho que é confuso pensar em retroceder como lidar com uma árvore implícita. Quando li isso pela primeira vez, concordei, mas indo mais fundo, percebi que a "árvore implícita" é realmente .. a árvore de recursão .. Pegue qualquer exemplo que use retrocesso, como permutar uma sequência de caracteres, não há árvore lógica (nenhuma árvore implícita) em relação ao problema, mas temos uma árvore de recursão que modela o processo de construção de string incremental. Quanto à poda, é a poda feita na árvore de recursão onde uma força bruta total é executada ... (continua)
Gang Fang

(continuação) Por exemplo, imprime todas as permutações da string de "resposta" e digamos que o terceiro caractere deve ser o caractere "a". Os primeiros 2 níveis da árvore de recursão obedecem a O (n!), Mas no terceiro nível todos os ramos, exceto aqueles que estão anexando "a", são podados (retrocedidos).
Gang Fang de

6

O retrocesso é geralmente implementado como DFS mais remoção de pesquisa. Você atravessa a árvore do espaço de busca, primeiro construindo soluções parciais ao longo do caminho. O DFS de força bruta pode construir todos os resultados da pesquisa, mesmo os que não fazem sentido na prática. Isso também pode ser muito ineficiente para construir todas as soluções (n! Ou 2 ^ n). Portanto, na realidade, ao fazer o DFS, você também precisa remover as soluções parciais, que não fazem sentido no contexto da tarefa real, e se concentrar nas soluções parciais, que podem levar a soluções ótimas válidas. Esta é a técnica real de retrocesso - você descarta as soluções parciais o mais cedo possível, dá um passo para trás e tenta encontrar o ótimo local novamente.

Nada para de percorrer a árvore do espaço de pesquisa usando BFS e executar a estratégia de retrocesso ao longo do caminho, mas não faz sentido na prática porque você precisaria armazenar o estado de pesquisa camada por camada na fila, e a largura da árvore cresce exponencialmente até a altura, portanto, perderíamos muito espaço muito rapidamente. É por isso que as árvores geralmente são percorridas usando DFS. Neste caso, o estado de pesquisa é armazenado na pilha (pilha de chamadas ou estrutura explícita) e não pode exceder a altura da árvore.


5

Normalmente, uma pesquisa em profundidade é uma forma de iterar por meio de uma estrutura de gráfico / árvore real em busca de um valor, enquanto o retrocesso é iterar por um espaço de problema em busca de uma solução. Backtracking é um algoritmo mais geral que não necessariamente se relaciona com árvores.


5

Eu diria que o DFS é a forma especial de retrocesso; retrocesso é a forma geral de DFS.

Se estendermos o DFS a problemas gerais, podemos chamá-lo de retrocesso. Se usarmos o retrocesso para problemas relacionados à árvore / gráfico, podemos chamá-lo de DFS.

Eles carregam a mesma ideia no aspecto algorítmico.


A relação entre DFS e Backtracking é, na verdade, apenas o oposto. Verifique minha resposta que detalha isso.
KGhatak


5

IMHO, a maioria das respostas são imprecisas e / ou sem qualquer referência a verificar. Então, deixe-me compartilhar uma explicação muito clara com uma referência .

Primeiro, o DFS é um algoritmo geral de passagem (e pesquisa) de gráfico. Portanto, pode ser aplicado a qualquer gráfico (ou mesmo floresta). Árvore é um tipo especial de gráfico, portanto, o DFS também funciona para árvore. Em essência, vamos parar de dizer que funciona apenas para uma árvore ou semelhantes.

Baseado em [1], Backtracking é um tipo especial de DFS usado principalmente para economizar espaço (memória). A distinção que estou prestes a mencionar pode parecer confusa, pois nos algoritmos de Graph desse tipo estamos tão acostumados a ter representações de lista de adjacências e usar um padrão iterativo para visitar todos os vizinhos imediatos ( para árvore, são os filhos imediatos ) de um nó , muitas vezes ignoramos que uma má implementação de get_all_immediate_neighbors pode causar uma diferença no uso de memória do algoritmo subjacente.

Além disso, se um nó de grafo tem fator de ramificação b e diâmetro h ( para uma árvore, esta é a altura da árvore ), se armazenarmos todos os vizinhos imediatos em cada etapa da visita a um nó, os requisitos de memória serão big-O (bh) . No entanto, se pegarmos apenas um único vizinho (imediato) de cada vez e o expandirmos, a complexidade da memória se reduzirá a big-O (h) . Enquanto o primeiro tipo de implementação é denominado DFS , o último tipo é denominado Backtracking .

Agora você vê, se você está trabalhando com linguagens de alto nível, provavelmente você está realmente usando o Backtracking disfarçado de DFS. Além disso, manter o controle dos nós visitados para um conjunto de problemas muito grande pode consumir muita memória; pedindo um design cuidadoso de get_all_immediate_neighbors (ou algoritmos que podem lidar com a revisitação de um nó sem entrar em um loop infinito).

[1] Stuart Russell e Peter Norvig, Artificial Intelligence: A Modern Approach, 3rd Ed


2

A profundidade primeiro é um algoritmo para percorrer ou pesquisar uma árvore. Vejo aqui . Backtracking é um termo muito mais amplo que é usado sempre que um candidato a solução é formado e posteriormente descartado pelo retrocesso para um estado anterior. Veja aqui . A primeira pesquisa em profundidade usa backtracking para pesquisar primeiro um branch (candidato à solução) e, se não for bem-sucedido, pesquisar os outros branch (s).


2

O DFS descreve a maneira como você deseja explorar ou percorrer um gráfico. Concentra-se no conceito de ir o mais fundo possível, dada a escolha.

O retrocesso, embora geralmente implementado via DFS, concentra-se mais no conceito de remoção de subespaços de pesquisa pouco promissores o mais cedo possível.


1

Em uma pesquisa aprofundada , você começa na raiz da árvore e, em seguida, explora ao longo de cada galho, depois volta para cada nó pai subsequente e atravessa seus filhos

Backtracking é um termo generalizado para começar no final de uma meta e retroceder gradativamente, construindo gradualmente uma solução.


4
Retroceder não significa começar do fim e retroceder. Ele mantém um registro dos nós visitados para retroceder se um beco sem saída for encontrado.
Günther Jena

1
"começando no final ...", hein !!
7kemZmani

1

IMO, em qualquer nó específico do retrocesso, você tenta aprofundar primeiro a ramificação em cada um de seus filhos, mas antes de se ramificar em qualquer nó filho, você precisa "apagar" o estado anterior do filho (esta etapa é equivalente a voltar caminhar até o nó pai). Em outras palavras, cada estado de irmão não deve impactar um ao outro.

Ao contrário, durante o algoritmo DFS normal, você geralmente não tem essa restrição, você não precisa apagar (retroceder) o estado anterior dos irmãos para construir o próximo nó irmão.


1

Ideia - Comece de qualquer ponto, verifique se é o ponto final desejado, se sim, então encontramos uma solução senão vai para todas as próximas posições possíveis e se não pudermos ir mais longe, volte para a posição anterior e procure outras alternativas marcando essa corrente o caminho não nos levará à solução.

Agora backtracking e DFS são 2 nomes diferentes dados à mesma ideia aplicada em 2 tipos de dados abstratos diferentes.

Se a ideia é aplicada na estrutura de dados da matriz, chamamos isso de backtracking.

Se a mesma ideia for aplicada em árvore ou gráfico, então o chamamos de DFS.

O clichê aqui é que uma matriz pode ser convertida em um gráfico e um gráfico pode ser transformado em uma matriz. Então, nós realmente aplicamos a ideia. Se estiver em um gráfico, então o chamamos de DFS e se estiver em uma matriz, então o chamamos de retrocesso.

A ideia em ambos os algoritmos é a mesma.


0

O retrocesso é apenas uma primeira pesquisa em profundidade com condições de encerramento específicas.

Considere caminhar por um labirinto onde para cada etapa que você toma uma decisão, essa decisão é uma chamada para a pilha de chamadas (que conduz sua primeira pesquisa em profundidade) ... se você chegar ao fim, poderá retornar ao seu caminho. No entanto, se você chegar a um beco sem saída, deseja retornar de uma determinada decisão, basicamente retornando de uma função em sua pilha de chamadas.

Então, quando penso em retroceder, me preocupo

  1. Estado
  2. Decisões
  3. Casos Base (Condições de Rescisão)

Eu explico no meu vídeo sobre retrocesso aqui .

Uma análise do código de backtracking está abaixo. Neste código de retrocesso, quero todas as combinações que resultarão em uma determinada soma ou meta. Portanto, eu tenho 3 decisões que fazem chamadas para minha pilha de chamadas, em cada decisão eu posso escolher um número como parte do meu caminho para chegar ao número de destino, pular esse número ou selecioná-lo e selecioná-lo novamente. E então, se eu atingir uma condição de encerramento, minha etapa de retrocesso é apenas retornar . Retornar é a etapa de retrocesso, porque sai dessa chamada na pilha de chamadas.

class Solution:    

"""

Approach: Backtracking 

State
    -candidates 
    -index 
    -target 

Decisions
    -pick one --> call func changing state: index + 1, target - candidates[index], path + [candidates[index]]
    -pick one again --> call func changing state: index, target - candidates[index], path + [candidates[index]]
    -skip one --> call func changing state: index + 1, target, path

Base Cases (Termination Conditions)
    -if target == 0 and path not in ret
        append path to ret
    -if target < 0: 
        return # backtrack 

"""

def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
    """
    @desc find all unique combos summing to target
    @args
        @arg1 candidates, list of ints
        @arg2 target, an int
    @ret ret, list of lists 
    """
    if not candidates or min(candidates) > target: return []

    ret = []
    self.dfs(candidates, 0, target, [], ret)
    return ret 

def dfs(self, nums, index, target, path, ret):
    if target == 0 and path not in ret: 
        ret.append(path)
        return #backtracking 
    elif target < 0 or index >= len(nums): 
        return #backtracking 


    # for i in range(index, len(nums)): 
    #     self.dfs(nums, i, target-nums[i], path+[nums[i]], ret)

    pick_one = self.dfs(nums, index + 1, target - nums[index], path + [nums[index]], ret)
    pick_one_again = self.dfs(nums, index, target - nums[index], path + [nums[index]], ret)
    skip_one = self.dfs(nums, index + 1, target, path, ret)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.