《算法导论》中动态规划求解钢条切割问题

动态规划算法概述

动态规划(dynamic programming)1是一种与分治方法很像的方法,都是通过组合子问题的解来求解原问题。不同之处在于,动态规划用于子问题重叠的情况,比如我们学过的斐波那契数列。在斐波那契数列的求解问题中,我们经常要对一个公共子问题进行多次求解,而动态规划算法,则对每个子问题只求解一次,将其解保存在一个表格中,从而避免了大量的冗余计算量。

动态规划算法常用于寻找最优解问题(optimization problem)。而其规划大概可分为四步:

1.刻画一个最优解的结构特征。

2.递归的定义最优解的值。

3.计算最优解的值。

4.利用计算出的信息构造一个最优解2

我们将以《算法导论》中的一个习例作为展示的对象,讲解动态规划算法的应用方法。

钢条切割问题

现有某公司,购买长钢条以切割成短钢条出售。若不计切割成本,请求出如何切割以使公司利益最大。该公司短钢条售价如下:

长度:1 2 3 4 5 6 7 8 9 10

价格:1 5 8 9 10 17 20 24 30

现假设一段钢条的长度为n,我们可以试求当 n = 4时,我们能获得的最大收益。此时,我们对于第一次分割有5种选择(0,1,2,3,4),以此类推,对于n = 4的情况,我们一共有8种情形:(4),(1,3),(2,2),(3,1),(1,1,2),(1,2,1),(2,1,1),(1,1,1,1)。易算得,最高价值为(2,2)情况下取得,最高收益为10.

那么,我们如何用函数来描述这一过程呢?首先,我们可以假设第一次切割所得的第一部分钢条长度为x (属于[0,n])。则我们现在有了两根钢条,一根长为x,另一根长为 n - x。那么长为n的钢条所得利益的最优解就来自于长为x 和 n - x两段钢条最优解的和。如此划分,即可将问题逐步化简为一个个子问题,以R来表示公司所得利益,P来表示钢条单价,则有R对n的函数:

Rn = max(Px + R(n - x))。

普通递归方法实现

可以看出上述公式是一个很明显的递归函数,我们很容易就可以得到下列代码:

 1 #include<iostream>
 2 #include<vector>
 3 #include<algorithm>
 4 #define null -1
 5 using namespace std;
 6  
 7 int cut_rod(int n, vector<int> p) {
 8 if (n == 0)  return 0;
 9 int q = null;                                   
10 for (int i = 1; i <= n; i++) {
11 q = max(q , p[i-1] + cut_rod(n - i, p) );
12 }
13 return q;
14 }
15  
16 int main() {
17 cout << "输入产品各段数所对应的价格(从小到大)" << endl;
18 int n = 0;
19 vector<int> p;
20 while (cin >> n  && n != null) {                            //输入-1表示停止
21 p.push_back(n);
22 n = 0;
23 }
24 vector<int> results(p.size() + 1, null);                    //在result中创建n + 1个元素([0,n]共n+1个),并统一赋值为null
25 cout << "请输入所需切割钢材长度" << endl;
26 cin >> n;
27 cout << cut_rod(n, p);
28 //cout << memo_cut_rod(n, p, results);
29 //cout << bot_cut_rod(n, p, results);
30 return 0;
31 }

我们已在斐波那契数列的学习中证明了,这种算法的缺点是很明显的,随着递归的深入,其计算量会爆炸性的增长。易得其时间复杂度 : T = 2N

那么我们要怎么利用动态规划的方法来进行简便运算呢?方法有两种:一种称之为带备忘的自顶向下法(top-down with memoization3),另一种则是自底向上法(bottom-up method)。

带备忘的自顶向下法

此方法与正常的递归方法并无太大区别,但在过程中,每一个子问题的解都会被保存下来,在每次求解之前都会验证是否已经对该子问题进行了求解,若是,则直接返回保存的值;不是,再进行正常运算。据此理论,易得代码:

 1 int memo_cut_rod(int n, vector<int> p, vector<int> results) {
 2 if (results[n] > 0) return results[n];
 3 if (n == 0) return 0;
 4 int q = null;
 5 for (int i = 1; i <= n; i++) {
 6 q = max(q, p[i - 1] + memo_cut_rod(n - i, p,results));
 7 }
 8 return q;
 9 }
10  
11  
12 int main() {
13 cout << "输入产品各段数所对应的价格(从小到大)" << endl;
14 int n = 0;
15 vector<int> p;
16 while (cin >> n  && n != null) {                            //输入-1表示停止
17 p.push_back(n);
18 n = 0;
19 }
20 vector<int> results(p.size() + 1, null);                    //在result中创建n + 1个元素([0,n]共n+1个),并统一赋值为null
21 cout << "请输入所需切割钢材长度" << endl;
22 cin >> n;
23 //cout << cut_rod(n, p);
24 cout << memo_cut_rod(n, p, results);
25 return 0;
26 } 

自底向上法

自底向上法采用了与正常递归相似的顺序,但免除了从顶到下的过程以及冗余的计算,直接从最小问题算起,最后构成最优解。其代码如下:

int bot_cut_rod(int n,vector<int> p,vector<int> results) {
results[0] = 0;
for (int j = 1; j <= n; ++j) {
int q = null;
for (int i = 1; i <= j; ++i) {
q = max(q, p[i -1] + results[j - i]);
}
results[j] = q;
}
return results[n];
}

总结

