动态规划法(dynamic programming),这里的programming不是编程的意思,而是一种表格法
不同于前面的分治法,是把问题分成若干互不相交的小问题,递归的解决这些问题,然后合并解决大问题,当各个小问题是重叠的时,分治法会做很多重复的计算,会反复求解公共子问题,而动态规划会把公共子问题只求解一次,保存在表格(数组)里,从而避免了重复计算。
动态规划通常会求解最优化问题,最优解可能不唯一
通常按以下步骤设计一个动态规划算法:
1、刻画一个最优解的结构特征
2、递归的定义最优解的值
3、计算最优解的值,通常采用自底向上的方法
4、利用计算出的信息构造一个最优解
从一个简单的斐波那契数列说起:
1,1,2,3,5,8,13….求第n个数
我们都知道计算起来就是前两个相加得到第三个,最初接触用递归解决
Re_resolve*(int n)
{
if(n==1)
return 1;
else if(n==2)
return 1;
else
return Re_resolve(n-1)+Re_resolve(n-2);
}
计算第三个和第四个的时候,都要重新去递归到1,2为止,所以n稍大一点就会计算的非常慢
如果改成这样:
int f[1000001];
f[1] = 1;
f[2] = 1;
for(int i = 3; i<=1000000;i++)
f[i] = f[i-1]+f[i-2];
在main函数里输入一个n 对应返回f[n],就不需要重复的去计算前n-1个数值,因为数组里已经有了,所以这会快很多。这就是一种动态规划的思想
钢条切割问题
问题描述:
某公司购买钢条将其切割成锻钢条出售,切割过程本身没有支出,而钢条切割的长度不同价格不同,比如:
长度i 1 2 3 4 5 6 7 8 9 10
价格pi 1 5 8 9 10 17 17 20 24 30
给定一段长度为n的钢条和价格表,求使收益最大的钢条切割方案
分析: 这是一个最优化问题,对于一般的n 用rn表示最大收益, rn = max{pn, r1+rn-1,r2+rn-2,…..rn-1+r1}; pn表示不切割的时候,其他表示切成两段,分别长i 和n-i,接着求解这两段的最优切割收益
这里为了求得原问题,先求形式完全一样但规模更小的子问题,即完成首次切割后,将切割出的两部分当做两个独立的实例,在组合子问题的最优解,在所有可能的两段切割方案中选取组合收益最大的,构成原问题的最优解。
所以钢条切割问题有最优子结构性质:
问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解
钢条切割问题还存在一种思路上更为简单的递归方式:
将钢条从左边切割下长度为i 的一段,只对右边的n-i长度的进行递归切割,所以rn = max(pi + rn-i); (左边一段的加上右边的最大收益)这样最优解只包含一个子问题
自顶向下递归实现:
CUT_Rod(p[],n)//价格表和长度
{
if(n==0)
return 0;
q = -100000000;
for(int i = 1;i<=n;i++)
q = max(q, p[i]+ CUT_Rod(p,n-i));
return q;
}
但是同样的,当n比较大时,时间会变得非常长
现在展示如何将CUT_Rod转化成更高效的dp算法
朴素递归算法之所以效率低是因为反复求解相同的问题,所以动态规划会仔细安排求解顺序,对每个子问题只求解一次并将结果保存下来(就像斐波那契数列的数组求解一样)
动态规划有两种等价的实现方法,第一种方法为带备忘的自顶向下法
此方法仍按自然的递归形式编写代码,但过程中会保存每个子问题的解(通常保存在数组或者散列表中),当需要一个子问题的解时,首先检查是否已经保存过此解,如果是,则直接返回保存的值,否则按常规方式计算
第二种方法是自底向上法,这种方法一般要恰当定义子问题的规模,使得任何子问题的规模都仅依赖于更小的子问题。因而我们可以将子问题按规模排序,从小到大的实现(就像斐波那契的非递归算法),当求解某个子问题时,其所依赖的更小的问题都已经解决
两种算法具有相近的时间开销,因为没有递归的开销所以自底向上方法的时间复杂性函数通常有更小的常数系数
自顶向下的钢条切割:
Memorized_cut_rod(p[], n)
{
int *r = new int[n+2];
for(int i = 0;i<=n;i++)
{
r[i] = -1000000;
}
return Memorized_cut_rod_aux(p,n,r);
}
//最初的CUT_Rod引入备忘机制
Memorized_cut_rod_aux(p,n,r)
{
if(r[n]>=0)
return r[n];//所需值已知
if(n==0)
q = 0;
else
{q = -10000000;
for(int i = 1;i<=n;i++)
q = max(q, p[i]+Memorized_cut_rod_aux(p,n-i,r));
}
r[n] = q;
return q;
}
自底向上版:
Bottom_Up_Cut_Rod(p[],n)
{
int *r = new int[n+2];
r[0]= 0;
for(int j= 1;j<=n;j++)
{
q = -1000;
for(int i = 1;i<=j;i++)
q = max(q, p[i]+r[j-i]);//这一小段的最优方案
r[j] = q;
}
return r[n];
}
重构解
前文给出最优解的值,并没有给出最优解的方案,所以要进行一些改变,扩展之前的动态规划算法,使每个字问题不仅保存子问题的最优值,还保存对应的切割方案,用数组s[]记录切割方案, 伪代码如下
Bottom_Up_Cut_Rod(p[],n)
{
int *r = new int[n+2];
int * s = new int[n+2];
r[0]= 0;
for(int j= 1;j<=n;j++)
{
q = -1000;
for(int i = 1;i<=j;i++)
{
if(q<p[i]+r[j-i]) { q = p[i]+r[j-i]; s[j] = i; } } r[j] = q; } return r[n]; } Print_Cut_Rod(p[], int n) { (r,s) = Bottom_Up_Cut_Rod(p[],n); while(n>0)
{
print s[n];
n = n-s[n];
}
}