目录
0. 参考
本博客是看了视频后做的入门笔记,强烈建议去看一下,1个多小时就能入门。
1. 常见动态规划题目类型
- 计数:
- 有多少种方式走到右下角
- 有多少种方法选出k个数使得和为Sum
- 求最大最小值:
- 从左上角走到右下角路径的最大数字和
- 最长上升子序列长度
- 求存在性:
- 取石子游戏,先手是否必胜
- 能不能选出k个数使得和是Sum
2. 动态规划解题步骤
- 确定状态
- 状态在动态规划中的作用属于定海神针;
- 简单的说,就是解动态规划时需要开一个数组,数组的每个元素
f[i]
或者f[i][j]
代表什么,类似解数学题中,xyz代表什么一样,具体分为下面两个步骤:- 研究最优策略的最后一步;
- 化为子问题。
- 转移方程
- 根据子问题定义直接得到;
- 初始条件和边界情况
- 初始条件一般都是
f[0]
、f[1]
这种,初始条件是无法根据转移方程确定的点; - 边界条件主要是看数组的边界,数组越不越界。
- 计算顺序
- 利用之前的计算结果
3. 动态规划实例
3.1 硬币问题
3.1.1 问题
题目:有三种硬币,面值分别为2、5、7,每种硬币有无限个,买一本书需要27元,如何用最少的硬币整好付清。
3.1.2 问题分析
根据2中解题步骤来分析。这是一个求最大最小值问题,考虑用动态规划来求解。
- 确定状态
- 最后一步。
假设已经有最优策略,即有k枚硬币面值分别为 a 1 , a 2 , . . . , a k a_1,a_2,...,a_k a1,a2,...,ak,其面值和为27,即 a 1 + a 2 + . . . , + a k = 27 a_1+a_2+...,+a_k=27 a1+a2+...,+ak=27。所以一定存在最后的硬币 a k a_k ak,除去这枚硬币,前面所有的硬币面值加起来是 27 − a k 27-a_k 27−ak,即 a 1 + a 2 + . . . , + a k − 1 = 27 − a k a_1+a_2+...,+a_{k-1}=27-a_k a1+a2+...,+ak−1=27−ak,如下图所示。
又因为是最优策略,所以拼出的 27 − a k 27-a_k 27−ak的硬币数也是最少的,否则这就不是最优策略了。
- 所以得到子问题:最少用多少枚硬币可以拼出总面值为 27 − a k 27-a_k 27−ak的序列。注:原问题是:最少用多少枚硬币可以拼出总面值为27的序列。所以我们可以提取标黄部分为状态,即设状态 f [ x ] = f[x]= f[x]=最少用多少枚硬币拼出 x x x,我们的总目标是求 f [ 27 ] f[27] f[27]。
- 子问题:最后一枚硬币 a k a_k ak面值只可能为2、5、7,如果 a k = 2 a_k=2 ak=2,则 f [ 27 ] = f [ 27 − 2 ] + 1 f[27]=f[27-2]+1 f[27]=f[27−2]+1;如果 a k = 5 a_k=5 ak=5,则 f [ 27 ] = f [ 27 − 5 ] + 1 f[27]=f[27-5]+1 f[27]=f[27−5]+1;如果 a k = 2 a_k=2 ak=2,则 f [ 27 ] = f [ 27 − 7 ] + 1 f[27]=f[27-7]+1 f[27]=f[27−7]+1;所以, f [ 27 ] = m i n { f [ 27 − 2 ] + 1 , f [ 27 − 5 ] + 1 , f [ 27 − 7 ] + 1 } f[27]=min\{f[27-2]+1,f[27-5]+1,f[27-7]+1\} f[27]=min{f[27−2]+1,f[27−5]+1,f[27−7]+1}。
- 转移方程
- 初始条件和边界条件
总目标 f [ x ] = m i n { 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]+1\} f[x]=min{f[x−2]+1,f[x−5]+1,f[x−7]+1}
- 边界条件::x-2,x-5,x-7小于0怎么办,小于0的时候必须得停下来,因为不存在小于0的面值,所以令 f [ k ] = + ∞ , k < 0 f[k]=+∞,k<0 f[k]=+∞,k<0,比如 f [ 1 ] = m i n { f [ − 1 ] + 1 , f [ − 4 ] + 1 , f [ − 6 ] + 1 } = + ∞ f[1]=min\{f[-1]+1,f[-4]+1,f[-6]+1\}=+∞ f[1]=min{f[−1]+1,f[−4]+1,f[−6]+1}=+∞;
- 初始条件: f [ 0 ] = 0 f[0]=0 f[0]=0,f[0]没法根据上面的式子推导出来,但我们可以根据题目推出只要0个硬币就可以拼出总面值为0的序列。
- 计算顺序
本题是正序的,因为当我们要计算f[X]时,f[X-2],f[X-5],f[X-7]必须都已知。
3.1.3 代码
public class coinDynamic {
// A存储各种面值的硬币,M是目标面值之和
public static int coinChange(int[] A ,int M){
int[] f = new int[M+1];
// 初始条件
f[0] = 0;
for (int i = 0; i < M; i++) {
// 为了比较最小值,必须先初始化设置一个很大的数
f[i] = Integer.MAX_VALUE;
// 遍历所有面值的硬币
for(int j = 0;j < A.length;j++){
// 如果满足边界条件,并且不能越界,
// 如果f[i-A[j]]==Integer.MAX_VALUE,那么Integer.MAX_VALUE+1就溢出了
if (i >= A[j] && f[i-A[j]]<Integer.MAX_VALUE){
f[i] = Math.min(f[i],f[i-A[j]]+1);
}
}
}
// 若所给硬币无法组成总的面值,返回-1
if (f[M]==Integer.MAX_VALUE)
return -1;
return f[M];
}
}
3.2 机器人路径问题
3.2.1 问题
如下图所示,给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步只可以向下或者向右走一步,问有多少种不同的方式走到右下角。
3.2.2 问题分析
这是动态规划中的计数问题。
- 确定状态
- 最后一步:聚焦机器人最后挪动的一歩,右下角坐标为(m-1,n-1),那么前一步机器人一定在(m-2,n-1)或者(m-1,n-2),如下图所示。
- 子问题:由加法原理可知,机器人走到(m-1,n-1)的方式数等于机器人走到(m-2,n-1)加上机器人走到(m-1,n-2)的方式数。原问题为有多少种方式从左上角走到(m-1,n-1),子问题是有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)。
- 转移方程
- 根据1中的子问题可以得出转移方程为:
- 边界条件与初始条件
-
边界条件:由于机器人只能向下或向右移动,所以i=0或者j=0时,只能从一个方向过来,所以 f [ i ] [ j ] = 1 f[i][j]=1 f[i][j]=1;
-
初始条件:在右上角[0,0]处,机器人只有一种方式到达,即 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1.
- 计算顺序
可以按照行或者列计算,这里以行为例,先计算到达第1行每个格子的方式数,再计算第2行,以此类推,计算到最后一行时,取最后一个数就是答案,如下图所示。
3.2.3 代码实现
public static int uniquePaths(int m, int n) {
int[][] f = new int[m][n];
for (int i = 0; i < m; i++) { // 按行计算
for (int j = 0; j < n; j++) { // 扫描第i行中所有列
// 边界条件与初始条件
if (i==0 || j==0){
f[i][j] = 0;
}else {
f[i][j] = f[i-1][j] + f[i][j-1];
}
}
}
// 最后一个就是答案
return f[m-1][n-1];
3.3 青蛙跳石头问题
3.3.1 题目
有n块石头分别放在x轴的0,1,…,n-1位置,一只青蛙在石头0,想要跳到石头n-1,若青蛙在第i块石头上,他最多可以向右跳距离ai,问青蛙能否跳到石头n-1。
为什么说输入1
的输入是True呢?
初始时刻青蛙在a[0]位置,可以最多向右跳2个位置,可以跳到a[2]的位置;同理,又因为a[2]=1,此时青蛙可以跳到a[3]的位置;a[3]=1,所以可以跳到a[4]=4的位置。
而输入2
的输出就是False,青蛙无论怎么跳都跳不到a[4]=4的位置,究其原因,是因为没有一块石头能够达到a[4]=4的位置。
3.3.2 题目分析
这是一个存在型动态规划问题。
- 确定状态
- 最后一步:青蛙从第i块石头跳到第n-1块(最后一块)石头上,i<(n-1),假设这两块石头之间距离为
a
i
a_i
ai,所以必须满足
d
i
s
t
(
i
,
n
−
1
)
<
=
a
i
dist(i,n-1)<=a_i
dist(i,n−1)<=ai;
- 化为子问题:青蛙能够从石头i跳到石头n-1(最后一块)上,青蛙能够跳到石头i,……
- 转移方程
- 边界条件与初始条件
-
边界条件:枚举的i和j都不会越界,所以没有边界条件
-
初始条件:f[0]=true,因为青蛙一开始就在石头0上
- 计算顺序
从头开始计算,不能倒着计算,倒着也不方便算。
3.3.3 代码分析
public class canJump {
public static boolean canJump(int[] A) {
int size = A.length;
boolean[] f = new boolean[size];
f[0] = true;
for (int j = 1; j < size; j++) {
f[j] = false; // 默认应该是跳不过去
for (int i = 0; i < size; i++) {
f[j] = f[i] && (i + A[i] >= j);
}
}
return f[size-1];
}
}