所有的递归都需要改成动态规划吗?
答案是 不需要。
能改成动态规划的递归,统一特征:
决定返回值的可变参数类型往往都比较简单,一般不会比int类型更复杂。
在前面的 64. 最小路径和 问题中,可变参数是当前位置i和j,i,j是int型变量,很方便我们建立DP二维表格。
看下面的这道题,对于参数比较复杂的递归问题,比如 需要记录路径的递归 ,就不方便改为动态规划。
LeetCode 79. 单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
提示:
m == board.lengthn = board[i].length1 <= m, n <= 61 <= word.length <= 15board和word仅由大小写英文字母组成
**进阶:**你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?
问题分析:
一个没有学习过递归和DFS算法的人遇到这个问题的思路是这样的:
先找到所有值为word[0]的格子,之后对这些格子,每个格子都作为开头去尝试一遍,尝试的步骤:
1.查看旁边有没有值为word[i+1]的格子,如果有,则走到word[i+1]的格子,之后重新尝试
2.如果没有值为word[i+1]的格子,则这条路走不通,尝试其他路。
现在我们了解了递归,那我们可以把这个思路转化为递归的思路:
定义f(i,j,k)为将(i,j)格子作为开头能否匹配出(探索出)字符串word[k,...],如果能,则f(i,j,k)记为true,否则记为false。有了这个定义式,状态转移式子也很容易得出来:
递归的结束条件是:
匹配到最后一个字符并且匹配成功。即
最后要注意的是,但凡是 搜索问题, 都要防止“走回头路”,搜过的点(可以理解成搜索过的状态)不能再搜,所以一般来说我们都要采取一些辅助措施来避免“走回头路”。
解决代码:
static constexpr int directions[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool exist(vector<vector<char>>& board, string word) {
int rows = board.size();
int columns = board[0].size();
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (board[i][j] == word[0])
if (f(i, j, 0, board, word))
return true;
}
}
return false;
}
bool f(int i, int j, int k, vector<vector<char>>& board, string word) {
if (board[i][j] != word[k]) // 此节点不符合要求,不能再延伸搜索
return false;
if (k == word.size() - 1) //匹配成功
return true;
// 表示节点(i,j)在路径上
board[i][j] = '0';
// 向周边节点扩散
for (auto& direction : directions) {
int next_i = i + direction[0];
int next_j = j + direction[1];
if (next_i >= 0 && next_i < board.size() && next_j >= 0 &&
next_j < board[0].size() &&
f(next_i, next_j, k + 1, board, word)) {
board[i][j] = word[k];
return true;
}
}
// 恢复原样
board[i][j] = word[k];
return false;
}
现在来重新思考一下之前的问题,这段代码能改成二维DP的代码吗?
答案是不能,因为我们需要时刻记录当前的路径,DP做不到这一点。所以参数类型比较复杂的递归,没有必要改成DP。