[USACO 2012 Open Gold] Bookshelf【优化dp】

传送门1:http://www.usaco.org/index.php?page=viewproblem2&cpid=138

传送门2:http://www.lydsy.com/JudgeOnline/problem.php?id=2678

最开始没看到要将那些书按顺序放!!一定是按顺序!我还以为是自己安排那个书架,白费了我好久时间。

然而看对题之后仍然不会。。。

令f(i)表示前i本书已经放到书架上,且第i本是某个书架的最后一本的最小高度和,则

f(i) = min { f(j) + max { h[j + 1], h[j + 2]....., h[i] } }, 其中w[i + 1] + w[i + 2] + ... + w[j] <= L.

裸的是O(N^2)的(好像Silver版本的这一题就是O(N^2)可以过),所以需要优化。(又是dp优化!每次看dp优化少说都要2个钟头,烦)

显然,f(j)随j的增大而增大(非严格增大,即大于等于),而对于不变的i,max { h[j + 1], h[j + 2]....., h[i] } 随j的增大而减小(非严格减小,即小于等于),因此我们可以把i前面的所有j,按照max { h[j + 1], h[j + 2]....., h[i] }的大小来分块,把这个值相等的j分到一块去,那么对于这一块j,显然取最前面那个最好,因为根据刚刚所说,f(j)随j的增大而增大(非严格增大,即大于等于)。然而有很多块,那么应该取那一块的最前面那个呢?我们应该用一个平衡树(multiset,就是STL set的可以重复元素版本)把所有可以取的值存起来,并在dp过程中不停地删除那些不能取的值,并添加新的可以取的值。具体见代码。

#include <cstdio>
#include <set>

const int maxn = 100005;

int n, ttttttttt[maxn], num, *tail = ttttttttt;
long long L, s[maxn], h[maxn], f[maxn];
std::multiset<long long> val;

int main(void) {
	freopen("bookshelf.in", "r", stdin);
	freopen("bookshelf.out", "w", stdout);
	scanf("%d%I64d", &n, &L);
	for (int i = 1; i <= n; ++i) {
		scanf("%I64d%I64d", h + i, s + i);
		s[i] += s[i - 1];
	}
	
	// num表示现在拥有块的个数, tail[i]表示第i个块的结尾位置,
	// 注意tail是一个指针,这是有方便之处的,之后就会看到 
	for (int i = 1; i <= n; ++i) {
		// 下面是将第i本书考虑进去后,重新更新分块 
		while (num && h[i] >= h[tail[num]]) {
			val.erase(val.find(f[tail[num - 1]] + h[tail[num]]));
			--num;
		}
		tail[++num] = i;
		val.insert(f[tail[num - 1]] + h[i]);
		
		// tail[0]应该表示的是合法的j使得前缀和s[i] - s[j] <= L
		// 当 > L时,就应该erase掉对应的值,然后++tail[0],但如果
		// 下一个就是这个块的结尾了,就要指针移动啦,因为这个块整
		// 个都被删除了,然后再--num 
		while (s[i] - s[tail[0]] > L) {
			val.erase(val.find(f[tail[0]] + h[tail[1]]));
			if (tail[0] + 1 == tail[1]) {
				++tail;
				--num;
			}
			else {
				val.insert(f[tail[0] + 1] + h[tail[1]]);
				++tail[0];
			}
		}
		
		f[i] = *val.begin();
	}
	printf("%I64d
", f[n]);
	return 0;
}

官方题解是记录了一个块有多大,我是记录了这个快的结尾位置,都一样。

原文地址:https://www.cnblogs.com/ciao-sora/p/6054559.html