动态规划解题步骤
题目的类型
1.计数型:
1.1多少种方式走到右下角
1.2多少种方式选出K个数字使得和为sum
2.求最大最小值:
2.1从左上角走到右下角的最大数字和
2.2最长上升子序列长度
3.求存在性(博弈)
3.1取石子,先手是否能赢
3.2能不能选出k个数使得和为sum
解题四个步骤:
例题:三种硬币分别为2元,5元和7元,每种硬币足够多,买一本书27块钱,最少需要几个硬币能付清?
动态规划组成部分1:确定状态:
状态在动态规划里面非常的重要,简单的来说就是解动态规划的时候需要开一个数组,数组的每个元素a[i]或者a[i][j]分别代表什么。
1.1确定状态两个意识:
1.1.1最后一步
虽然我们不知道最优策略是什么,但是最优策略坑定是K枚硬币a1,a2到ak的面值加起来为27
所以一定有最后一个硬币ak,除掉了这一个硬币,前面的面值就是27-ak;
关键点1:
我们不去关系前k-1枚硬币如何拼出来,甚至我们不知道ak和k的数值,但是我们知道前面的硬币可以拼出27-ak
关键点2:
因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则就不是最优策略
1.2子问题
原问题是用最少的硬币拼出27,所以子问题就是:用最少的硬币可以拼出27-ak,和原问题比,子问题的规模更小。
为了简化定义,所以我们就设状态f(x)=最少用多少枚硬币拼出x。
等等,我们还不知道最后的那枚硬币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}
那么和递归有什么区别?
重复计算,时间复杂度非常高。
那么动态规划是怎么避免的:将计算结果保存下来,并改变计算顺序,也就是空间换时间。可以理解为记忆化搜索。
动态规划组成部分2:转移方程:
上面分析了f[x]=最少多少枚硬币可以拼出x
对于任意x,f[x]= min{f[x-2]+1, f[x-5]+1, f[x-7]+1}
动态规划组成部分3:初始条件和边界情况:
问题一:
x-2,x-7,x-5小于0怎么办?
如果拼不出来x,我们就定义f[x]是正无穷,例如f[-1],f[-2]都等于正无穷
所以f[1]=min{f[-1]+1,f[-4]+1,f[-6]+1}=正无穷,表示拼不出来1
问题二:
什么时候停下来?
初始条件f[0]=0,这个必须手动定义,为什么?
假如我们不定义f[0]=0;那么使用转移方程后将会出现:f[0]=min{f[-2]+1,f[-5]+1,f[-7]+1}=正无穷。
动态规划组成部分4:计算顺序:
大多数动态规划都是从0开始,二维的话就是从左到右从上到下来确定。
计算顺序的确定其实很简单:
当我们计算到f[x]的时候,f[x-2]、f[x-5]、f[x-7]都已经得到结果了,所以这里应该从小到大。
这道题我们从0到27,每个位置尝试三种硬币,所以时间复杂度就是273;如果到n,有m种硬币,那么时间复杂度就是mn;远远小于递归。
接下来就是把四个步骤转化为代码:一定要自己憋出来,哪怕是憋几天都别看答案,憋出来了会好很多。
思想很重要但是把思想转化成代码的能力同样重要
上代码
#include <iostream>
#define max 999
using namespace std;
int main()
{
int a[28]={0};
int b[3]={2,5,7};
for(int i=0;i<=27;i++)
{
a[0]=0;
int min=999;
for(int j=0;j<=2;j++)
{
if(i-b[j]<0&&i!=0)
{
a[i]=max;
}
else
{
if(a[i-b[j]]+1<min)
min=a[i-b[j]]+1;
}
}
a[i]=min;
cout<<i<<" "<<a[i]<<endl;
}
}
当然,肯定有更简洁的写法,但我这个写法可读性可能会好一些。