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

动态规划算法概述ios

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

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

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

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

  3.计算最优解的值。spa

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

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

钢条切割问题blog

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

长度: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来表示钢条单价,则有Rn的函数:             

Rn = maxPx + Rn - 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,为备忘之意。

 

            参考文献:

《算法导论》

相关文章
相关标签/搜索