声明:本文记录自某动态规划公开课内存,并非原创!
动态规划题目特点
1.计数:
- 有多少种方式走到右下角
- 有多少种方式选出k个数使得和是Sum
2.求最大最小值
- 从左上角走到右小角路径的最大数字和
- 最长上升子序列长度
3.求存在性
- 取石子游戏,先手是否必胜
- 能不能选出K个数使得和是Sum
例:
有三种硬币,2元、5元、7元,每种硬币都有足够多;买一本书要27元;如何用最少的硬币组合正好付清,不需要对方找钱?
思路:
1.动态规划组成部分一:确定状态
- 状态在动态规划中的作用属于定海神针!!
- 简单的说,解动态规划的时候需要开一个数组,数组的每一个元素f[i]或者f[i][j]代表什么:
- 确定状态需要两个意识:
1)最后一步:
虽然不知道最优策略是什么,但是最优策略肯定是K枚硬币a1,a2,…,ak面值加起来是27;
最后一步对应的就是最优策略中的最后一个决策。所以一定有一枚最后的硬币:ak;
除掉这枚硬币,前面硬币的面值加起来是27-ak;
关键点1:我们不关心前面的K-1枚硬币是怎么拼出27-ak的(可能有1种拼法可能有100种拼法),而且我们现在甚至还不知道ak和K,但是我们确定前面的硬币拼出了27-ak.
关键点2:因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则这就不是最优策略了
2)子问题
所以我们就要求:最少用多少枚硬币可以拼出27-ak
我们将原问题转化成了一个子问题,而且规模更小:27-ak
为了简化定义,我们设状态f(x)=最少用多少硬币拼出X
问题是,到现在还不知道最后那枚硬币ak是多少!!但最后那枚硬币ak只可能是2,5或者7.
如果ak是2,f(27)=f(27-2)+1;如果ak是5,f(27)=f(27-5)+1;如果ak是7,f(27)=f(27-7)+1;
需要求最少的硬币数,所以:
f(27)=min{ f(27-2)+1 , f(27-5)+1 , f(27-7)+1 }
递归解法伪代码:
int f(int X) //f(x)=最小用多少枚硬币拼出X
{
if(X==0) //0元钱只要0枚硬币
{
return 0; //初始化用无穷大
}
int res =MAX_VALUE;
if(X>=2) //最后一枚硬币是2元
{
res =Math.min(f(X-2)+1 , res);
}
if(X>=5) //最后一枚硬币是5元
{
res =Math.min(f(X-5)+1 , res);
}
if(X>=7) //最后一枚硬币是7元
{
res =Math.min(f(X-7)+1 , res);
}
return res;
}
递归解法的问题;
效率低下:上图为递归程序的执行树,可以看出f(20)执行了3次,f(15)执行了2次….(不光是这几个点本身算了很多次,他们下面的全部计算分支也都算了很多次)
如何避免?
动态规划思想的解法是将计算结果保存下来,并改变计算顺序
2.动态规划组成部分二:转移方程
- 设状态f[X]=最少用多少枚硬币拼出X
- 对于任意X,f[X]= min{ f[x-2]+1 , f[x-5]+1 , f[x-7]+1 }
3.动态规划组成部分三:初始条件和边界情况
- 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
初始条件其实就是用转移方程算不出来的,但我们却需要它,这是就手工定义它们!
边界情况在这个案例中就是不要数组越界:不管是往下越界还是往上超过了都不行!
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[27]
- 计算顺序的确定只有一个原则:计算f[X]等式左边的时候等式右边都已经算过了!当我们计算到f[x]时,f[x-2],f[x-5],f[x-7]
- 每一步尝试三种硬币,一共27步
- 与递归算法相比,没有任何重复计算
- 算法时间复杂度(即需要进行的步数):27*3,凑n块钱共m种硬币则是n*m次计算
5.小结
·求最值型动态规划
·动态规划组成部分;
1)确定状态
最后一步(最优策略中使用的最后一枚硬币ak)
化成子问题(最少的硬币拼出更小的面值27-ak)
2)转移方程
把想法换成式子
3)初始条件和边界情况
·消除冗余,加速计算