一.动态规划的基本思想
动态规划(Dynamic Programming,简称DP),虽然抽象后进行求解的思路并不复杂,但具体的形式千差万别,找出问题的子结构以及通过子结构重新构造最优解的过程很难统一。
在做这些题之前,没有接触过任何关于动态规划的概念,所以写的很吃力,很难过
下去了解了一些关于动态规划的概念,在此做个记录和总结
动态规划的基本模型:
- 确定问题的决策对象
- 对决策问题划分阶段
- 对各个阶段确定状态变量
- 根据状态变量来确定费用函数和目标函数
- 确定状态转移方程
动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。并且保存子问题的解
动态规划是一种牺牲了空间,换取时间的算法.
如何正确的理解动态规划算法?
A * "1+1+1+1+1+1+1+1 =?" *
A : "上面等式的值是多少"
B : *计算* "8!"
A *在上面等式的左边写上 "1+" *
A : "此时等式的值为多少"
B : *quickly* "9!"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"
动态规划算法是在记住每一步的最优解,从每一个子问题的最优解来推出整个问题的最优解
动态规划 备忘录法:
动态规划的一种变形,使用自顶向下的策略,更像递归算法。
- 初始化时表中填入一个特殊值表示待填入,当递归算法第一次遇到一个子问题时,计算并填表;以后每次遇到时只需返回以前填入的值。
动态规划 自底向上法:
- 这种方法一般需要恰当的定义子问题的”规模”概念,使得任何子问题的求解都执行依靠“更小的”子问题来解决,每个子问题只需要解决一次,在求解子问题的时候,应该确保你之前的更小的子问题已经求解成功
采用动态规划求解的问题需要具有两个特性:
最优子结构(Optimal Substructure):问题的一个最优解中所包含的子问题的解也是最优的。
重叠子问题(Overlapping Subproblems):用递归算法对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。
问题具有最优子结构性质,我们才能写出最优解的递归方程;具有重叠子问题特性,我们才能通过避免重复计算来减少运行时间。
适合用动态规划方法求解的最优化问题应该具备的第二个性质是子问题空间必须足够的”小”,即问题的递归算法会反复地求绝相同的子问题,而不是一直生成新的子问题,一般来,如果可以用递归的方式求解相同的子问题,就称最优化问题具有重叠子问题.
综上所述,动态规划的关键是 —— 记忆,空间换时间,不重复求解,从较小问题解逐步决策,构造较大问题的解。
举一个例子:求斐波拉契数列
Fibonacci (n) =1; n=0
Fibonaci (n)=1 ; n=1;
Fibonacci(n)=Fibonacci(n-1)+Fibonacci(n-2);
大家都知道这个数列用递归来求解是十分方便的,在这里先使用递归解法
int fib(int n)
{
if(n<=0)
return 0;
if(n == 1)
return 1;
return fib(n-1)+fib(n-2);
}
这种做法虽然很简单,但是它所使用的时间复杂度是指数的
为什么会这样?
在这个递归树中,可以清楚的看到很多节点被多次反复的执行,所以在时间上的开销是很大的,而动态规划为我们提供了一种新的方式,即把执行过的节点保存在表中,如果后面要实行的时候,查表就可以,
①自顶向下的备忘录法
备忘录法的概念在前面由所提及,在递归的过程中保存每个子问题的解,需要子问题的解,就检查有没有保存过这个解,直接返回
int Fibonacci(int n)
{
if(n<=0)
return ;
int Memo[n+1];
for(int i == 0;i <=n;i++)
{
Memo[i]=-1;
}
return fib(n,Memo);
}
int fib(int n,int Memo[])
{
if(Memo[n]!=-1)
return Memo[n];
//超出范围直接返回不然就保存
if(n<=2)
Memo[n]=1;
else
Memo[n]=fib(n-1,Memo)+fib(n-2,Memo);
return Memo[n];
}
②自底向上的动态规划
带备忘的方法还是使用了递归,在这里我们可以换种想法,先计算子问题,再计算大问题
int fib(int n)
{
if(n<=0)
return n;
int Memo[n+1];
Memo[0]=0;
Memo[1]=1;
for(i=2;i<=n;i++)
{
Memo[i]=Memo[i-1]+Memo[i-2];
}
return Memo[n];
}
使用这种方式也是利用数组现保存计算的值,通过观察参与循环的只有i,i-1,i-2项,因此该方法的空间也可以进一步的