本文根据B站up九章算法,视频【动态规划专题班】ACM总冠军、清华+斯坦福大神带你入门动态规划算法。
先看一个例题,本文围绕例题来讲解(LintCode第669题)
动态规划组成部分:
部分1:确定状态
一般来说,做动态规划问题时一般需要开一个数组,状态就是数组的每个元素a[i]或a[i][j]代表什么。
确定状态需要两个意识:
(1). 最后一步
(2). 子问题
但是我们不知道最后一枚硬币具体的值,而问题有需要返回最优解,所以
f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
此时时使用递归也可以得出结果:
#include<bits/stdc++.h>
#include<algorithm>
#include<math.h>
using namespace std;
int f(int x);
int main(){
int ans=f(27);
cout<<ans;
}
int f(int x){
if(x==0)return 0;
int ans=1000000;
if(x>=2)
ans=min(f(x-2)+1,ans);
if(x>=5)
ans=min(f(x-5)+1,ans);
if(x>=7)
ans=min(f(x-7)+1,ans);
return ans;
}
但是递归做了很多重复的计算,效率低下:
而动态规划则是 将计算结果保存下来并改变计算顺序
部分2:转移方程
注意:再写转移方程时,子问题和最后一步在第一步确定状态时都已经做出
在写转移方程时
部分3:初始条件和边界情况
(1)初始条件
初始条件既人为定义那些用转移方程算不出来的结果
在例题中比如 f[0] :如果凑0元则需要0个硬币,所以给定初始条件:f[0]=0;
此后的每一项都能推出,比如f[2]=f[2-2]+1=1;
注意:
初始条件类似于递归最深的一层,需要有确定的数,之前的每一层才能都有返回值
(1)边界情况
边界情况即预防数组越界等情况。
在例题中,比如求凑-2元需要多少个硬币,此时我们让f[-2]返回正无穷大。
部分4:计算顺序
确定计算顺序只有一个原则:
那就是在计算到某一项时,他所需要的之前项都已经计算出来了
大部分都是从小到大。
例题中,拼出x元的硬币:f[x]=min{f[x-2]+1,f[x-5]+1,f[x-7]+1}
初始条件:f(0)=0
然后计算f[1],f[2],f[3],f[4]…f[27]
在我们计算f[x]时,他所需要的f[x-2],f[x-5],f[x-7]都已经计算出来了
不难看出,动态规划做此例题时间复杂度比递归要小的多。
- 每一步尝试三种硬币,一共27步。
- 与递归相比没有任何重复计算。
- 算法的时间复杂度(既需要进行的步数):273(如果计算n元、m种硬币,则时间复杂度为n,
小结
例题题解代码:
#include<bits/stdc++.h>
#include<algorithm>
#include<math.h>
using namespace std;
int f[28]={0};
int a[3]={2,5,7};
int dp(int x);
int main(){
int ans=dp(27);
cout<<ans;
}
int dp(int x){
f[0]=0;//初始条件
for(int i=1;i<=x;i++){
f[i]=10000000;//初始化数组
for(int j=0;j<3;j++){//最后一步
if(i>=a[j]&&f[i-a[j]]!=10000000)
//这一步判断是为了防止
//1:要拼的钱数小于硬币的最大面值
//2;剩下的硬币数无法拼成,比如27元可以用22+一个5来拼成,但如果22拼不出来,则需要跳过这种情况
f[i]=min(f[i-a[j]]+1,f[i]);
}
}
if(f[x]==10000000){
f[x]=-1;
}
return f[x];
}