我们知道Serling公司出售一段长度为i英寸的钢条的价格为(i=1,2,3......n)钢条的长度为整英寸。价格表样例如:
钢条切割问题:
给定一段长度为n英寸的钢条和一个价格表,求切割钢条的方案,使得销售收益
最大。注意,如果长度为n英寸的钢条的价格足够大,最优解可能就是完全不需要切割。
下面给出4英寸的不同切割方案:
长度为n英寸的钢条有n-1个切割点,每个点都可选择切割与不切割,则共有2*2*2......*2*2(n-1个2相乘)=种切割方案。
4英寸长度的钢条的最大收益切割为2+2,收益为10。我们从中可以知道n英寸的钢条最大收益可由切割后的小钢条的最大收益组合而成,这就是“大事化小,小事化了”。
下面给出了10英寸及以内(价格表只给到了10英寸的价格)的钢条最佳切割方案:
我们发现它们最优解都是由无切割的长度组合,这些无法切割的长度就是最优的子结构。
一,自顶向下递归法
我们将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割(递归)。这样,不做任何切割的方案可以描述为:切割的长度为n,剩余长度为0。
以4英寸的钢条为例,可得如下的树:
代码实现:
#pragma once
#include<iostream>
using namespace std;
//钢条长度的价值
typedef struct Steel_Bar
{
int value[11] = {0,1,5,8,9,10,17,17,20,24,30};
}SB;
//返回最大值
int MAX(int a, int b)
{
if (a > b)
return a;
else
return b;
}
//递归
int CUT_ROD(SB s,int n)
{
if (n == 0)
return 0;
int q=-1; //q来存储最大收益
for (int i = 1; i <= n; i++)
{
q = MAX(q, s.value[i] + CUT_ROD(s, n - i));//观察树,找出一层中最大收益
}
return q;
}
void tests()
{
SB s;
for (int i = 0; i <= 10; i++)
{
cout <<"长度为"<<i<<"英寸的钢条最大收益为:" << CUT_ROD(s, i) << endl;
}
}
输出:
长度为0英寸的钢条最大收益为:0
长度为1英寸的钢条最大收益为:1
长度为2英寸的钢条最大收益为:5
长度为3英寸的钢条最大收益为:8
长度为4英寸的钢条最大收益为:10
长度为5英寸的钢条最大收益为:13
长度为6英寸的钢条最大收益为:17
长度为7英寸的钢条最大收益为:18
长度为8英寸的钢条最大收益为:22
长度为9英寸的钢条最大收益为:25
长度为10英寸的钢条最大收益为:30
当n为4时剖析图:
当你在计算机上运行它时,你会发现,一旦输入的规模稍稍变大,程序运行时间会变的非常长。这是因为他反复的求解了相同的子问题。
我们来分析分析该算法的运行时间,可根据上方树列出如下公式,1是根结点对应的最初一次调用,后面是每次分割后的递归。
数学归纳法求解:
T(0)=1=2^0
T(1)=1+T(0)=2^1
T(2)=1+T(0)+T(1)=4=2^2
假设n=k T(k)=2^k
要证T(k+1)=2^(k+1)
T(k+1)=1+T(0)+T(1)+...+T(k)=1+2^0+2^1+2^2+...+2^k
=1+1*(1-2^(n+1))/1-2=2^(k+1)
证毕
即T(n)=2^n
该算法的运行时间是n的指数函数,效率是非常低的,我们不让它重复计算相同的子问题不就大大缩减了运行时间,这就是我们接下来的动态规划算法。
二,动态规划方法求解最优钢条切割问题
我们已经看到,朴素递归算法之所以效率很低,是因为它反复求解相同的子问题。因此,动态规划方法仔细安排求解顺序,对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需要查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间。
1.带备忘的自顶向下法
#pragma once
#include<iostream>
using namespace std;
//钢条长度的价值
typedef struct Steel_Bar
{
int value[11] = {0,1,5,8,9,10,17,17,20,24,30};
}SB;
//返回最大值
int MAX(int a, int b)
{
if (a > b)
return a;
else
return b;
}
//输出备忘录
void PrintMemo(int r[])
{
for (int i = 0; i <= 10; i++)
{
cout << "备忘录r[" << i << "]=" << r[i] << endl;
}
}
int CUT_ROD(SB s, int n, int r[])
{
if (r[n]>=0)
return r[n]; //n>=0说明以找到最优解,直接放回,不需要重复计算
int q=-1;
for (int i = 1; i <= n; i++)
{
q = MAX(q, s.value[i] + CUT_ROD(s, n - i,r));
}
r[n] = q; //将找到的最优解储存在备忘录
return q;
}
//带备忘的自顶向下法
int MEMOIZED_CUT_ROD(SB s, int n)
{
int r[11]; //备忘录数组
r[0] = 0; //n为0时,价值为0
for (int i = 1; i < 11; i++)
{
r[i] = -1;
}
int result= CUT_ROD(s, n,r);
PrintMemo(r);
return result;
}
void tests()
{
SB s;
for (int i = 0; i <= 10; i++)
{
cout <<"长度为"<<i<<"英寸的钢条最大收益为:" << MEMOIZED_CUT_ROD(s, i) << endl;
}
}
输出:
备忘录r[0]=0
备忘录r[1]=-1
备忘录r[2]=-1
备忘录r[3]=-1
备忘录r[4]=-1
备忘录r[5]=-1
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为0英寸的钢条最大收益为:0
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=-1
备忘录r[3]=-1
备忘录r[4]=-1
备忘录r[5]=-1
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为1英寸的钢条最大收益为:1
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=-1
备忘录r[4]=-1
备忘录r[5]=-1
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为2英寸的钢条最大收益为:5
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=-1
备忘录r[5]=-1
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为3英寸的钢条最大收益为:8
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=-1
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为4英寸的钢条最大收益为:10
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=-1
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为5英寸的钢条最大收益为:13
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=-1
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为6英寸的钢条最大收益为:17
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=-1
备忘录r[9]=-1
备忘录r[10]=-1
长度为7英寸的钢条最大收益为:18
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=-1
备忘录r[10]=-1
长度为8英寸的钢条最大收益为:22
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=25
备忘录r[10]=-1
长度为9英寸的钢条最大收益为:25
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=25
备忘录r[10]=30
长度为10英寸的钢条最大收益为:30
n为4时剖析图:
观察上面剖析图,分析消耗的运行时间,由于直接查备忘录返回的时间忽略不计,得如下图:
如图可看出,循环和递归构成了一个等差数列,不难分析出运行的时间为
2.自底向上
#pragma once
#include<iostream>
using namespace std;
//钢条长度的价值
typedef struct Steel_Bar
{
int value[11] = {0,1,5,8,9,10,17,17,20,24,30};
}SB;
//返回最大值
int MAX(int a, int b)
{
if (a > b)
return a;
else
return b;
}
//输出备忘录
void PrintMemo(int r[])
{
for (int i = 0; i <= 10; i++)
{
cout << "备忘录r[" << i << "]=" << r[i] << endl;
}
}
//带备忘的自底向上法
int BOTTOM_UP_CUT_ROD(SB s, int n)
{
int r[11]; //备忘录数组
r[0] = 0; //n为0时,价值为0
for (int j = 1; j <= n; j++)
{
int q = -1;
for (int i = 1; i <= j; i++)
{
q = MAX(q, s.value[i] + r[j - i]);
}
r[j] = q;
}
PrintMemo(r);
return r[n];
}
void tests()
{
SB s;
for (int i = 0; i <= 10; i++)
{
cout <<"长度为"<<i<<"英寸的钢条最大收益为:" << BOTTOM_UP_CUT_ROD(s, i) << endl;
}
}
输出:
备忘录r[0]=0
备忘录r[1]=-858993460
备忘录r[2]=-858993460
备忘录r[3]=-858993460
备忘录r[4]=-858993460
备忘录r[5]=-858993460
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为0英寸的钢条最大收益为:0
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=-858993460
备忘录r[3]=-858993460
备忘录r[4]=-858993460
备忘录r[5]=-858993460
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为1英寸的钢条最大收益为:1
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=-858993460
备忘录r[4]=-858993460
备忘录r[5]=-858993460
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为2英寸的钢条最大收益为:5
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=-858993460
备忘录r[5]=-858993460
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为3英寸的钢条最大收益为:8
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=-858993460
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为4英寸的钢条最大收益为:10
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=-858993460
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为5英寸的钢条最大收益为:13
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=-858993460
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为6英寸的钢条最大收益为:17
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=-858993460
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为7英寸的钢条最大收益为:18
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=-858993460
备忘录r[10]=-858993460
长度为8英寸的钢条最大收益为:22
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=25
备忘录r[10]=-858993460
长度为9英寸的钢条最大收益为:25
备忘录r[0]=0
备忘录r[1]=1
备忘录r[2]=5
备忘录r[3]=8
备忘录r[4]=10
备忘录r[5]=13
备忘录r[6]=17
备忘录r[7]=18
备忘录r[8]=22
备忘录r[9]=25
备忘录r[10]=30
长度为10英寸的钢条最大收益为:30
样例:
由样例图可看出自底向上算法嵌套的是双重循环,内层for循环的迭代次数构成一个等差数列,不难分析过程的运行时间是
3.优化(保存最优解第一段钢条的切割长度)
#pragma once
#include<iostream>
#include<iomanip>
using namespace std;
//钢条长度的价值
typedef struct Steel_Bar
{
int value[11] = {0,1,5,8,9,10,17,17,20,24,30};
}SB;
//返回最大值
int MAX(int a, int b)
{
if (a > b)
return a;
else
return b;
}
//输出备忘录
void PrintMemo(int r[],int length[],int n)
{
cout << "钢条的长度:";
for (int i = 0; i <= n; i++)
{
cout << left << setw(5) << i << " ";
}
cout << endl;
cout << "最大的收益:";
for (int i = 0; i <= n; i++)
{
cout<< left << setw(5) << r[i] << " ";
}
cout << endl;
cout << "第一段长度:";
for (int i = 0; i <= n; i++)
{
cout << left << setw(5) << length[i] << " ";
}
cout << endl;
}
//带备忘的自底向上法
int BOTTOM_UP_CUT_ROD(SB s, int n)
{
int r[11]; //备忘录数组
int length[11];//存储第一段长度
r[0] = 0; //n为0时,价值为0
length[0] = 0;
for (int j = 1; j <= n; j++)
{
int q = -1;
for (int i = 1; i <= j; i++)
{
if (q < s.value[i] + r[j - i]) //前一个分割收益小于该分割收益,替换收益并修改其对应的第一条钢条的切割长度
{
q = s.value[i] + r[j - i];
length[j] = i;
}
}
r[j] = q;
}
PrintMemo(r, length,n);
return r[n];
}
void tests()
{
SB s;
for (int i = 0; i <= 10; i++)
{
cout <<"长度为"<<i<<"英寸的钢条最大收益为:" << BOTTOM_UP_CUT_ROD(s, i) << endl;
}
}
输出:
钢条的长度:0
最大的收益:0
第一段长度:0
长度为0英寸的钢条最大收益为:0
钢条的长度:0 1
最大的收益:0 1
第一段长度:0 1
长度为1英寸的钢条最大收益为:1
钢条的长度:0 1 2
最大的收益:0 1 5
第一段长度:0 1 2
长度为2英寸的钢条最大收益为:5
钢条的长度:0 1 2 3
最大的收益:0 1 5 8
第一段长度:0 1 2 3
长度为3英寸的钢条最大收益为:8
钢条的长度:0 1 2 3 4
最大的收益:0 1 5 8 10
第一段长度:0 1 2 3 2
长度为4英寸的钢条最大收益为:10
钢条的长度:0 1 2 3 4 5
最大的收益:0 1 5 8 10 13
第一段长度:0 1 2 3 2 2
长度为5英寸的钢条最大收益为:13
钢条的长度:0 1 2 3 4 5 6
最大的收益:0 1 5 8 10 13 17
第一段长度:0 1 2 3 2 2 6
长度为6英寸的钢条最大收益为:17
钢条的长度:0 1 2 3 4 5 6 7
最大的收益:0 1 5 8 10 13 17 18
第一段长度:0 1 2 3 2 2 6 1
长度为7英寸的钢条最大收益为:18
钢条的长度:0 1 2 3 4 5 6 7 8
最大的收益:0 1 5 8 10 13 17 18 22
第一段长度:0 1 2 3 2 2 6 1 2
长度为8英寸的钢条最大收益为:22
钢条的长度:0 1 2 3 4 5 6 7 8 9
最大的收益:0 1 5 8 10 13 17 18 22 25
第一段长度:0 1 2 3 2 2 6 1 2 3
长度为9英寸的钢条最大收益为:25
钢条的长度:0 1 2 3 4 5 6 7 8 9 10
最大的收益:0 1 5 8 10 13 17 18 22 25 30
第一段长度:0 1 2 3 2 2 6 1 2 3 10
长度为10英寸的钢条最大收益为:30