算法导论——钢条切割问题(C语言)

《算法导论》在对动态规划讲解时,第一个问题就是钢条切割的问题。
在书中,对这个问题提供了3种思路

  1. 利用普通的递归来解,时间复杂度为O(2^n)
  2. 对普通的递归进行优化,使其带有记忆功能,减少运行时间。即自顶向下的动态规划
  3. 运用数组,自底向上的动态规划

现在依次讲解这三个思路,并且用代码实现它们!

首先先看题

各种长度的钢条能买多少钱的对照表
现在给你一根钢条,你可以把它切成几个小部分(也可以不用切割),要找到一种方法,使得这根钢条可以卖出最多的价钱。

如:长度为4的钢条有三种卖法

  1. 不切割直接买,可以买价格9
  2. 切割为1,3再买,同样是价格9
  3. 切割为2,2再买,价格为10

可以看出,把4分为2,2来买可以达到最大收益。

普通递归方法

在上面的例子中,一根钢条可以被分为很多份,可以在被分的这些小份中继续分解,来找到最优解。这和递归的思路非常相似——将一个大问题分解子问题来求解。
首先假设一个问题的子问题的解都是最优的,于是,对于一个长度为L的钢条,若把他分为两段,L的最优解可能为 k,L-k (k=0,1,2,…,L-1)
现在遍历k的值,就可以找出最大的解(默认k,L-k为这个长度的最优解)
可以知道,虽然现在不知道k,L-k 是否为最优解,但是整个过程是递归的,在递归计算中可以知道k,L-k的最优解。

普通递归代码实现
创建两个大小为11的数组来储存这些钢条能卖出的价格,和当前钢条能买到的最大价格

int N[11]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};

编写递归函数

int re(int n)
{
	if(n==0)  //钢条长度为零的时候,返回零
		return 0;
	int q=N[n];
	int i;
	for(i=1;i<n;i++)  //遍历k~L-k每一种情况,找到里面的最大值,k为零时为不切割的情况,q=N[n]已经储存,不需要考虑
		q=maxx(q,r[i]+re(n-i));
	r[n]=q; //钢条长度为n时最多可以买到价格q
	return q;
}

完整代码

#include<stdio.h>
#include<time.h>
//int N[44]={0,1,5,8,9,10,17,17,20,24,30,0,1,5,8,9,10,17,17,20,24,30,0,1,5,8,9,10,17,17,20,24,30,0,1,5,8,9,10,17,17,20,24,30};
int N[11]={0,1,5,8,9,10,17,17,20,24,30};
int r[11]={0};
int re(int);
int maxx(int ,int);
int main()
{
	int current;
	puts("输入你要裁剪的钢条长度:");
	scanf("%d",&current);
	clock_t start, finish;
	start=clock();
	int i;
	printf("%d长的钢条最大收益是:%d\n",current,re(current));
	printf("各个长度的收益情况:\n长度:");
	for(i=0;i<=current;i++)
	{
		printf("%3d ",i);
	}
	puts("");
	printf("收益:");
	for(i=0;i<=current;i++)
	{
		printf("%3d ",r[i]);
	}
	puts("");
	finish=clock();
	double Total_time = (double)(finish-start) / CLOCKS_PER_SEC;
	printf("%f 秒\n",Total_time);
	scanf("%d",&current);
}
int re(int n)
{
	if(n==0)
		return 0;
	int q=N[n];
	int i;
	for(i=1;i<n;i++)
		q=maxx(q,r[i]+re(n-i));
	r[n]=q;
	return q;
}
int maxx(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}

可以扩大数组N的大小,使其可以测试更多的钢条。
我测试了一下他们的时间

  1. n为31时运行时间为5s
  2. n为32时运行时间为10s
  3. n为33时运行时间为20s

从这里也可以看出,普通递归的时间复杂度为O(2^n),是指数的。

自顶向下的动态规划法

观察普通的递归方法可以知道,创建的数组r除了最后打印收益结果时发挥了作用,并没有发挥其他的功能。并且仔细调试普通递归方法可以发现,它重复多次的计算了许多子问题。比如当n=4时,普通递归运行的方式是

