详述钢条切割问题--算法导论

        我们知道Serling公司出售一段长度为i英寸的钢条的价格为p_{i}(i=1,2,3......n)钢条的长度为整英寸。价格表样例如:

钢条切割问题:

给定一段长度为n英寸的钢条和一个价格表p_{i},求切割钢条的方案,使得销售收益r_{n}最大。注意,如果长度为n英寸的钢条的价格足够大,最优解可能就是完全不需要切割。

        下面给出4英寸的不同切割方案:

长度为n英寸的钢条有n-1个切割点,每个点都可选择切割与不切割,则共有2*2*2......*2*2(n-1个2相乘)=2^{n-1}种切割方案。

        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(n)=1+\sum_{j=0}^{n-1}T(j)

数学归纳法求解:

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时剖析图: 

观察上面剖析图,分析消耗的运行时间,由于直接查备忘录返回的时间忽略不计,得如下图:

如图可看出,循环和递归构成了一个等差数列,不难分析出运行的时间为\Theta (n^{2})

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循环的迭代次数构成一个等差数列,不难分析过程的运行时间是\Theta (n^{2})

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值