(来自算法导论P204)
问题描述:
给定一段长度为n英寸的钢条和一个价格表Pi(i=1,2,3,...,n),求切割钢条方案,使收益rn最大。
思路(递归):
在钢条左边切割i段,再对右边剩余长度n-i段继续进行切割(递归求解),对左边的一段的则不再进行切割。
所以有:若不做切割第一段长度为n,收益为pn,剩余部分长度为0,对应的收益ro=0;
则求解公式:r(n)=max(1<=i<=n)(p(i)+r(n-i)).
不难由组合数学知道共有2次方n-1种情况。(n个钢条有n-1个可能的切割点,每个切割点有两种情况(切,不切)),不难知道如果不考虑位置关系,有大量重复。
时间复杂度求解:T(N)=1+T(0)+T(1)+...+T(N-1);
可求得到:T(N)-T(N-1)=T(N-1);
T(N)=2^n;
用动态规划解决:由于递归求解了大量子问题,用时空权衡
方法一:自顶向下带备忘的:(思维难度较小)
c++代码实现:
//自顶向下
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
int cut_ans(int r[],int p[],int n);
int main()
{
int p[100]={0,1,5,8,9,10,17,17,20,24,30},n;
int r[100]; // 时空权衡。
memset(r,-1,sizeof(r));
// for(int i=0;i<100;i++)
// p[i]=i;
cin>>n;
cout<<cut_ans(r,p,n)<<endl;
return 0;
}
int cut_ans(int r[],int p[],int n)
{
int q=-1;
if(r[n]>=0) return r[n];
if(n==0) q=0;
else for(int i=1;i<=n;i++)
q=max(q,cut_ans(r,p,n-i)+p[i]); //q 取循环过程结果的最大值,我记得我原来用了条件句来记录最高点,实在愚昧。。
r[n]=q;
return r[n];
}
// 如果想知道时间效率提高了多么高,可以把我中间“//”的去掉,然后测n<100时有多快、。
方法2:自底向上:
和自顶向上具有相同的复杂度,由于没有频繁的递归函数调用开销,将比自顶向下的时间复杂性函数具有更小的系数。
c++代码实现:
//自顶向上
#include<iostream>
#include<algorithm>
using namespace std;
int cut_ans(int r[],int p[],int n );
int main()
{
int r[100];
r[0]=0;
int p[100]={0,1,5,8,9,10,17,17,20,24,30},n;
cin>>n;
cout<<cut_ans(r,p,n)<<endl;
return 0;
}
int cut_ans(int r[],int p[],int n)
{
for(int m=1;m<=n;m++)
{
int q=-1;
for(int i=0;i<m;i++)
{
q=max(q,r[i]+p[m-i]);
r[m]=q;
}
}
return r[n];
}
重构解
如何输出对应的具体切割方法?
这时候就要动点脑筋,每次求解出n长度对应的最优解r(n)时候记录下左边第一段切割的长度。记录在s【n】数组里面。
因为用了dp,所以会依次记录下从1 to n的左边第一段的切割的长度。
输出先上伪代码再分析:
print——cut-rod-solution(p,n)
(r,s)=extended-bottom-up-cut-rod(p,n)
while n>0
print s[n];
n=n-s[n];
这样的循环输出第一段长度就自然输出了对于n的具体切割的每段长度的解。
再上重构解的代码:
重构解:
extaned -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
还有一个问题的延伸:每次切割还需要固定的成本,设计一个动态规划求解问题。
解答:(仍然是自顶向下和自底向上)
//切割成本为c时的自顶而下的dp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
int cut_ans(int r[],int p[],int n ,int c);
int r[100],c,n;
memset(r,-1,sizeof(r));
r[0]=0;
int p[100]={0,1,5,8,9,10,17,17,20,24,30};
cin>>c>>n;
cout<<cut_ans(r,p,n,c)<<endl;
}
int cut_ans(int r[],int p[],int n,int c )
{
int q=-1;
if(n==0) return r[0];
if(r[n]>0) return r[n];
for(int i=1;i<=n;i++)
{
q=max(q,p[i]+cut_ans(r,p,n-i,c)-c*(i!=n)); //当i!=c时,也就是在中间某处切割了一次,所以收益减去c。当i==c时,也就是不切割,收益不用减去c。
}
r[n]=q;
return r[n];
}
*/
//切割成本为c时的自底而上的dp
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
int cut_ans(int r[],int p[],int n ,int c);
int r[100]={0},c,n;
int p[100]={0,1,5,8,9,10,17,17,20,24,30};
cin>>c>>n;
cout<<cut_ans(r,p,n,c)<<endl;
}
int cut_ans(int r[],int p[],int n,int c )
{
for(int i=1;i<=n;i++)
{
int q=-1;
for(int j=1;j<=i;j++)
{
q=max(q,p[j]+r[i-j]-c*(j==i)); 当i!=c时,也就是在中间某处切割了一次,所以收益减去c。当i==c时,也就是不切割,收益不用减去c。
}
r[i]=q;
}
return r[n];
}
可以用输入(1,5);(2,5);(100,5)验证。
应该输出12,11,10。
思路总结(算法导论)
1.刻画一个最优解的结构特征。
2.递归的定义最优解的值。
3.计算最优解的值,通常采用自底向上的方法。
4.利用计算信息构造一个最优解。