动态规划基础篇
入门案例
前一段时间在刷力扣,一开始只刷数组相关的题目,还有链表等,后来发现越来越多的算法题并不是逻辑思维能力足够就能做做出来的,依赖于一定的算法基础,比如匹配字符串,虽然暴力算法很好实现肯定有很多的纰漏。好的算法绝对不是代码行数越短越好,比如以下两个代码就是很好的例子。
问题描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
这道题一看,就会轻易的知道用递归,f(1)=1;f(2)=1;f(n)=f(n-1)+f(n-2);
public static int JumpFloor(int target) {
if(target<1) return 0;
else if(target==1)return 1;
else if(target==2) return 2;
else return JumpFloor(target-1)+JumpFloor(target-2);
}
缺点也是很明显
2071ms
我们可以简单分析以下:
由于我们每次return的结果等于前两次的结果之和,我们可以尝试画出流程图图。我们发现Jump(38),Jump(37)已经计算过了,这里又进行一次计算,而且黄色部分是省略部分,这个整体结构将异常的庞大,会出现大量的重复计算的问题,所以导致用时非常的长。
这是一道经典的面试题,我们也知道可以使用循环的方式代替递归来完成这个效果,但是这里我主要是讲一个思想。
我们知道跳楼梯的最后一步不是跳1步就是跳两步,什么意思呢?假设我们不知道楼梯有多高,我们随意进行跳跃,当我们离终点只剩下1步或者2步时,我们下一步就能跳到终点,所以说我们有多少种情况能跳到还差一步跳到终点的情况数加上有多少种情况能跳到还差两步跳到终点的情况数之和。
这里我们得到一个算式F(n)=F(n-1)+F(n-2);当然这个算式不是什么情况下都可以用的,比如F(1)=F(0)+F(-1),跳到台阶为1的高度的情况等于跳到台阶为0和-1之和,显然不合理。所以我们进行条件判断。
if(target<1) return 0;
else if(target==1)return 1;
else if(target==2) return 2;
上述问题是大量重复,我们的解决思路是,先把每一个F(n)算出来加到一个表中,然后我们使用算式时,可以从表中拿数据。
当然以下代码又更好的解决思路,由于我们只需要得到跳n级台阶的数量,所以这里我们创建一个数组存储结构有点多余,这里主要是阐述一个基本思想。
public int JumpFloor(int n) {
//边缘条件判断
if(n==0){
return 1;
}
if(n<3){
return n;
}
//保存算数结果
int[] results=new int[n+1];
//计算每一位
for(int i=0;i<n+1;i++){
if(i==0){
results[i]=1;
}
else if(i<3){
results[i]=i;
}
else{
int sum=(results[i-1]+results[i-2]);
results[i]=sum;
}
}
//直接查询表格
return results[n];
}
思想核心
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。
看不明白是吧,我们简单的概括一下:
- 动态规划是我们解决一个问题的最优解的时候,将问题拆分成多个子问题,分别找到这些子问题的最优解,最终得到整体问题最优解的一个思想
- 动态规划算法没有具体的代码模板,能够解决一些寻找最优值值,计算总数,等一些场景。
动态规划算法的重要概念:
-
状态
-
起始条件
什么是状态?
状态的意思是我们在解决一个问题的时候,所能提出的最优的解需要满足的条件。
比如F(n)=F(n-1)+F(n-2)
,本题是找出跳到n级台阶的所有情况,只有满足上述方程的情况下,我们才能得到所有的情况,如果我们的方程有纰漏,那么就是最优解了,在本题中甚至还不是个正确的解。我们称F(n)=F(n-1)+F(n-2)
这个需要我们满足的最优解的算式为状态方程,也可以叫转移方程。
什么是起始条件?
本题中的起始条件是,是对状态方程的补充,计算状态方程无法计算出的一些边缘值。
if(target<1) return 0;
else if(target==1)return 1;
else if(target==2) return 2;
基本思想讲完了,我们看一个经典案例
经典案例1
问题描述:
你有三种硬币,分别是2元,5元和7元,每个硬币都足够的多。
买一本书要27个硬币,如何一次付清不找零钱的情况下,使用最少的硬币
传统思维
- 先用大的硬币,3x7=21,再用小一号的:21+5=26,此时不满足条件,我们拿走一个5元和2元的,此时7x2=14。
- 用小一号的硬币7+7+5+2+2+2+2我们得出了一个结果。
实际上最优的结果是7+5+5+5+5,我们的规律应该有机可寻的,这里我们按照动态规划的思想来解决问题。
动态规划解决思路
- 最优解
什么是最优解,那就是用最少的硬币来解决问题。我们假设最优的解为n个硬币,此时我们知道n个硬币的和为27。
- 转换方程
动态规划算法需要我们将最优的问题拆分为子问题,通过寻找子问题的最优结果得到整体的最优解,我们分析以下当我们执行最后一步的时候,我们手里的硬币有三种情况:2,5,7。也就是说,我们在执行最后一步时,我们的硬币总数应该为27-2,27-5,或者27-7三种情况,这三种情况都是我们这问题的子问题(子情况),我们三种情况只有一种是符合条件,即硬币最少的,所以我们得到状态方程:
F(n)=min{F(n-2),F(n-5),F(n-7)}+1
- 起始条件
只要一个,那么就是0的时候我们返回0。如果是1,3这种情况怎么办呢?我们认为,如果硬币无论怎么找都找不清,那么我们认为这个算法没有解,我们的F(1)状态方程返回值可以为Integer.MAX_VALUE
正无穷,即如果F(n)算不清,则F(n)等于正无穷。如果F(27)=min{F(n-2),F(n-5),F(n-7)}+1
的结果为正无穷,那么说明三钟情况的结果都是正无穷。
代码实现
有了转换方程和起始条件,我们可以开始写代码,模板和入门案例一样
- 起始条件
- 创建数组,存储查询结果
- 查询数组得到结果
我们先给出一个初代码
/**
* 最小的硬币
*
* @param number 数量
* @return int
*/
public static int minCoins(int number){
//如果
if (number==0){
return 0;
}
//记录查询结果
int [] results=new int[number+1];
//查询
for (int i = 0; i < results.length; i++) {
//计算2和5的最小值
int min=Math.min(results[i-2],results[i-5]);
//计算上述最小值和7的最小值
results[i]=Math.min(min,results[i-7])+1;
}
return results[number];
}
我们知道当i<7
的时候,执行上述代码肯定要抛出角标越界异常,所以这里要求我们单独计算一下i<7
时的情况,我们认为i-2,i-5,i-7
如果小于0,我们给他赋值为正无穷。
我们结合图片来看一下
我们新建一个数组,长度为9,如果指向的值是数组中的空值,那么
我们看一下第0位,0-2,0-5,0-7都是负数,但是我们认为0的最少硬币为0,所以我们给第0位赋值0。
下一位1,三个min都是负数,给他赋值为无穷
以此类推,我们可以得到前7位。
从第8位开始,不会再出现角标问题,我们直接通过状态方程计算次数:
F(8)=min{F(6),F(3),F(1)}+1
=F(6)+1
=4
根据以上推论,我们得到几个规律
- 如果
min{F(n-2),F(n-5),F(n-7)}=Integer.MAX_VALUE
则不加一 - 否则结果等于
min{F(n-2),F(n-5),F(n-7)}+1
- 计算F(7),F(5),F(2)时,我们应该等于min值应该等于F(0),由于我们的0的条件以及判断过了,所以我们认为此时的F(0)情况也应该定义为
Integer.MAX_VALUE
我们更新一下代码
小于7的值我直接枚举了!
public static int minCoins(int number){
//如果
if (number==0){
return 0;
}
//记录查询结果
int [] results=new int[number+1];
results[0]=Integer.MAX_VALUE;
//查询
for (int i = 1; i < results.length; i++) {
if (i<=7){
if (i==1 || i==3){
results[i]=Integer.MAX_VALUE;
}
else if (i==2 || i==5 || i==7){
results[i]=1;
}
else {
results[i]=i/2;
}
}
else{
results[i]=Math.min(Math.min(results[i-2],results[i-5]),results[i-7]);
if (results[i]!=Integer.MAX_VALUE){
results[i]++;
}
}
}
return results[number];
}
测试代码
public static void main(String[] args) {
System.out.println(minCoins(27));
}
测试结果
5
总结
- 动态规划算法先得到最优解的满足条件,再通过状态方程得出子问题和整体的关系,通过计算所有子问题的最优解,得到整体的最优解
- 初始条件判断是用于解决状态方程无法处理的部分的