算法导论-第15章-动态规划-15.1 钢条切割问题

一、综述


动态规划是通过组合子问题的解而解决整个问题的。

动态规划对每个子问题只求解一次,将其结果保存在一张表中。
动态规划通常用于最优化问题。
动态规划的设计步骤:
a. 描述最优解的结构
b. 递归定义最优解的值
c. 按自底向上的方式计算最优解的值
d. 由计算出的信息构造一个最优解


动态规划方法对每个子问题只求解一次,并将结果保存下来。如果随后再次需要此子问题的解,只需查找保存的结果,而不必重新计算。因此,动态规划方法是付出额外的内存空间来节省计算时间,是典型的时空权衡的例子。时间上的节省是非常巨大的:可能将一个指数时间的解转化为一个多项式时间的解。
动态规划有两种等价的实现方法
1、带备忘的自顶向下法。
此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间;否则,按通常方式计算这个子问题。
2、自底向上法。
这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都只依赖于“更小的”子问题求解。因而我们可以将子问题按规模排序,按从小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它时,它的所有前提子问题都已求解完毕。
通常情况下,如果每个子问题都必须至少求解一次,自底向上法会比自顶向下法快,因为自底向上法没有递归调用的开销,表的维护开销也更小。如果子问题空间中的某些子问题完全不必求解,备忘法就体现出优势了,因为它只会求解那些绝对必要的子问题。

二、代码


15.1 钢条切割
假设公司出售一段长度为i英寸的钢条的价格为Pi(i = 1, 2, ...单位:美元),下面给出了价格表样例:
长度i  1 2 3 4  5  6  7  8  9  10
价格Pi 1 5 8 9 10 17 17  20 24 30
切割钢条的问题是这样的:给定一段长度为n英寸的钢条和一个价格表Pi,求切割方案,使得销售收益Rn最大。
当然,如果长度为n英寸的钢条价格Pn足够大,最优解可能就是完全不需要切割。
对于上述价格表样例,我们可以观察所有最优收益值Ri及对应的最优解方案:
R1 = 1,切割方案1 = 1(无切割)
R2 = 5,切割方案2 = 2(无切割)
R3 = 8, 切割方案3 = 3(无切割)
R4 = 10, 切割方案4 = 2 + 2
R5 = 13, 切割方案5 = 2 + 3
R6 = 17, 切割方案6 = 6(无切割)
R7 = 18, 切割方案7 = 1 + 6或7 = 2 + 2 + 3
R8 = 22, 切割方案8 = 2 + 6
R9 = 25, 切割方案9 = 3 + 6
R10 = 30,切割方案10 = 10(无切割)
更一般地,对于Rn(n >= 1),我们可以用更短的钢条的最优切割收益来描述它:
Rn = max(Pn, R1 + Rn-1, R2 + Rn-2,...,Rn-1 + R1)
首先将钢条切割为长度为i和n - i两段,接着求解这两段的最优切割收益Ri和Rn - i(每种方案的最优收益为两段的最优收益之和),由于无法预知哪种方案会获得最优收益,我们必须考察所有可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,我们当然可以选择不做任何切割。
 思路:先将钢条切成两条,有n-1种方案,每一种方案的最优解都等于两个子钢条的最优解。我们从这n-1个伪最优解再挑出最优的解了。

 自顶向下递归实现的伪代码:

CUT-ROD(p,n)
 if n == 0
     return 0
 q=负无穷
 for i = 1 to n
     q=max(q,p[i]+CUT-ROD(p,n-i))
 return q


C++代码

#include<iostream>
using namespace std;

#define NIL (-0x7fffffff-1)

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int cut_rod(int *p,int n)
{
	if(n==0)
		return 0;
	int q=NIL;
	if(n<=10)
	{
		for (int i=0;i<n;i++)
		{
			q=max(q,p[i]+cut_rod(p,n-1-i));
		}
		return q;
	}
	else if(n>10)
	{
		int b=n/10;
		n=n-b*10;
		if(n==0)
			q=0;
		for (int i=0;i<n;i++)
		{
			q=max(q,p[i]+cut_rod(p,n-1-i));;
		}
		return q+b*30;
	}
}
int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	int result=cut_rod(p,n);
	cout<<result<<endl;
	return 0;
}

上面只用了分治策略,这个算法的性能很差,是指数次的,递归调用次数T(n)=2^n,因为在子问题的求解中很多都是重复的。

下面介绍动态规划法
1、带备忘的自顶向下法

