动态规划算法,可以说已经是各大互联网公司笔试、面试必考的算法之一了,为了抓住金九银十,学习要争分夺秒......
可是如何分辨题目是否可以使用动态规划进行解题呢?别急,动态规划题目的特点可以大致分为以下三类:
1.计数
-有多少种方式走到右下角,比如机器人寻路等
-有多少种方法选出k个数使得和是sum
2.求最大最小值
-从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3.求存在性
-取石子游戏,先手是否必胜
-能不能选出k个数使得和是sum
什么是动态规划?
接下来通过例题进行进一步的分析。
题目:你有三种硬币,分别面值2元,5元和7元,每种硬币都足够多。买一本书需要27元,如何用最少的硬币组合正好付清,不需要对方找钱。
直觉:直观分析,用最少的硬币组合—>尽量用面值大的硬币,那么
- 7+7+7=21
- 21+5=26
- emmmmm....
貌似不行,再来,换策略
改算法:尽量用最大的硬币,最后如果可以使用一种硬币付清就行
- 7+7+7=21
- 21+2+2+2=27
- 6枚硬币,应该对了吧...
可是答案居然是,居然是
正确答案:7+5+5+5+5=27,5枚硬币
哎,老老实实根据算法,按部就班来做题吧,不YY了。
首先第一步:动态规划组成部分——确定状态
- 状态在动态规划中的作用属于定海神针
- 简答的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似于解数学题中,X,Y,Z代表什么
- 确定状态需要两个意识:最后一步;子问题
最后一步
- 虽然我们不知道最优策略是什么,但是最优策略肯定是k枚硬币a1,a2,a3,...,ak,面值加起来是27
- 所以一定有一枚最后的硬币:ak
- 除掉这枚硬币,前面硬币的面值加起来是27-ak
关键点1
我们不关心前面的k-1枚硬币是如何拼出27-ak的(可能有1种拼法,可能有100种拼法),而且我们现在甚至还不知道ak和k,但是我们可以确定前面的硬币拼出了27-ak。
关键点2
因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则这就不是最优策略了。
子问题
- 所以我们就要求:最少用多少枚硬币可以拼出27-ak
- 原问题是最少用多少枚硬币拼出27
- 我们将原问题转化成了一个子问题,而且规模更小:27-ak
- 为了简化定义,我们设状态f(X)=最少用多少枚硬币拼出X
但是,等等,我们还不知道最后那枚硬币ak是多少
由于题目中给出了三种币值,那么最后那枚硬币ak只可能是2,5或7
- 如果ak是2,f(27)应该是f(27-2)+1(加上最后这一枚硬币2)
- 如果ak是5,f(27)应该是f(27-5)+1(加上最后这一枚硬币5)
- 如果ak是7,f(27)应该是f(27-7)+1(加上最后这一枚硬币7)
除此之外,没有其他可能了
因此需要求最少的硬币数,所以不难推出以下关系式
f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
根据上面的推导,我们很容易写出递归的程序,如下
public static void main(String[] args) {
System.out.println(getMinCoinCount(27));//5
}
//写一个方法,获取购买X元书需要的最少硬币
public static int getMinCoinCount(int X) {
if (X == 0) return 0;//递归的出口
int res = Integer.MAX_VALUE;//将初始值设置为最大值
//如果ak为2
if (X >= 2 && getMinCoinCount(X - 2) != Integer.MAX_VALUE) {
res = Math.min(getMinCoinCount(X - 2) + 1, res);
}
//如果ak为5
if (X >= 5 && getMinCoinCount(X - 5) != Integer.MAX_VALUE) {
res = Math.min(getMinCoinCount(X - 5) + 1, res);
}
//如果ak为7
if (X >= 7 && getMinCoinCount(X - 7) != Integer.MAX_VALUE) {
res = Math.min(getMinCoinCount(X - 7) + 1, res);
}
return res;
}
虽然递归能解决问题,但并不是最好的解决方法,接下来谈谈递归解法的问题
从图中不难看出,f(20)重复计算了3次,f(15)重复计算了两次,如何解决这个问题呢?主角动态规划又站起来了,“放开那方程,让我来”。动态规划的第二步,登上历史舞台
动态规划组成部分二——转移方程
- 设状态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)+1}
- 两个问题: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
动态规划组成部分四——计算顺序
- 拼出X所需的最少硬币数:f(X) = min{f(X-2)+1,f(X-5)+1,f(X-7)+1}
- 初始条件:f[0] = 0
- 然后计算f[1],f[2],...,f[27]
- 当我们计算到f[X]时,f[X-2],f[X-5],f[X-7]都已经得到结果了
f[X]=最少用多少枚硬币拼出X
f[X]=无穷 表示无法用硬币拼出X
- 每一步尝试三种硬币,一共27步
- 与递归相比,没有任何重复计算
- 算法时间复杂度(即需要进行的步数):27*3
- 递归时间复杂度:>>27*3
小结
求最值型动态规划
动态规划的组成部分:
1.确定状态
- 最后一步(最优策略中使用的最后一枚硬币ak)
- 转化子问题(最少的硬币拼出更小的面值27-ak)
2.转移方程
- f(X) = min{f(X-2)+1,f(X-5)+1,f(X-7)+1}
3.初始条件和边界情况
- f[0] = 0,如果不能拼出Y,f[Y] = 正无穷
4.计算顺序
- f[0],f[1],f[2],...
消除冗余,加速计算
最后代码实现,完结!
public static void main(String[] args) {
int[] arr = {2, 5, 7};//币值的种类
int M = 37;//商品总价
System.out.println(getMinCoinCount(arr, M));
}
public static int getMinCoinCount(int[] arr, int M) {
int[] f = new int[M + 1];//定义动态规划的一维数组
f[0] = 0;//初始条件
for (int i = 1; i <= M; i++) {
f[i] = Integer.MAX_VALUE;//初值设置为无穷大
for (int j = 0; j < arr.length; j++) {
if (i >= arr[j] && f[i - arr[j]] != Integer.MAX_VALUE) {
f[i] = Math.min(f[i - arr[j]] + 1, f[i]);
}
}
}
if (f[M] == Integer.MAX_VALUE) {//如果拼不出,返回-1
f[M] = -1;
}
return f[M];
}
以上内容均来自B站九章算法课程。
课程地址https://www.bilibili.com/video/BV1xb411e7ww?from=search&seid=15667342454332890058