什么是动态规划?
首先给出一道例题:
给定一个矩阵网格,一个机器人从左上角出发,每次可以向下或向右走一步。
A:求有多少种方式走到右下角。
B:输出所有走到右下角的路径。
那么哪道题可以用动态规划来解呢?
答案是A。
动态规划一般求出来的是最优解或者是方式数。
动态规划题目的特点
1.计数
-有多少种方式走到右下角
-有多少种方法选出k个数使得和是sum
2.求最大最小值
-从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3.求存在性
-取石子游戏,先手是否必胜
-能不能选出k个数使得和是sum
接下来给出具体的几道例题的解题方案
第一题:Coin Change(最值型)
你有三种硬币,分别面值是2元,5元和7元,每种硬币都有足够多,卖一本书需要27元,如何用最少的硬币组合正好付清,不需要对方找钱。
通过最少两个字可以知道这是一道求最大最小动态规划。
接下来是解题步骤
一:确定状态
简单的说,解决动态规划的时候需要开一个数组,数组的每个元素f[i]或"f[i,j]代表什么
-
类似于解数学题中,X,Y,Z代表什么
-
确定状态需要两个意识(最后一步和子问题)
-
虽然我们不知道最优的策略是什么,但是最优的策略肯定是K枚硬币a1,a2,…ak,面值加起来是27
-
所以一定有一枚最后的硬币ak
-
除掉这枚银币,前面的硬币面值加起来是27-ak
关键点1:我们不关心前面K-1枚硬币是怎么拼出27-ak的(可能有一种拼法,可能有100种拼法),而且我们现在甚至不知道ak和K,但我们确定前面的硬币拼出了27-ak。
关键点2:因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则这就不是最优策略了。 -
所以我们就要求:最少用多少枚硬币可以拼出27-ak
-
原问题是最少用多少枚硬币拼出27
-
我们将问题一样规模变小的问题叫做子问题,因此我们将原问题转换成了一个子问题,而且规模更小:27-ak
-
为了简化定义,我们设状态f(X)=最少用多少枚硬币拼出X
-
现在我们还不知道最后的那枚硬币ak面值是多少
-
最后那枚硬币只可能是2,5, 7
-
如果ak是2,f(27)应该是f(27-2)+1
-
如果ak是5,f(27)应该是f(27-5)+1
-
如果ak是7,f(27)应该是f(27-7)+1
-
除此之外没有其他可能
-
需要求最少的硬币数,所以:
-
f(27) = min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
上面的式子是可以递归计算的,但是会进行大量的重复计算,效率低下
因此,我们可以将结果保存下来,并改变计算顺序。
二:转移方程
- 设状态f[X] = 最少用多少枚硬币拼出X
- 对于任意X
- f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]+1}
三:初始条件和边界情况
- f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]}
- 两个问题:X-2,X-5或者X-7小于0怎么办?什么时候停下来?
- 如果不能拼出Y,就定义f[Y] = 正无穷,例如:f[-1]=f[-2]=…=正无穷
- 所以f[1] = min{f[-1]+1,f[-4]+1,f[-6]+1} = 正无穷,表示拼不出来1
- 初始条件f[0] = 0(就是用转移方程算不出来的,需要手工定义)
- 边界条件就是不要数组越界
四:计算顺序
- 初始条件:f[0] = 0
- 然后计算f[1],f[2],…,f[27]
- (一般一维的计算顺序都是由小到大,二维是从左向右,从上向下)
- 顺序原则其实很简单,就是当我们计算f[X]时,f[X-2],f[X-5],f[X-7]都已经得到结果了
public class Solution {
/**
* @param coins: a list of integer
* @param amount: a total amount of money amount
* @return: the fewest number of coins that you need to make up
*/
public int coinChange(int[] coins, int amount) {
// write your code here
// 定义动态规划需要的数组
int[] f = new int[amount+1];
// 特殊值
f[0] = 0;
for(int i=1;i<=amount;i++){
f[i] = Integer.MAX_VALUE;
for(int j=0;j<coins.length;j++){
if( i-coins[j]>=0 && f[i-coins[j]]!=Integer.MAX_VALUE){
f[i] = Math.min(f[i-coins[j]]+1,f[i]);
}
}
}
if(f[amount]==Integer.MAX_VALUE){
f[amount] = -1;
}
return f[amount];
}
}
第二题 不同的路径(计数型)
有一个机器人的位于一个 m × n 个网格左上角。
机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。
问有多少条不同的路径?
解题步骤
最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一步:向右或者向下
- 右下角坐标设为(m-1,n-1)
- 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)
- 那么,如果机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)
- 问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)
- 所以找到子问题了
状态:设f[i,j]为机器人有多少种方式从左上角走到(i,j)
所以对于任意一个格子(i,j):
f[i][j] = f[i-1][j] + f[i][j-1]
初始条件和边界情况:
- 初始条件:f[0,0] = 1,因为机器人只有一种方式到左上角
- 边界情况:i=0或j=0,则前一步只能有一个方向过来f[i,j] = 1
计算顺序 - 从0行到m-1 最后答案f[m-1,n-1]
public class Solution {
/**
* @param m: positive integer (1 <= m <= 100)
* @param n: positive integer (1 <= n <= 100)
* @return: An integer
*/
public int uniquePaths(int m, int n) {
// write your code here
int f[][] = new int[m][n];
f[0][0] = 0;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(i==0||j==0){
f[i][j] = 1;
}else{
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
}
return f[m-1][n-1];
}
}
第三题 Jump Game
给出一个非负整数数组,你最初定位在数组的第一个位置。
数组中的每个元素代表你在那个位置可以跳跃的最大长度。
判断你是否能到达数组的最后一个位置。
确定状态
最后一步:如果青蛙能跳到最后一块石头n-1,我们考虑它跳的最后一步
- 这一步是从石头i跳过来的,i<n-1
- 这需要两个条件同时满足:
1.青蛙可以跳到石头i
2.最后一步不超过跳跃的最大距离:n-1-i<=ai
**状态:**设f[j]表示青蛙能不能跳到石头j
初始条件和边界情况
初始条件:f[0] = True,因为青蛙一开始就在石头0
计算顺序:
从左到右,从小到大
最后返回f[n-1]
public class Solution {
/**
* @param A: A list of integers
* @return: A boolean
*/
public boolean canJump(int[] A) {
// write your code here
boolean[] f = new boolean[A.length];
f[0] = true;
for(int j=1;j<A.length;j++){
for(int i=0;i<j;i++){
if(f[i]==true && i+A[i]>=j){
f[j] = true;
}
}
}
return f[A.length-1];
}
}