您的位置:首页 > 其它

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

2014-11-07 08:31 477 查看



一、综述

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


动态规划对每个子问题只求解一次,将其结果保存在一张表中。
动态规划通常用于最优化问题。
动态规划的设计步骤:
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
>=0
return r

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
=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
>=0)
return r
;
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
=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
=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

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
;
}
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-s

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
<<'\t';
n=n-s
;
}
}

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
<<endl;//最大受益

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




内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息