最近做《剑指offer》矩阵中路径的题目,是一道典型的回溯法题目。 过程中犯了一些错误,记录一下。
1.迭代过程中对访问每个节点函数功能没想清除,违反了单一职责元素。
2.没利用或运算符短路求值的特性导致进行了多余运算
下面是例题以及改进过程
题目
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
主函数:遍历每个点,如果找到一条路径就返回true
bool exist(vector<vector<char>>& board, string word)
{
vector<vector<bool>> ischecked(board.size(), vector<bool>(board[0].size(), false));
for (int i = 0; i < board.size(); ++i)
for (int j = 0; j < board[0].size(); ++j)
if (dfs(board, word, 0, ischecked, i, j))
return true;
return false;
}
递归函数:
//版本一 求得所有能遍历结果再返回与运算结果,这样的坏处是如果b1为true,后面的其实不用算,要利用好或运算符的最短路径求法。
bool dfs(vector<vector<char>>& board, string& word, int checkindex, vector<vector<bool>>& ischecked, int row, int column)
{
if (checkindex == word.size() - 1 && word[checkindex] == board[row][column]) return true;
if (board[row][column] != word[checkindex]) return false;
ischecked[row][column] = true;
bool b1 = false, b2 = false, b3 = false, b4 = false;
if (row + 1 < board.size() && !ischecked[row + 1][column]) b1 = dfs(board, word, checkindex + 1, ischecked, row + 1, column);
if (row - 1 >= 0 && !ischecked[row - 1][column]) b2 = dfs(board, word, checkindex + 1, ischecked, row - 1, column);
if (column + 1 < board[0].size() && !ischecked[row][column + 1]) b3 = dfs(board, word, checkindex + 1, ischecked, row, column + 1);
if (column - 1 >= 0 && !ischecked[row][column - 1]) b4 = dfs(board, word, checkindex + 1, ischecked, row, column - 1);
ischecked[row][column] = false;
return b1 || b2 || b3 || b4;
}
//版本二 有点冗余
bool dfs(vector<vector<char>>& board, string& word, int checkindex, vector<vector<bool>>& ischecked, int row, int column)
{
if (checkindex == word.size() - 1 && word[checkindex] == board[row][column]) return true;
if (board[row][column] != word[checkindex]) return false;
ischecked[row][column] = true;
bool b1 = false, b2 = false, b3 = false, b4 = false;
if (row + 1 < board.size() && !ischecked[row + 1][column]) b1 = dfs(board, word, checkindex + 1, ischecked, row + 1, column);
if(b1 == true) return b1;
if (row - 1 >= 0 && !ischecked[row - 1][column]) b2 = dfs(board, word, checkindex + 1, ischecked, row - 1, column);
if(b2 == true) return b2;
if (column + 1 < board[0].size() && !ischecked[row][column + 1]) b3 = dfs(board, word, checkindex + 1, ischecked, row, column + 1);
if(b3 == true) return b3;
if (column - 1 >= 0 && !ischecked[row][column - 1]) b4 = dfs(board, word, checkindex + 1, ischecked, row, column - 1);
if(b4 == true) return b4;
ischecked[row][column] = false;
return false;
}
//版本三 对于一个格子,总是访问它相邻四个格子,至于能不能访问,由进入那个格子的函数进行判断。
//本质上,版本二的错误是功能划分失败,违反单一元素:一个格子只应该检查是否到达结束条件或者它能否访问,不应该去判断其他格子能否访问。
bool dfs(vector<vector<char>>& board, string& word, int checkindex, vector<vector<bool>>& ischecked, int row, int column)
{
if (checkindex == word.size()) return true;
bool result = false;
if (row >= 0 && row < board.size() && column >= 0 && column < board[0].size() && word[checkindex] == board[row][column] && !ischecked[row][column])
{
ischecked[row][column] = true;
result = dfs(board, word, checkindex + 1, ischecked, row + 1, column) ||
dfs(board, word, checkindex + 1, ischecked, row - 1, column) ||
dfs(board, word, checkindex + 1, ischecked, row, column + 1) ||
dfs(board, word, checkindex + 1, ischecked, row, column - 1);
ischecked[row][column] = false;
}
return result;
}
解答有关回溯题目的思路
通常用在二维方格运动的问题上。
- 确认递归结束条件的最简形式和返回值。
一点优化:通常递归状态可以分为递归过程中,递归结束。递归结束又可以分成达到完成条件结束和未达到完成条件结束。
通过排列组合这些递归状态可以减少代码量,
比如上面的例题先处理递归还没到结束的状态,然后默认返回false,而不是先处理未达成完成条件结束的代码;而机器人的运动范围这道题目,则是将递归结束两种情况统一处理。 - 根据单一职责原则想好递归函数应该干些什么,而哪些又不归它干。
比如上面一道题目版本二犯的错误,一个格子只应该检查是否到达结束条件或者它能否访问,不应该去判断其他格子能否访问。 - 对于只要求一条有效路径的题目,利用好或运算符短路求值特性。 见递归函数注释。
补充:回溯和深度优先区别
深度优先对于所有节点只访问一次,用一个visited标记每个点以前被访问过没有即可。 回溯对于一个节点可能访问多次,也可能一次都不访问,所以要记录这次遍历是否被访问,而不是记录以前有没有被访问。