使用回溯(而不是 DFS)背后的直觉
Posted
技术标签:
【中文标题】使用回溯(而不是 DFS)背后的直觉【英文标题】:Intuition behind using backtracking (and not DFS) 【发布时间】:2018-03-16 00:56:36 【问题描述】:我正在解决 LeetCode.com 上的Word Search 问题:
给定一个二维板和一个单词,找出该单词是否存在于网格中。
单词可以由顺序相邻单元格的字母构成,其中“相邻”单元格是水平或垂直相邻的单元格。同一个字母单元格不能多次使用。
我用在线帮助写的解决方案如下:
class Solution
public:
//compare this with Max Area of Island:
//they 'look' similar, but this one uses a backtracking approach since we retract when we don't find a solution
//In case of Max Area Of Island, we are not 'coming back' - technically we do come back due to recursion, but we don't track
//that since we don't acutally intend to do anything - we are just counting the 1s.
bool exist(vector<vector<char>>& board, string& word)
if(board.empty()) return false;
for(int i=0; i<board.size(); i++)
for(int j=0; j<board[0].size(); j++)
//if(word[0] == board[i][j])
if(existUtil(board, word, i, j, 0)) //matching the word[i][j] with 0th character of word
return true;
return false;
bool existUtil(vector<vector<char>>& board, string& word, int i, int j, int match)
if(match==word.size()) return true;
if(i<0 || i>=board.size() || j<0 || j>=board[0].size()) return false;
if(board[i][j]!=word[match]) return false;
board[i][j] = '*';
bool ans = existUtil(board, word, i+1, j, match+1) || //[i+1,j]
existUtil(board, word, i-1, j, match+1) || // [i,j+1]
existUtil(board, word, i, j+1, match+1) || // [i-1,j]
existUtil(board, word, i, j-1, match+1); // [i,j-1]
board[i][j] = word[match];
return ans;
;
我的问题很简单 - 为什么我们使用回溯方法而不仅仅是传统的 DFS?与我们所做的非常相似,我们可以从每个字符开始并进行 DFS 以确定是否可以找到目标单词。但是我们没有这样做,为什么?
我想了很多,并提出了以下推理,但我不确定 - 我们使用回溯方法,因为 同一个字母单元格可能不会被多次使用。 所以,当我们进行回溯时,我们用'*'替换原始字符,然后在我们回来时重新替换它。但这在某种程度上感觉不对,因为我们可以使用 visited
矩阵来代替。
【问题讨论】:
DFS是一种回溯 请描述如果您的程序使用(您认为是)DFS 会有什么不同。 @aschepler,它几乎是相同的 - 只是我不会有board[i][j] = '*';
和 board[i][j] = word[match];
而是有 visited[][]
以确保我不会访问同一个元素两次。
@Yola,相反,我会说回溯是一种 DFS。你怎么看?
我认为您不仅可以通过 DFS 方式回溯
【参考方案1】:
问:我的问题很简单 - 为什么我们使用回溯方法而不仅仅是传统的 DFS?
因为回溯对于解决这类问题比普通的 DFS 更有效。
DFS 和回溯之间的区别很细微,但我们可以这样总结:DFS 是一种图搜索技术,而回溯是一种问题解决技术(由 DFS + pruning 组成,此类程序称为回溯器)。因此,DFS 会访问每个节点,直到找到所需的值(在您的情况下为目标词),而回溯更智能 - 当确定在那里找不到目标词时,它甚至不会访问特定的分支。
假设您有一本包含所有可能单词的字典,并在板上搜索以找到板上存在的所有单词(Boggle 游戏)。您开始遍历棋盘并按顺序偶然发现字母“J”、“A”、“C”,因此当前前缀是“JAC”。伟大的。让我们看看字母“C”的邻居,例如它们是“A”、“Q”、“D”、“F”。普通的 DFS 会做什么?它会跳过'A',因为它来自那个节点到'C',但是它会盲目地访问每个剩余的节点,希望找到一些单词,即使我们知道没有以“JACQ”、“JACD”开头的单词”和“JACF”。 Backtracker 将立即修剪带有“JACQ”、“JACD”和“JACF”的分支,例如咨询从字典构建的辅助 trie 数据结构。在某些时候,即使 DFS 也会回溯,但只有在它没有去向的情况下——即所有周围的字母都已被访问过。
总而言之-在您的示例中,传统的 DFS 将对每个节点盲目地检查所有相邻节点,直到找到目标单词或直到其所有邻居都被访问-它才会回溯。另一方面,Backtracker 会不断检查我们是否在“正确的轨道”上,执行此操作的代码中的关键行是:
if (board[i][j] != word[match]) return false;
【讨论】:
这里的关键:因此,DFS 访问每个节点,直到找到所需的值(在您的情况下为目标词),而回溯更智能 - 当它确定时,它甚至不会访问特定的分支在那里找不到目标词。以上是关于使用回溯(而不是 DFS)背后的直觉的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode 988:回溯和深度优先搜索(DFS)的区别