EMOIZED-CUT-ROD(p,n)
let r[0..n] be a new array
for i = 0 to n
//记录长度为n时已经求得的受益最大的结果
return MEM-CUT-ROD-AUX(p,n,r)

MEMOIZED-CUT-ROD-AUX(p,n,r)
if r[n]>=0
	return r[n]
if n==0
	q=0
else 
	q=负无穷
	for i=1 to n
		q=max(q,p[i]+MEM-CUT-ROD-AUX(p,n-i,r))
r[n]=q
return q


C++代码

#include<iostream>
using namespace std;
#define NIL -10000

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int memoized_cut_rod_aux(int *p,int n,int *r)
{
	int q=NIL;
	if(r[n]>=0)
		return r[n];
	if(n==0)
		q=0;
	else
		{
			q=NIL;
			if(n<=10)
			{
				for (int i=0;i<n;i++)
				{
					q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
				}
				r[n]=q;
				return q;
			}
			else if(n>10)
			{
				int b=n/10;
				n=n-b*10;
				if(n==0)
					q=0;
				for (int i=0;i<n;i++)
				{
					q=max(q,p[i]+memoized_cut_rod_aux(p,n-1-i,r));
				}
				r[n]=q+b*30;
				return q+b*30;
			}
		}
	return 0;
}
int memoized_cut_rod(int *p,int n)
{
	int *r;
	r=(int *)malloc(sizeof(int)*(n+1));
	for (int i=0;i<n;i++)
	{
		r[i]=NIL;
	}
	return memoized_cut_rod_aux(p,n,r);
}

int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	int result=memoized_cut_rod(p,n);
	cout<<result<<endl;
	return 0;
}

2、自底向上版本

//从小到大依次求出每种长度的受益的最优解
 BOTTOM-UP-CUT-ROD(p,n)
 let r[0..n] be a new array
 r[0]=0
 for j=1 to n
     q=负无穷
     for i=1 to j
         q=max(q,p[i]+r[j-i])
	 r[j]=q
 return r[n]
C++代码
//自底向上的方法,这里面的n不能超过11,受P限制
#include<iostream>
using namespace std;
#define NIL -10000

int max(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}
int bottom_cut_rod(int *p,int n)
{
	int *r;
	r=(int *)malloc(sizeof(int)*(n+1));
	r[0]=0;
	for (int j=0;j<n;j++)
	{
		int q=NIL;
		for(int i=0;i<=j;i++)
		{
			q=max(q,p[i]+r[j-i]);
		}
		r[j+1]=q;
	}
	return r[n];
}
int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	cout<<"Please input a int number: ";
	cin>>n;
	cout<<bottom_cut_rod(p,n);
	cout<<endl;
	return 0;
}
3、下面的伪代码还保留了切割长度

EXTEND-BOTTOM-UP-CUT-ROD(p,n)
 let r[0..n] and s[0..n] be new arrays
 r[0]=0
 for j = 1 to n
     q=负无穷
     for i =1 to j
        if q < p[i]+r[j-i]
             q=p[i]+r[j-i]
             s[j]=i
             r[j]=q
 return r and s

4、 打印出切割长度
 PRINT-CUT-ROD-SOLUTION(p,n)
 (r,s)=EXTEND-BOTTOM-UP-CUT-ROD(p,n)
 while n >0
     print s[n]
     n=n-s[n]

C++代码
#include<iostream>
using namespace std;
#define NIL -10000

void extern_bottom_up_cut_rod(int *p,int n,int *r,int *s)
{
	r[0]=0;
	s[0]=0;
	for (int j=0;j<n;j++)
	{
		int q=NIL;
		for (int i=0;i<=j;i++)
		{
			if(q<(p[i]+r[j-i]))
			{
				q=p[i]+r[j-i];
				s[j+1]=i+1;
				r[j+1]=q;
			}
		}
	}
}
void print_cut_rod_solution(int *s,int n)
{
	while (n>0)
	{
		cout<<s[n]<<'\t';
		n=n-s[n];
	}
}

int main()
{
	int p[]={1,5,8,9,10,17,17,20,24,30};
	int n;
	int *r;
	int *s;

	cout<<"Please input a int number: ";
	cin>>n;

	r=(int *)malloc(sizeof(int)*(n+1));//存放n时的最大受益
	s=(int *)malloc(sizeof(int)*(n+1));//存放n时的切割长度

	extern_bottom_up_cut_rod(p,n,r,s);

	cout<<r[n]<<endl;//最大受益

	cout<<"切割的长度分别是: "<<endl;
	print_cut_rod_solution(s,n);
	cout<<endl;
	free(r);
	free(s);
	return 0; 
}
















  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值