前言
最近看了左程云大神1的动态规划算法视频,算是初步掌握了动态规划类型的解法,实现思路分为三步:
- 1 暴力递归:写出简单的暴力递归实现方法(最重要的一步)。
- 2 记忆化递归:根据变量的变化范围,构造一个数组来存储已经遍历过的过程,对暴力递归过程进行剪枝。
- 3 优化递归过程:根据记忆化数组的变化过程,构造动态规划状态转移方程,实现动态规格。
状态转移方程本质上是经过上述三步逐渐构造出来的,初学者按照上述顺序一步一步实现,就能从本质上掌握动态规划问题的解法。
例子
下面让我们用上述思路求解Leetcode上的一道动态规划问题。
题目
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
暴力解法
根据题意可知,若当前网格的位置为 ( i , j ) , 0 < = i < m , 0 < = j < n (i, j), 0 <= i < m, 0 <= j < n (i,j),0<=i<m,0<=j<n,由于目标是移动到 ( m − 1 , n − 1 ) (m - 1, n - 1) (m−1,n−1),所以当 i = m − 1 , j = n − 1 i = m - 1, j = n - 1 i=m−1,j=n−1时,递归结束;否则仅当 i = m − 1 i = m - 1 i=m−1时,此时只能往右移动,即移动到 ( i + 1 , j ) (i + 1, j) (i+1,j)的位置,仅当 j = n − 1 j = n - 1 j=n−1时,此时只能往下移动,即移动到 ( i , j + 1 ) (i, j + 1) (i,j+1)的位置,否则既能够向右移动,也能够向下移动,如下代码所示:
int bfs(const vector<vector<int>>& grid, int i, int j)
{
if (i == m - 1 && j == n - 1){
return grid[i][j];
}
if (i == m - 1){
return grid[i][j] + bfs(grid, i, j + 1);
}
if (j == n - 1){
return grid[i][j] + bfs(grid, i + 1, j);
}
return min(bfs(grid, i + 1, j), bfs(grid, i, j + 1)) + grid[i][j];
}
由于题目的意思是返回路径上的数字最小总和,所以函数返回值就为当前路径的最小数字和加上当前位置的数字,当只有一种方向时只有一种路径,直接加上,当有多种路径时,需要选择数字和最小的路径,所以用了min函数判断。
记忆化搜索
根据上述的暴力解法,我们发现,最后的结果只受到当前位置 ( i , j ) (i ,j) (i,j)的影响,由于 0 < = i < m , 0 < = j < n 0 <= i < m, 0 <= j < n 0<=i<m,0<=j<n,所以可以构造一个数组来进行记忆化搜索:
int dp[m][n];
// 用-1值表示未访问过
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
dp[i][j] = -1;
}
}
当 d p [ i ] [ j ] dp[i][j] dp[i][j]访问过时,直接返回保存的 d p [ i ] [ j ] dp[i][j] dp[i][j]的结果,否则用一个数字来存储结果,并继续遍历。
int bfs(const vector<vector<int>>& grid, int i, int j, vector<vector<int>>& dp){
if (dp[i][j] != -1) {
return dp[i][j];
}
int ans = 0;
if (i == m - 1 && j == n - 1){
ans = grid[i][j];
}
else if (i == m - 1){
ans = grid[i][j] + bfs(grid, i, j + 1, dp);
}
else if (j == n - 1){
ans = grid[i][j] + bfs(grid, i + 1, j, dp);
}
else {
ans = min(bfs(grid, i + 1, j, dp), bfs(grid, i, j + 1, dp)) + grid[i][j];
}
dp[i][j] = ans;
return ans;
}
求从 ( m − 1 , n − 1 ) (m - 1, n - 1) (m−1,n−1)到 ( 0 , 0 ) (0, 0) (0,0)的路径数字的最小和(只能往上或往左)等价于上述问题,此时根据上述代码可知:
- 当 i = m − 1 , j = n − 1 i = m - 1, j = n - 1 i=m−1,j=n−1时,有 d p [ i ] [ j ] = g r i d [ i ] [ j ] dp[i][j] = grid[i][j] dp[i][j]=grid[i][j]
- 当 i = m − 1 , j = 0 , 1 , 2 , . . . , n − 2 i = m - 1, j=0,1,2,...,n-2 i=m−1,j=0,1,2,...,n−2时,只能往左走,有 d p [ i ] [ j ] = g r i d [ i ] [ j ] + d p [ i ] [ j + 1 ] dp[i][j]=grid[i][j] + dp[i][j+1] dp[i][j]=grid[i][j]+dp[i][j+1]
- 当 i = 0 , 1 , 2 , . . . , m − 2 , j = n − 1 i=0,1,2,...,m-2, j= n - 1 i=0,1,2,...,m−2,j=n−1时,只能往上走,有 d p [ i ] [ j ] = g r i d [ i ] [ j ] + d p [ i + 1 ] [ j ] dp[i][j]=grid[i][j] + dp[i+1][j] dp[i][j]=grid[i][j]+dp[i+1][j]
- 当 i = 0 , 1 , 2 , . . . m − 2 , j = 0 , 1 , 2 , . . . , n − 2 i=0,1,2,...m-2, j=0,1,2,...,n-2 i=0,1,2,...m−2,j=0,1,2,...,n−2时,既能往左走,又能往右走,但我们需要选择数字和最小的路径,有 d p [ i ] [ j ] = g r i d [ i ] [ j ] + m i n ( d p [ i + 1 ] [ j ] , d p [ i ] [ j + 1 ] ) dp[i][j]=grid[i][j] + min(dp[i + 1][j], dp[i][j+1]) dp[i][j]=grid[i][j]+min(dp[i+1][j],dp[i][j+1])
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
dp[m - 1][n - 1] = grid[m - 1][n - 1]; // 初始
for (int j = n - 2; j >= 0; j --){
dp[m - 1][j] = dp[m - 1][j + 1] + grid[m - 1][j];
}
for (int i = m - 2; i >= 0; i --){
dp[i][n - 1] = dp[i + 1][n - 1] + grid[i][n - 1];
for (int j = n - 2; j >= 0; j --){
dp[i][j] = grid[i][j] + min(dp[i + 1][j], dp[i][j + 1]);
}
}
return dp[0][0];
相关链接
https://www.bilibili.com/video/BV1ET4y1U7T6/?p=1&vd_source=2137b727093a8a6ae621d2bca2b3a16f ↩︎