Programming Challenges 习题11.6.5

PC/UVa:111105/10003

Cutting Sticks

一根长度为L的木棍,要在N个点进行切割,每次切割的费用和切割前的长度相同,求最小的切割费用。例如长度为10的木棍,分别在4578处切割,切割顺序为4 5 7 8时,费用为10 + 6 + 5 + 3 = 24,切割顺序为5 4 7 8时,费用为10 + 5 + 5 + 3 = 23

在这里插入图片描述

用上一篇中总结的动态规划套路来分析。这道题目分阶段决策的步骤已经很明显了,所以不需要再次进行转化了,直接从第二步开始,寻找递推关系。

找递推关系要满足最优子结构和无后效性两个性质。假如现在已经切了i个点,接下来要切第i + 1个点。直观的看,只要已经切割的点是确定的,那么在切割下一个点时,剩下的木棍就是一样的,所以后续的切割费用和前面的切割顺序没有关系,所以这满足无后效性。例如已经切割了47两个点,现在要切5,如果是4 7的顺序,那么5这个点的费用是3,如果是7 4的顺序,5这个点的费用依然是3,两种方法的区别在于已切割点所产生的费用不一样,但是对于5这次切割没有影响,所以可以将4 7作为整体看待

第三步,合并重叠子问题。假设切割3次后已经切割了4 5 7,那么这一次切割的可能是4,也可能是5,也可能是7,对应前两次的顺序可能是5 77 54 77 44 55 4,一共6种。无论怎么样,最后都会得到4 5 7,所以只要在能得到4 5 7的这6种切割顺序中,保留一个费用最小的即可。

这个思路虽然可行,但是实现时开销很大。每一个点有切和没切两种状态,所以可以用1比特表示,但是最多可以切50刀,那也就是一共2 ^ 50这么多状态,全部存下来不太可能,需要换一种表示方法,即使用该切割点所划分的木棍来表示,这也就是网上可以找到的记忆化搜索的算法。这种方式是将一个大问题,分解成若干个小问题,即每切一次之后变成了两个小问题,然后再进行合并,典型的分治法,故而采用递归的方法,代码可以参考这里

上面的思路是在整理博客的过程中才发现可行的,代码也是引用的其他博主的,下面是我自己写的。

依旧是动态规划的思路,但是这一次不是采用切割的方式,而是采用合并的方式。假设现在已经合并了i个切割点,接下来要合并第i + 1个切割点。同样的,只要已经合并的点是确定的,无论以什么样的顺序合并,那么目前所得的木棍就是一样的,所以这就保证了最优子结构和无后效性。对于重叠子问题的处理,也是只保留合并了相同切割点时的最小费用。

但是这方法依旧面临上面的同样的问题,状态空间太大,无法表示,但是可以使用已经合并得到的长木棍的最小费用来表示,即Cost[i][j]表示合并得到端点ij所表示的木棍的最小费用,这个和已经合并的点有着一一对应的关系。这样除了切割点外,还需要0点和L点来表示整根木棍。

cutStick()是上述动态规划方法的直接体现,N个切割点总共需要合并N次,每一次都要尝试合并所有的切割点,合并时还要考虑如何组成一个更长的木棍。当前的切割点除了能合并两根最短的木棍外,根据已经合并的次数,还可以向左或者向右多合并几根,但是总量不能超过当前的合并次数。例如当切割点4作为第3个合并点时:

  • 除了可以得到[0, 5]这个结果(其实在将4作为第一个点合并时已经计算过了)
  • 还可以向右多延伸一段,把[5, 7]这根木棍也合并进来,得到[0, 7]最小费用(其实在将4 5作为前两次的合并点时也已经计算过了)
  • 当然最需要计算的结果是向右延伸两段,根据[4, 8]这根木棍的最小费用,得到[0, 8]的最小费用(但其实不一定是最小值,也许在将5或者7作为第3次的合并点时会更小)

上面的方法其实是在第curr次合并时,会将由curr + 1段(因为合并一次是将两根木棍合并)短木棍组成的长木棍的最小费用都计算出来。我最开始写的代码中还会判断[begin, pos][pos, end]所表示的木棍有没有被合并,在整理博客时发现已经不需要判断了。cutStick()在UVa的运行时间1.08

既然算法的道理已经非常明了了,那么可以将循环简化,依次将合并得到所有段数为2N + 1的木棍的最小费用计算出来,也就是cutStick2(),UVa运行时间0.07

#include <iostream>
#include <vector>
#include <climits>

using namespace std;

void cutStick(const vector<int> &viPos)
{
	size_t allPos = viPos.size();
	vector<vector<int>> Cost(allPos - 1, vector<int>(allPos, INT_MAX));
	for (size_t idx = 0; idx < viPos.size() - 1; idx++)
	{
		Cost[idx][idx + 1] = 0;
	}
	int cost;
	//合并的次数是切的次数
	for (size_t curr = 1; curr <= viPos.size() - 2; curr++)
	{
		//尝试合并每一点
		for (size_t pos = 1; pos < viPos.size() - 1; pos++)
		{
			for (size_t left = 0; left < curr; left++)
			{
				size_t begin = pos - 1;
				if (begin >= left){
					begin -= left;
					for (size_t right = 0; right + left < curr; right++)
					{
						size_t end = pos + 1;
						if (end + right < viPos.size()){
							end += right;
							cost = Cost[begin][pos] + Cost[pos][end] + viPos[end] - viPos[begin];
							if (cost < Cost[begin][end]){
								Cost[begin][end] = cost;
							}
						}
					}
				}
			}
		}
	}
	cout << "The minimum cutting is ";
	cout << Cost[0][viPos.size() - 1];
	cout << '.' << endl;
	return;
}

void cutStick2(const vector<int> &viPos)
{
	size_t allPos = viPos.size();
	vector<vector<int>> Cost(allPos - 1, vector<int>(allPos, INT_MAX));
	for (size_t idx = 0; idx < viPos.size() - 1; idx++)
	{
		Cost[idx][idx + 1] = 0;
	}
	int cost;
	for (size_t len = 2; len < viPos.size(); len++)
	{
		for (size_t begin = 0; begin + len < viPos.size(); begin++)
		{
			size_t end = begin + len;
			for (size_t pos = begin + 1; pos < end; pos++)
			{
				cost = Cost[begin][pos] + Cost[pos][end] + viPos[end] - viPos[begin];
				if (cost < Cost[begin][end]){
					Cost[begin][end] = cost;
				}
			}
		}
	}
	cout << "The minimum cutting is ";
	cout << Cost[0][viPos.size() - 1];
	cout << '.' << endl;
}

int main()
{
	int L, N;
	while (cin >> L){
		if (L == 0) break;
		cin >> N;
		int pos;
		vector<int> viPos;
		viPos.push_back(0);
		for (int n = 0; n < N; n++)
		{
			cin >> pos;
			viPos.push_back(pos);
		}
		viPos.push_back(L);
		cutStick2(viPos);
	}
	return 0;
}
/*
100
3
25 50 75
10
4
4 5 7 8
*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值