自底向上法与带备忘的自顶向下法具有相同的渐进运行时间,两者的时间复杂度都为 T = n2。相比之前的2n强了太多。而使用动态规划算法的重中之重,是找好问题划分的方法,将问题一步一步化简到最小,把大问题化简成一个个小问题,小问题往往比大问题好解的多,最后再由小问题推导出大问题的答案,就算是大功告成了。

注释:

1.此处的programming是指一种表格法,而非编程

2.当我们仅仅需要一个最优解的值的时候,我们往往可以省略掉第4步。

3.此处并非拼写错误,确实为memoization,而非memorization。前者源自memo,为备忘之意。

参考文献:

《算法导论》

时间: 2024-12-25 12:34:20

《算法导论》中动态规划求解钢条切割问题的相关文章

算法导论读书笔记之钢条切割问题

算法导论读书笔记之钢条切割问题 巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo) 给定一段长度为n英寸的钢条和一个价格表 pi (i=1,2, -,n),求切割钢条的方案,使得销售收益rn最大.注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割. 若钢条的长度为i,则钢条的价格为Pi,如何对给定长度的钢条进行切割能得到最大收益? 长度i   1   2    3   4     5      6     7     8  

【算法导论】动态规划之“钢管切割”问题

动态规划,其实跟分治法有些相似,基本思想都是将复杂的问题分成数个简单的子问题,然后再去解决.它们的区别在于,分治法关注的子问题不相互"重叠",而动态规划关注的子问题,多是相互"重叠"的.比如在快速排序中,我们将数据分成两部分,这两部分再分别快速排序的递归思想,也就是将整个问题的排序划分为子问题子数组的排序,但是这两个子数组的排序之间并没有相互联系,a子数组的排序不会因为b子数组的排序而得到任何"好处"或者"坏处".但是有些时候

算法导论--动态规划(钢条切割)

钢条切割问题 现有一段长度为n英寸的钢条和一个价格表pi,求切割方案使销售利益最大rn最大 长度为n英寸的钢条共有2n?1种不同的切割方案,因为可以每个整英寸的位置都可以决定切割或者不切割. 为了得到rn最大,可以把这个问题分成子问题求解,先切一刀,再考虑余下的部分的最大收益即求 rn=max{pk+rn?k}(k=1,2,3-n-1), pk部分不进行继续切割,直接作为一个整体售出 ; rn?k部分继续切割,考虑所有的情况,分成子问题. 求出所有k值对应的收益最大者作为rn 也有可能不进行任何

动态规划之钢条切割

动态规划方法通常用来求解最优化问题.动态规划算法设计步骤: 1.刻画一个最优解的结构特征. 2.递归定义最优解的值. 3.计算最优解的值,通常采用自底向上的方法. 4.利用计算出的信息构造一个最优解. 动态规划的实现方法: 带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中).当需要一个子问题的解时,过程首先检查是否已经保存过此解.如果是,则直接返回保存的值,从而节省了计算时间:否则,按通常方式计算这个子问题. 自底向上法:这种方法一般

算法导论 之 动态规划 - 矩阵链相乘

1 引言 在大学期间,我们学过高等数学中的线性规划,其中有关于矩阵相乘的章节:只有当矩阵A的列数与矩阵B的行数相等时,A×B才有意义.一个m×n的矩阵A(m,n)左乘一个n×p的矩阵B(n,p),会得到一个m×p的矩阵C(m,p).矩阵乘法满足结合律,但不满足交换律. 假设现要计算A×B×C×D的值,因矩阵乘法满足结合律,不满足交换律,即:A.B.C.D相邻成员的相乘顺序不会影响到最终的计算结果,比如: A×(B×(C×D)).A×((B×C)×D).(A×B)×(C×D).A×(B×C)×D.

MIT:算法导论——15.动态规划

[设计一个动态规划算法的四个步骤] 1.刻画一个最优解的特征. (最优子结构?!) 2.递归地定义最优解的值. 3.计算最优解的值,通常采用自底向上方法. 4.利用计算出的信息构造一个最优解. 适合动态规划方法求解的最优化问题需要具备的两个性质: [最优子结构(optimal substructure)] 问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解.(动态规划具体实现之处) [重叠子问题(overlapping subproblem)] 如果递归算法反复求解相同的子问题,

算法导论---------动态规划之钢条切割

动态规划方法通常用来求解最优化问题.动态规划算法设计步骤: 1.刻画一个最优解的结构特征. 2.递归定义最优解的值. 3.计算最优解的值,通常采用自底向上的方法. 4.利用计算出的信息构造一个最优解. 动态规划的实现方法: 带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中).当需要一个子问题的解时,过程首先检查是否已经保存过此解.如果是,则直接返回保存的值,从而节省了计算时间:否则,按通常方式计算这个子问题. 自底向上法:这种方法一般

【算法设计-动态规划】钢条切割问题

问题:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,...,n),求切割钢条方案,使得销售收益rn最大.如果长度为n英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割. 方法一:递归 从上而下把所有的全部搜索一遍 int CUT_ROD(int p[],int n) { if(n==0) return 0; int q=INT_MIN; for(int i=1;i<=n;i++) { q=max(q,p[i]+CUT_ROD(p,n-i)); printf("n=%d&qu

算法导论_动态规划_最长公共子序列

一.动态规划的概念 动态规划(Dynamic Programming)是通过组合子问题的解而解决整个问题的.分治是指将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原始问题的解,与此不同,动态规划适用于子问题不是独立的情况,也就是各个子问题包含公共的子问题.在这种情况下,采用分治法会做许多不必要的工作,即重复地求解公共地子问题.动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免每次遇到各个子问题时重新计算答案. 动态规划通常应用于最优化问题.此类问题可