动态规划通用解法

前言

最近看了左程云大神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) (m1,n1),所以当 i = m − 1 , j = n − 1 i = m - 1, j = n - 1 i=m1,j=n1时,递归结束;否则仅当 i = m − 1 i = m - 1 i=m1时,此时只能往右移动,即移动到 ( i + 1 , j ) (i + 1, j) (i+1,j)的位置,仅当 j = n − 1 j = n - 1 j=n1时,此时只能往下移动,即移动到 ( 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) (m1,n1) ( 0 , 0 ) (0, 0) (0,0)的路径数字的最小和(只能往上或往左)等价于上述问题,此时根据上述代码可知:


  • i = m − 1 , j = n − 1 i = m - 1, j = n - 1 i=m1,j=n1时,有 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=m1,j=0,1,2,...,n2时,只能往左走,有 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,...,m2,j=n1时,只能往上走,有 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,...m2,j=0,1,2,...,n2时,既能往左走,又能往右走,但我们需要选择数字和最小的路径,有 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];

相关链接


  1. https://www.bilibili.com/video/BV1ET4y1U7T6/?p=1&vd_source=2137b727093a8a6ae621d2bca2b3a16f ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值