#动态规划(一)
算法导论动态规划——钢条切割
- **Serling公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。假定我们知道Serling公司出售一段长为i英寸的钢条的价格为pi(i=1,2,…,单位为美元)。钢条的长度均为整英寸。图15-1给出了一个价格表的样例。
钢条切割问题是这样的:给定一段长度为n英寸的钢条和一个价格表 p i {p_i} pi(i=1,2,…n),求切割钢条方案,使得销售收益 r n {r_n} rn最大。注意,如果长度为n英寸的钢条的价格 p n {p_n} pn足够大,最优解可能就是完全不需要切割。
问题分析
分割钢条的所有方案共有 2 n − 1 2^{n-1} 2n−1(长度为n的钢条,可切割点有n-1个,可以选择切或不切)
- 假设最佳方案为将钢条切割为k段,分别为 i 1 {i_1} i1, i 2 {i_2} i2,··· i k {i_k} ik;对于各段得到的最大收益分别为人 r 1 {r_1} r1, r 2 {r_2} r2,··· r k {r_k} rk;
- 计算ri是小于规模n的新的子问题,且 r 1 {r_1} r1, r 2 {r_2} r2,··· r k {r_k} rk,可能有相互重叠部分。
- 这个题可以用深搜做,遍历所有方案做比较,时间效率为指数级。
- 若用动态规划做:分析样例表:
- n=1时, r 1 {r_1} r1=1;
- n=2时, r 2 {r_2} r2=5;
- n=3时, r 3 {r_3} r3=8;
- n=4时, r 4 {r_4} r4= r 2 {r_2} r2+ r 2 {r_2} r2=10;
- n=5时, r 5 {r_5} r5= r 2 {r_2} r2+ r 3 {r_3} r3=13;
- n=6时, r 6 {r_6} r6=17;
- n=7时, r 7 {r_7} r7= r 2 {r_2} r2+ r 5 {r_5} r5= r 2 {r_2} r2+ r 2 {r_2} r2+ r 3 {r_3} r3=18
- n=8时, r 8 {r_8} r8= r 3 {r_3} r3+ r 5 {r_5} r5=21
- n=9时, r 9 {r_9} r9= r 3 {r_3} r3+ r 6 {r_6} r6= r 4 {r_4} r4+ r 5 {r_5} r5=25
- n=10时,
r
10
{r_{10}}
r10=30;
方法一:选择第一个切割点:
r n {r_n} rn=max( p n {p_n} pn, r 1 {r_1} r1+ r n − 1 {r_{n-1}} rn−1,··· r i {r_i} ri+ r n − i {r_{n-i}} rn−i)i>=1&&i<=n/2;
代码块(一)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int p[1001];
int r[1001];
int N;
int main() {
memset(r,-1,sizeof(r));
cin>>N;
for(int i =1; i<=N; i++) {
cin>>p[i];
}
int CutRod(int n);
int ans;
ans=CutRod(N);
cout<<ans<<endl;
}
int CutRod(int n) {
if(n==1)return p[n];
int m=p[n];
for(int i =1; i<=n/2; i++) {
m=max(m,CutRod(i)+CutRod(n-i));
}
return m;
}
方法二:将钢条从左边切割下长度为i的一段,只对右边剩下的长度为n-i的一段继续进行切割,对左边的一段则不再进行切割。这样得到的公式为:。这样原问题的最优解只包含一个相关子问题(右端剩余部分)的解,而不是两个。:
r
n
{r_n}
rn=max(
p
i
{p_i}
pi+
r
n
−
i
{r_{n-i}}
rn−i),其中i>=n/2&&i<=n;
当i=n时,
r
n
{r_n}
rn=(
p
n
{p_n}
pn+
r
0
{r_{0}}
r0)=
p
n
{p_n}
pn,即对长度为n的钢条不切割的状态。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int p[1001];
int r[1001];
int N;
int main() {
int CutRod2(int n);
memset(r,-1,sizeof(r));
cin>>N;
for(int i =1; i<=N; i++) {
cin>>p[i];
}
int ans;
ans=CutRod2(N);
cout<<ans<<endl;
}
int CutRod2(int n) {
int m=p[n];
if(n==0)return 0;
else if(n==1)return p[n];
else {
for(int i =n/2; i<=n; i++) {//因为n=1时,n/2==0,p[0]越界,所以,n==1要单独拿出来。
int t=p[i]+CutRod2(n-i);
if(t>m)
m=t;
}
return m;
}
}
方法三:在递归过程中涉及到很多次的重复递归计算,可以将曾经计算过的数据保存到数组,后续直接从数组中找,没有再计算。下面在方法二的基础上加入记忆数组。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int p[1001];
int r[1001];
int N;
int memory[1001];
int main() {
int CutRod2(int n);
memset(r,-1,sizeof(r));
cin>>N;
for(int i =1; i<=N; i++) {
cin>>p[i];
}
int ans;
memset(memory,-1,sizeof(memory));//对memory数组初始化;因为收益不为负数,所以初始化为-1
memory[0]=0 ;
memory[1]=p[1];
ans=CutRod2(N);
cout<<ans<<endl;
}
int CutRod3(int n) {
if(memory[n]>=0)return memory[n];
else {
for(int i =n/2; i<=n; i++) {
//因为n=1时,n/2==0,p[0]中没有存有效数据,所以,n==1的情况要单独拿出来。
int t=p[i]+CutRod2(n-i);
if(t>memory[n])
memory[n]=t;
}
//memory数组保存中间计算结果。且结果一旦保存,不会再发生改变。
return memory[n];
}
}
重构解(即保存切割策略)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int p[1001];
int r[1001];
int N;
int memory[1001];
int main() {
int CutRod3(int n);
memset(r,-1,sizeof(r));
cin>>N;
for(int i =1; i<=N; i++) {
cin>>p[i];
}
int ans;
memset(memory,-1,sizeof(memory));//对memory数组初始化因为收益不为负数,所以初始化为-1
memory[0]=0 ;
memory[1]=p[1];
r[1]=1;//对r[1]直接赋值
ans=CutRod3(N);
cout<<"最大收益为:"<<ans<<endl;
cout<<"可最大收益的策略为:";
while(N>0&&r[N]>0) {
cout<<r[N]<<" ";
N-=r[N];
}
}
int CutRod3(int n) {
if(memory[n]>=0)return memory[n];
else {
for(int i =n/2; i<=n; i++) {
//因为n=1时,n/2==0,p[0]中没有存有效数据,所以,n==1要单独拿出来。
int t=p[i]+CutRod3(n-i);
if(t>=memory[n]) {//当N=7时,t>memory[n] 输出方案为3 2 2
memory[n]=t; //t>=memory[n] 输出方案为 6 1
r[n]=i;
}
}
//memory数组保存中间计算结果。且结果一旦保存,不会再发生改变。
return memory[n];
}
}
方法一、二、三、及重构解均为自顶向下设计算法的。下面分析如何自底向上设计算法:
由子问题图可以发现,上层问题只依赖于比自己低的子问题。
#include<iostream>
using namespace std;
int p[1001];
int memory[1001];
int r[1001];
int N;
int main() {
cin>>N;
for(int i =1; i<=N; i++) {
cin>>p[i];
}
memory[0]=0;
for(int i=1; i<=N; i++) {
int max=p[i];//假设当前不切割为最大收益
int length=i;//假设当前不切割为最大收益的,策略长度为i
for(int j =1; j<i; j++) {
int t=memory[j]+memory[i-j];//由图可知,规模为i的问题,只与所有规模为j(j<i)的问题有关联。
if(max<t) {
max=t;
length=j;
}
}
memory[i]=max;
r[i]=length;
}
cout<<"最大收益为:"<<memory[N]<<endl;
cout<<"获得最大收益的策略是:";
while(N>0&&r[N]>0) {
cout<<r[N]<<" ";
N-=r[N];
}
}