“如果你想的算法是正确的,那么它一定能够被证明,不然就是直觉”
什么是动态规划(DP)
谈起动态规划(DP),我们首先要了解什么是DP
动态规划的一个思想就是,用空间换时间,既然我在转移的过程中,把某个点(i,j)的dp(i,j)给求了出来,那么为了避免下次其他点转移到这个点又要重复工作,干脆就把已经求出来的dp(i,j)的值记录下来,那么下次再遇到的时候,直接查表。这样,可以保证每个点(i,j)都只求一次dp(i,j)。
因此动态规划又叫做填表法,就是说动态规划就是个填表游戏。
我认为动态规划 可以理解为记忆化搜索
可以参考
1、自底向上:思想是逆向的,但也能正向解答。两者是相同的,只是求解顺序不一样。
2、状态转移方程:对于这个,我只能说,暴力怎么解,动态规划就怎么解。因为求解动态规划的顺序是先暴力递归——带备忘录的递归(即先记录一些已经求出来的值,没必要每次都重新求)——动态规划。并且看博客多了的人会发现,其实递归的递归体就是动态规划的状态转移方程。不同的思考,得出的状态转移方程也不一样。
3、最优子问题:大问题分成小问题,小问题寻找最优解构成大问题的最优解。这一点不必太在意,因为求解的过程就是在求解小问题的最优解。
上面这些话,我已经听过加看过几十遍了,可是见到动态规划系列的题目还是要么没思路,要么直接暴力,重新学习记录一下
最后学习动态规划,光靠理论是不行的,得结合实战
动态规划题目的特点
1.计数:
有多少种方式走到右下角
有多少种方法选出k个数使得和是Sum
2.求最大最小值
从左上角走到右下角路径的最大数字和
最长上升序列长度
3.求存在性
取石子游戏,先手是否必胜,求存在性
能不能选出k个数使得和是Sum
例1
你有三种硬币,分别面值2元,5元,7元,每种硬币足够多 买一本书27元 ,如何用最少的硬币组合正好付清,不需要对方找钱
最少揭示了可以使用DP
DP组成部分 1
确定状态
状态是什么?
状态在动态规划中的作用属于定海神针
简单的说,解动态规划的是否需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么
类似于数学方程中的的x,y,z代表什么。
确定状态需要两个意识
:最后一步
ak最优,27-ak也要最优
:子问题转变成
· 等等,我们还不知道最后那枚硬币ak是多少
· 最后那枚硬币ak只可能是2,5或者7
· 如果ak是2,f(27)应该是f(27-2)+1(加上最后这一枚硬币2)
· 如果ak是5,f(27)应该是(27-5)+1(加上最后这一枚硬币5)
· 如果ak是7,f(27)应该是f(27-7)+1(加上最后这一枚硬币7)
· 除此以外,没有其他的可能了
硬币这道题递归也可以解
核心代码如下:
int f(int x)
{ //f(x)=最少用多少枚硬币拼出x
if(x==0)
{
return 0; //0元只需要0枚硬币
}
int res = MAX_VALUE; //初始化用无穷大
if(x>=2) //最后一枚硬币是2元
{
res=Math.min(f(x-1) + 1,res);
}
if(x>=5) //最后一枚硬币是5元
{
res=Math.min(f(x-1) + 1,res);
}
if(x>=7) //最后一枚硬币是7元
{
res= Math.min(f(x-7)+1,res);
}
return res;
}
但是递归过程中会计算大量重复计算过的值,它并不会记忆,因此很可能造成超时问题。
DP组成部分 2 转移方程
设状态f【x】=最少用多少枚硬币拼出x
对于任意的x,
DP组成部分 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。。。正无穷加1还是正无穷
初始条件:
f[0]=0;有些题目会定义多个f[0],f[1]… 初始条件就是用转移方程算不出来,但需要手动定义。很多题目都是初始条件f[0]=0,这道题也是。
边界情况:
不要数组越界(下或上)
DP组成部分 4 计算顺序
大多数一维从小到大,大多数二维就会从左到右,从上到下
初始条件:f[0]=0
然后计算f[1],f[2],…f[27]
确定顺序方法:当我们计算到f[x]时,f[x-2],f[x-5],f[x-7]都已经得到结果了,因此需要用循环来保证
每一步尝试三种硬币,一共27步
与递归算法相比,没有任何重复计算
算法时间复杂度(即需要进行的步数):27 * 3