算法导论学习笔记-动态规划-15.1 钢条切割

简介
动态规划方法通常求解最优化问题,这类问题有很多可行的解,所以我们希望找到的是一个最优值,我们称这样的情况为一个最优解(an optimal solution),而不是最优解(the optimal solution),因为这个问题可能有多个解都能达到最大值。

一般按下面四个步骤来求解:
1.刻画一个最优解的结构特征。
2.递归地定义最优解的值。
3.计算最优解的值,通常采用自底向上的方法。
4.利用计算出的信息构造出一个最优解。
15.1 钢条切割

长度为n英寸的钢条有2n-1种切割方案,怎么出来的呢?可以把钢条看作n段,在i和i-1之间可以选择切开或者不切开,一共有(n-1)个可以被切开的地方,所以方案有 2n-1种不同方案。

对于最优切割方案
n = i1 + i2 + i3 + i4……+ik

有与之对应的最优收益值
n = pi1 + pi2 + pi3 + pi4……+pik

所以,对于一个已知长度的钢条,我们可以通过更短的钢条的最优切割来描述它。

即rn=max(pn , r1 + rn-1,r2 + rn-1,……,rn-1 + r1)

我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解,我们称钢条切割问题满足最优子结构(optimal substructure)性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

可以得到一个更为简单的递归求解方法:
rn=max(1<=i<=n)(pi + rn-1);

代码实现(递归):

int cut_rod(int n){
	if(n==0) return 0;
	int res=-INF;
	for(int i=1;i<=n;i++){//对左边一段切下来的长度进行枚举,之后左边的便不再进行切割 。 
		res=max(res,p[i]+cut_rod(n-i))//递归继续处理右边的部分。 
	}
	return res;
}

倘若通过递归求解的话,会浪费很多重复计算的时间,因为1、2之类的底下的长度会被多次计算。
通过记忆化可以提速,代码实现:
(如果你不了解记忆化,可以看这个帖子https://blog.csdn.net/su_bao/article/details/81181975)
inputs:

10
1 5 8 9 10 17 17 20 24 30
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXN 100000 //这里假设n最大为100000,视情况改变MAXN 
int mem[MAXN];//递归记忆化数组 
int p[MAXN];//记录i长度的钢条价格 
int cut_rod(int n){
	if(mem[n]) return mem[n];//对于已经处理过的值,直接返回记忆化数组的值 
	if(n==0) return 0;
	int res=-INF;
	for(int i=1;i<=n;i++){//对左边一段切下来的长度进行枚举,之后左边的便不再进行切割 。 
		res=max(res,p[i]+cut_rod(n-i));//递归继续处理右边的部分。 
	}
	mem[n]=res; 
	//cout<<"res of "<<n<<" : "<<res<<endl;//调试 
	return res;
}
int main(){
	int n;
	cin>>n;
	memset(mem,0,sizeof(mem)); 
	for(int i=1;i<=n;i++){
		cin>>p[i];//读入数据 
	}
	cout<<cut_rod(n)<<endl;//输出长度为n的钢条切割最优值 
}

通过中间的调试代码来输出中间值:

以上是递归方法求解最优钢条切割问题。
使用动态规划实现:
书上提到了两种方法:
1.带备忘的自顶向下法(发现这个就是记忆化
2.自底向上法
两种方法具有相同的渐进运行时间,但是由于自底向上没有频繁调用递归的开销,所以要更快一点。
代码:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXN 100000 //这里假设n最大为100000,视情况改变MAXN 
int a[MAXN];//记录结果 
int p[MAXN];//记录i长度的钢条价格 
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>p[i];//读入数据 
	}
	a[0]=0;
	for(int i=1;i<=n;i++){//当前钢条长度 
		a[i]=0;
		for(int j=1;j<=i;j++){//枚举分割方式 
			a[i]=max(a[i],p[j]+a[i-j]);//动态规划递推求解 
		}
		//cout<<"res of "<<i<<" : "<<a[i]<<endl;//调试 
	}
	cout<<a[n];
}

同样通过输出中间值,我们可以得到与递归同样的结果。
通过添加计时器,可以比较一下上面几种方法的时间。
(如果你不知道计时器,可以看这个帖子:https://blog.csdn.net/qq_40794973/article/details/81607896?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task )

子问题图

对于一个给定的子问题x,在求解它之前徐要求解邻接至它的子问题。邻接关系不一定是对称的。这种自底向上的动态规划算法是按“拓扑排序”或者“反序的拓扑排序”顺序来处理途中的顶点。
即对于一个问题,只有解决了它所依赖的所有子问题,才可以求解它。
而递归求解,则是一种类似dfs深度优先搜索的方法。

对于子问题图G=(V,E),我们该如何确定它的运行时间呢?
总时间等于每个子问题求解时间的总和(因为每个子问题之解决一次
对于通常的子问题,求解它的时间等于每个子问题顶点的出度(即有几条以它为起点的边
因此,通常情况下,动态规划的求解时间与其子问题图的顶点和边数量呈线性关系。

重构解
以刚刚的钢条切割为例,如果要返回最优的切割方案,我们该怎么做?
这时候我们就应该另外去保存对应的切割方案

即对长度j的钢条不仅计算其最大收益rj,也储存最优解的第一段钢条的长度sj

代码如下:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
#define MAXN 100000 //这里假设n最大为100000,视情况改变MAXN 
int a[MAXN];//记录结果 
int p[MAXN];//记录i长度的钢条价格 
int s[MAXN]={0};
void print_method(int x){//输出切割方案
	while(x!=0){
		cout<<s[x]<<' ';
		x-=s[x];
	}
	cout<<endl;
} 
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>p[i];//读入数据
	}
	a[0]=0;
	for(int i=1;i<=n;i++){//当前钢条长度 
		a[i]=0;
		for(int j=1;j<=i;j++){//枚举分割方式 
			if(a[i]<p[j]+a[i-j]){
				s[i]=j;//记录解的分割方案 
				a[i]=p[j]+a[i-j];
			}//动态规划递推求解 
		}
	}
	for(int i=1;i<=n;i++){
		cout<<"res of "<<i<<" : ";
		cout<<a[i]<<endl;
		print_method(i);	
	}
}

样例运行结果:
样例的结果
这是算法导论的:
算法导论
其实有点让我想到之前最短路问题Floyd法的记录路径方案:
即使用一个path数组来记录路径整体相对现在还没输出的第二个点。
在读入的时候

scanf("%d%d%d",&a,&b,&dist);
d[a][b] = dist;//储存原始边长度
path[a][b] = b;//对于原始的路径,从a到b的第二个点正好就是b

在更新d[i][j]时

if(d[i][k]!=-1&&d[k][j]!=-1&&d[i][k]+d[k][j]<d[i][j]){//松弛操作
	d[i][j] = d[i][k] + d[k][j];
	path[i][j] = path[i][k];
}

这步有点奇妙,两端路线合并时,相对于合并后总路径的第二个点就是前面一段路径的第二个点。

在输出时:

while(s!=e){
	printf("%d ->",s);
	s=path[s][e];
}
printf("%d",e);

仔细一想,当输出当前这个点s之后,现在剩余还没输出的路径就是去掉s之后剩下来的路径。即把起点s改成路径的第二个点就行了。
当然,最后也别忘了输出e

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值