PC/UVa:111105/10003
一根长度为L
的木棍,要在N
个点进行切割,每次切割的费用和切割前的长度相同,求最小的切割费用。例如长度为10
的木棍,分别在4
、5
、7
、8
处切割,切割顺序为4 5 7 8
时,费用为10 + 6 + 5 + 3 = 24
,切割顺序为5 4 7 8
时,费用为10 + 5 + 5 + 3 = 23
。
用上一篇中总结的动态规划套路来分析。这道题目分阶段决策的步骤已经很明显了,所以不需要再次进行转化了,直接从第二步开始,寻找递推关系。
找递推关系要满足最优子结构和无后效性两个性质。假如现在已经切了i
个点,接下来要切第i + 1
个点。直观的看,只要已经切割的点是确定的,那么在切割下一个点时,剩下的木棍就是一样的,所以后续的切割费用和前面的切割顺序没有关系,所以这满足无后效性。例如已经切割了4
和7
两个点,现在要切5
,如果是4 7
的顺序,那么5
这个点的费用是3
,如果是7 4
的顺序,5
这个点的费用依然是3
,两种方法的区别在于已切割点所产生的费用不一样,但是对于5
这次切割没有影响,所以可以将4 7
作为整体看待。
第三步,合并重叠子问题。假设切割3
次后已经切割了4 5 7
,那么这一次切割的可能是4
,也可能是5
,也可能是7
,对应前两次的顺序可能是5 7
、7 5
、4 7
、7 4
、4 5
、5 4
,一共6
种。无论怎么样,最后都会得到4 5 7
,所以只要在能得到4 5 7
的这6
种切割顺序中,保留一个费用最小的即可。
这个思路虽然可行,但是实现时开销很大。每一个点有切和没切两种状态,所以可以用1
比特表示,但是最多可以切50
刀,那也就是一共2 ^ 50
这么多状态,全部存下来不太可能,需要换一种表示方法,即使用该切割点所划分的木棍来表示,这也就是网上可以找到的记忆化搜索的算法。这种方式是将一个大问题,分解成若干个小问题,即每切一次之后变成了两个小问题,然后再进行合并,典型的分治法,故而采用递归的方法,代码可以参考这里。
上面的思路是在整理博客的过程中才发现可行的,代码也是引用的其他博主的,下面是我自己写的。
依旧是动态规划的思路,但是这一次不是采用切割的方式,而是采用合并的方式。假设现在已经合并了i
个切割点,接下来要合并第i + 1
个切割点。同样的,只要已经合并的点是确定的,无论以什么样的顺序合并,那么目前所得的木棍就是一样的,所以这就保证了最优子结构和无后效性。对于重叠子问题的处理,也是只保留合并了相同切割点时的最小费用。
但是这方法依旧面临上面的同样的问题,状态空间太大,无法表示,但是可以使用已经合并得到的长木棍的最小费用来表示,即Cost[i][j]
表示合并得到端点i
和j
所表示的木棍的最小费用,这个和已经合并的点有着一一对应的关系。这样除了切割点外,还需要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
。
既然算法的道理已经非常明了了,那么可以将循环简化,依次将合并得到所有段数为2
到N + 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
*/