【动态规划专题】—— 动态规划思想入门篇
动态规划入门
一、什么是动态规划算法?
- 动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。
- 它通过把原问题分解为简单的子问题来解决复杂问题。
- 设置单独的变量来保存历史记录
二、利用动态规划算法求解问题的主要步骤
概述: \color{green}{概述:} 概述:动态规划就是利用历史记录来避免重复计算,有点像高中的递推公式。
-
第一步,创建变量来保存这个历史记录,一般选用一维数组或二维数组,最重要的是我们要明确数组变量 dp[i] 代表什么含义
-
第二步,确定数组元素之间的关系式,因为我们要利用历史数据来推出新的元素值
例如:dp[n] = dp[n - 1] + dp[n - 2];
-
第三步,找出初始的元素值,就像斐波那契数列一样,要有初始值才能推算出后面的元素值
总结: \color{green}{总结:} 总结:有了初始值和递推公式,我们就可以求出dp[n]
的值
案例详解动态规划
一、爬楼梯问题
- 问题描述:到达目的楼层有n个台阶,每次可以上一个台阶或两个台阶,问有多少种走法?
根据动态规划三部曲:
(1)我们先确定数组元素代表什么?
因为要统计爬到第n个台阶有多少种走法,我们可以将 dp[i] 的元素值定义为爬到第 i 个台阶的走法个数
(2)确定元素之间的关系式:
我们可以这样想,第 i 个台阶可以为从第 i - 1 个台阶上来的,也可以是从第 i - 2 个台阶上来的
所以我们可以得到 dp[i] = dp [i - 1] + dp[i - 2]
(3)找出初始值
根据问题描述,我们可以知道走一次可以有两种情况,所以初始值就为这两种对应的情况
dp[0] = 0、 dp[1] = 1、 dp[2] = 3
- 到了这里我们就可以解决这个问题啦,下面是Java实现的算法
class Solution { public int climbStairs(int n) { if(n == 1){ return n; } //因为第n层的走法为 dp[n],所以初始化的时候数组大小至少要为 n+1 int [] dp = new int[n + 1]; //设置初始值 dp[0] = 0; dp[1] = 1; dp[2] = 2; //通过公式计算出dp[n] for(int i = 3; i <= n; i++){ dp[i] = dp[i - 1] + dp[i - 2]; } //返回结果 return dp[n]; }}
二、机器人移动问题
- 问题描述:机器人从n * m 的矩阵的左上角移动到右下角每次只能向下或向右移动一步,问有多少种移动方法?
老样子,还是根据要求解的问题入手
(1)问到右下角有多少种走法,那么我们就将dp[i][j] 定义为到达矩阵中 (i, j) 处的移动方法数。
(2)因为每次只能向右或向下移动一步,所以到达 (i, j) 位置可能是从 (i - 1, j) 或 (i, j - 1) 过来的
得到元素之间的关系式 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
(3)对于初始值的确定,我们可以看什么情况下递推公式不满足了。
我们发现如果目标位置为 dp[0][j] 或dp[i][0] 就不满足了
对于到达这两类的位置只有一种方法,一直向右移动或一直向下移动到达目标位置
dp[0][j] = 1;dp[i][0] = 1;
- 利用Java来实现这个算法:
class Solution { public int uniquePaths(int m, int n) { if(m == 0 && n == 0){ return 0; } //创建二维数组 int [][] dp = new int[m][n]; /** 因为在外面直接初始化 dp[0][] = 1、 dp[][0] = 1 是错误的, 要通过循环来完成,所以我直接把这个工作放到双层for循环中来 完成,如果i或j为零,那么就将此时的 dp[i][j]初始化为零 */ for(int i = 0; i < m; i++){ for(int j = 0; j < n; j++){ if(i == 0 || j == 0){ dp[i][j] = 1; }else{ dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; } } } return dp[m - 1][n - 1]; }}
三、最小路径和问题
- 问题描述:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。
此题与机器人移动问题类似,不过 dp[i][j] 代表的是到达当前位置的最小路径和,属于求最优解问题。
因为移动方向是固定的,所以到(i, j) 可能从两个地方过来的(i - 1, j) (i, j - 1) 【假设此时i, j >= 1】,选择路径值和小的那个加上当前位置的值作为到该位置的最优路径和
初始值问题就是最上边的一行和最左边的一列
- 利用Java来实现这个算法:
class Solution { public int minPathSum(int[][] grid) { /** dp[i][j] 因为到达(i,j)点的路径可能存在多条,所以该变量代表到达该位置的最小路径和 dp[i][j] = Math.min(dp[i - 1][j] + dp[i][j - 1]) + grid[i][j]; dp[0][j] = dp[0][j - 1] + grid[0][j]; dp[i][0] = dp[i - 1][0] + grid[i][0]; */ //获取矩阵grid的大小 int n = grid.length; int m = grid[0].length; if(n == 0 || m == 0){ return 0; } //创建dp数组 int [][] dp = new int [n][m]; //初始化左上角 dp[0][0] = grid[0][0]; //初始化最上边的一行 for(int i = 1; i < m; i++){ dp[0][i] = dp[0][i - 1] + grid[0][i]; } //初始化最左边的一列 for(int i = 1; i < n; i++){ dp[i][0] = dp[i - 1][0] + grid[i][0]; } //利用递推公式计算最优路径 for(int i = 1; i < grid.length; i++){ for(int j = 1; j < grid[0].length; j++){ dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; } } return dp[n - 1][m - 1]; }}
四、编辑距离问题
- 问题描述:
- 📖 解题思路:
(1)确定变量的含义
问题要求什么,我们就设什么,由于我们的目的求将 word1 转换成 word2 所使用的最少操作数 。
当字符串 word1、word2 的长度分别为 i、j时,将 word1 转化为 word2 所使用的最少操作次数为 dp[i] [j]。【之所以将i、j设置为长度,就是方便比较每个字符】
(2)确定元素之间的关系
- word1[i] 与 word2[j]相等,不需要任何操作——
dp[i][j] = dp[i - 1][j - 1]
- word1[i] 与 word2[j]不相等,三个操作中进行选择:
在 word1末尾插入一个与 word2[j] 相同的字符,则有
dp[i][j] = dp[i] [j-1] + 1
把word1[i]处的字符删除,则有dp[i][j] = dp[i - 1][j] + 1
把字符 word1[i] 替换成与 word2[j] 相等,则有dp[i][j] = dp[i-1][j-1] + 1
(3)确定初始值
A字符串为空,那么就有两种操作方法:
依次删除B字符串中的字符 或 将B中字符依次对应插入到A中
dp[0][j] = dp[0][j - 1];dp[i][0] = dp[i - 1][0];
- 📝 利用Java来实现这个算法:
class Solution { public int minDistance(String word1, String word2) { int n = word1.length(); int m = word2.length(); //创建二维结果数组 int [][] dp = new int[n + 1][m + 1]; // dp[0][0...n2]的初始值 for (int j = 1; j <= m; j++){ dp[0][j] = dp[0][j - 1] + 1; } // dp[0...n1][0] 的初始值 for (int i = 1; i <= n; i++){ dp[i][0] = dp[i - 1][0] + 1; } // 通过公式推出 dp[n1][n2] for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { // 如果 word1[i] 与 word2[j] 相等。第 i 个字符对应下标是 i-1 if (word1.charAt(i - 1) == word2.charAt(j - 1)){ dp[i][j] = dp[i - 1][j - 1]; }else { dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1; } } } return dp[n][m];}}