目录
什么是动态规划?
动态规划,英⽂:Dynamic Programming,简称DP,如果某⼀问题有很多重叠⼦问题,使⽤动态规划是最有效的。所以动态规划中每⼀个状态⼀定是由上⼀个状态推导出来的,这⼀点就区分于贪⼼,贪⼼没有状态推导,⽽是从局部直接选最优的。
动态规划问题解决步骤?
1. 确定dp数组(dp table)以及下标的含义
2. 确定递推公式
3. dp数组如何初始化
4. 确定遍历顺序
5. 举例推导dp数组
动态规划问题debug?
把 dp 数组打印出来,看看是不是按照自己的思路推导的。
例题:
例1:斐波那契数列
斐波那契数,通常⽤ F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后⾯每⼀项数字都是前⾯两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你n ,请计算 F(n) 。
示例 1:
输⼊:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输⼊:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输⼊:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
题解:
这里用一个一维数组来保存结果
1,确定 dp 数组以及下标的含义
dp [ i ] 的定义为:第 i 个数的斐波那契数值是 dp [ i ]
2,确定递推公式
题目中已给出:dp[i] = dp[i - 1] + dp[i - 2];
3. dp数组如何初始化
题目中已给出:dp [0] = 0; dp [1] = 1;
4. 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序是从前到后遍历的
5. 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导⼀下,当N为10的时候,dp数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55
Java代码如下:
public class 斐波那契数列 {
public static void main(String[] args) {
System.out.println(fib(10));
}
static int fib(int n) {
int []dp = new int [n+1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
例 2:爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的⽅法可以爬到楼顶呢?
注意:给定 n 是⼀个正整数。
示例 1:
输⼊: 2
输出: 2
解释: 有两种⽅法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输⼊: 3
输出: 3
解释: 有三种⽅法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
题解:
1. 确定dp数组以及下标的含义
dp [ i ]: 爬到第 i 层楼梯,有dp [ i ] 种⽅法
2. 确定递推公式
从dp [ i ]的定义可以看出,dp [ i ] 可以有两个⽅向推出来。
⾸先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种⽅法,那么再⼀步跳⼀个台阶不就是dp[i]了么。
还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种⽅法,那么再⼀步跳两个台阶不就是dp[i]了么。
那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!
所以dp[i] = dp[i - 1] + dp[i - 2] 。
3. dp数组如何初始化
因为n要求为正整数,所以不考虑dp[0]如果初始化,只初始化dp[1] = 1,dp[2] = 2
4. 确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序⼀定是从前向后遍历的
5. 举例推导dp数组
i = 1,dp [i]=1;
i = 2,dp [i]=2;
i = 3,dp [i]=3;
i = 4,dp [i]=5;
i = 5,dp [i]=8;
和斐波那契十分相像,但是递推公式是需要自己推导出来;
Java代码如下:
public class 爬楼梯 {
public static void main(String[] args) {
System.out.println(fib(5));
}
static int fib(int n) {
int []dp = new int [n];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
例 3 :使用最小花费爬楼梯
数组的每个下标作为⼀个阶梯,第 i 个阶梯对应着⼀个⾮负数的体⼒花费值 cost[i](下标从 0 开始)。每当你爬上⼀个阶梯你都要花费对应的体⼒值,⼀旦⽀付了相应的体⼒值,你就可以选择向上爬⼀个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
输⼊:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后⾛两步即可到阶梯顶,⼀共花费 15 。
示例 2:
输⼊:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费⽅式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,⼀共花费 6 。
题解:
1. 确定dp数组以及下标的含义
使⽤动态规划,就要有⼀个数组来记录状态,本题只需要⼀个⼀维数组dp[i]就可以了。
dp[i]的定义:到达第i个台阶所花费的最少体⼒为dp[i]。(注意这⾥认为是第⼀步⼀定是要花费)
2. 确定递推公式
可以有两个途径得到dp[i],⼀个是dp[i-1] ⼀个是dp[i-2]。
那么究竟是选dp[i-1]还是dp[i-2]呢?
⼀定是选最⼩的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
注意这⾥为什么是加cost[i],⽽不是cost[i-1],cost[i-2]之类的,因为题⽬中说了:每当你爬上⼀个阶梯都要花费对应的体⼒值
3. dp数组如何初始化
根据dp数组的定义,dp数组初始化其实是⽐较难的,因为不可能初始化为第i台阶所花费的最少体⼒。那么看⼀下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。
所以初始化代码为:dp[0] = cost[0]; dp[1] = cost[1];
4. 确定遍历顺序
因为是模拟台阶,⽽且dp[i]⼜dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。
5. 举例推导dp数组
拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟⼀下dp数组的状态变化,如下:
dp = [1,100,2,3,3,103,4,5,104,6]
java代码:
public class 花费最小力气爬楼梯 {
public static void main(String[] args) {
int []cost = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
System.out.println(fib(cost));
}
static int fib(int []cost) {
int []dp = new int [cost.length];
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < cost.length; i++) {
dp[i]=Math.min(dp[i-1], dp[i-2])+cost[i];
}
return dp[cost.length-1];
}
}
例 4:不同路径
⼀个机器⼈位于⼀个 m x n ⽹格的左上⻆ (起始点在下图中标记为 “Start” )。机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⻆(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
示例1:
输⼊:m = 3, n = 7
输出:28
题解:
1. 确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
2. 确定递推公式
想要求dp[i][j],只能有两个⽅向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。
此时在回顾⼀下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有⼏条路径,dp[i][j - 1
]同理。那么很⾃然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个⽅向过来。
3. dp数组的初始化
如何初始化呢,⾸先dp[i][0]⼀定都是1,因为从(0, 0)的位置到(i, 0)的路径只有⼀条,那么dp[0][j]也同理。所以初始化代码为:
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
4. 确定遍历顺序
这⾥要看⼀下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上⽅和左⽅推导⽽来,那么从左到右⼀层⼀层遍历就可以了。
这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]⼀定是有数值的。
5. 举例推导dp数组
如图所示:
Java代码:
public class 最短路径 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int m=sc.nextInt();
int n=sc.nextInt();
int [][]arr = new int[m][n];
System.out.println(fib(arr));
}
static int fib(int [][]arr) {
for (int i = 0; i < arr.length; i++) {
arr[i][0]=1;
}
for (int i = 0; i < arr[0].length; i++) {
arr[0][i]=1;
}
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j < arr[0].length; j++) {
arr[i][j]=arr[i-1][j]+arr[i][j-1];
}
}
return arr[arr.length-1][arr[0].length-1];
}
}
例 5 :不同路径 II
⼀个机器⼈位于⼀个 m x n ⽹格的左上⻆ (起始点在下图中标记为“Start” )。机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⻆(在下图中标记为“Finish”)。
现在考虑⽹格中有障碍物。那么从左上⻆到右下⻆将会有多少条不同的路径?
⽹格中的障碍物和空位置分别⽤ 1 和 0 来表示。
示例 1 :
输⼊:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 ⽹格的正中间有⼀个障碍物。
从左上⻆到右下⻆⼀共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输⼊:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
题解:
1. 确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
2. 确定递推公式
递推公式和62.不同路径⼀样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。但这⾥需要注意⼀点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。所以代码为:
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
3. dp数组如何初始化
不同路径中我们给出如下的初始化:
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
因为从(0, 0)的位置到(i, 0)的路径只有⼀条,所以dp[i][0]⼀定为1,dp[0][j]也同理。
但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是⾛不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。
所以本题初始化代码为:
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1;
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1;
注意代码⾥for循环的终⽌条件,⼀旦遇到obstacleGrid[i][0] == 1的情况就停⽌dp[i][0]的赋值1的操
作,dp[0][j]同理
4. 确定遍历顺序
从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,⼀定是从左到右⼀层⼀层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]⼀定是有数值。
5. 举例推导dp数组
题解:
public class 不同路径II {
public static void main(String[] args) {
int [][]arr = {{0,0,0},{0,1,0},{0,0,0}};
System.out.println(fib(arr));
}
static int fib(int [][]arr) {
int [][]dp = new int[arr.length][arr[0].length];
for (int i = 0; i < arr.length&&arr[i][0]==0; i++) {
dp[i][0]=1;
}
for (int i = 0; i < arr[0].length&&arr[0][i]==0; i++) {
dp[0][i]=1;
}
for (int i = 1; i < dp.length; i++) {
for (int j = 1; j < dp[0].length; j++) {
if(arr[i][j]!=1) {
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
// for (int i = 0; i < dp.length; i++) {
// for (int j = 0; j < dp[0].length; j++) {
// System.out.print(dp[i][j]);
// }
// System.out.println();
// }
return arr[arr.length-1][arr[0].length-1];
}
}
例 6:整数拆分:
给定⼀个正整数 n,将其拆分为⾄少两个正整数的和,并使这些整数的乘积最⼤化。 返回你可以获得的最⼤乘积
示例 1:
输⼊: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输⼊: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不⼩于 2 且不⼤于 58。