0被调用了8次!1被调用了4次
图片来源:https://zhuanlan.zhihu.com/p/70763958
重复的计算这些子问题是浪费时间的,并且随着钢条长度的增加,这些时间会指数增长。
现在,让数组r来储存这些子问题的数据,不在重复计算子问题。

修改后的递归代码

int upToDown(int n) //每次返回的值是当前长度n的最大收益 
{
	if(r[n]!=0)
		return r[n];
		
	if(n==0)
		return 0;

	int q=N[n];	
	int i;
	for(i=1;i<n;i++)
		q=maxx(q,r[i]+upToDown(n-i));
	r[n]=q;
	return q;
}

所添加的代码

if(r[n]!=0)
		return r[n];

由于r的初始值为零,r[n]不为零的时候,就代表子问题n已经被处理过,r[n]就是钢条长度的最大收益,可以直接返回,不需要继续递归
完整代码

#include<stdio.h>
int N[11]={0,1,5,8,9,10,17,17,20,24,30};
int upToDown(int);
int r[11]={0};
int maxx(int a,int b);
int main()
{
	int current;
	puts("输入你要裁剪的钢条长度:");
	scanf("%d",&current);
	int i;
	printf("%d长的钢条最大收益是:%d\n",current,upToDown(current));
	printf("各个长度的收益情况:\n长度:");
	for(i=0;i<=current;i++)
	{
		printf("%3d ",i);
	}
	puts("");
	printf("收益:");
	for(i=0;i<=current;i++)
	{
		printf("%3d ",r[i]);
	}
} 
int upToDown(int n) //即每次返回的值是当前长度n的最大收益 
{
	if(r[n]!=0)
		return r[n];
		
	if(n==0)
		return 0;

	int q=N[n];	
	int i;
	for(i=1;i<n;i++)
		q=maxx(q,r[i]+upToDown(n-i));
	r[n]=q;
	return q;
}
int maxx(int a,int b)
{
	if(a>=b)
		return a;
	else
		return b;
}

个人看法,若要求长度n的最大收益,就必须知道他的子问题的收益,如子问题收益已知就直接返回,如果不知道,就继续求解子问题的子问题。从这个思路可以看出,只有知道了子问题才可以求出当前问题,所以虽然是从n开始递归,但最终都是求出了最基本的子问题之后,再开始往上求解上一级的子问题。所以自顶向下也是有一个自底向上的过程。

自底向上的动态规划

由于最终都是要找到最底层,最基本的子问题,所以干脆直接从最底层开始自底向上的求出最终解。
同样创建两个大小为11的数组来储存这些钢条能卖出的价格,和当前钢条能买到的最大价格

int N[11]={0,1,5,8,9,10,17,17,20,24,30};
int n[11]={0};

动态规划的核心是保存已计算的子问题的值,并且是自底向上的动态规划,我们选用嵌套的两个for循环,从长度1开始依次计算各个长度的最优解。
核心代码:

	puts("输入你要裁剪的钢条长度:");
	scanf("%d",&current);
	for(i=1;i<=current;i++)
	{
		int q=N[i];
		for(j=1;j<=i;j++)
		{
			q=maxx(q,n[j]+n[i-j]);
		}
		n[i]=q;
	}

内层循环

for(j=1;j<=i;j++)
{
	q=maxx(q,n[j]+n[i-j]);
}

每一次内层循环之后,长度为i的最优解就已知了。

在钢条长度为i时,遍历i的各种切割方法,来求出长度i的最优解
仔细思考内外层循环可知

  1. i=1时,内层循环运行1次
  2. i=2时,内层循环运行2次
  3. i=3时,内层循环运行3次

i为n时,内层循环运行n次。
所以总的迭代次数构成一个公差为1的等差数列,显然时间复杂度为O(n^2).

总结

最终结果

利用动态规划,完成了时间复杂度从指数到常数时间的优化,足以看出动态规划的巨大威力,能用动态规划运用于求解有重复子问题的情况,动态规划不会重复计算子问题,从而大大节约了时间。

摘一段《算法导论》中的话

我通常按照4个步骤来设计一个动态规划算法
1.刻画一个最优解的特征
2.递归的定义最优值
3.计算最优值,通常采用自底向上的方法

从第2点可以看出,动态规划和递归之间有着千丝万缕的联系,动态规划可以利用递归的思路非递归的来解决问题,从而到优化时间的目的!

  • 6
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值