[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\n", f[n]); return 0; }
官方题解是记录了一个块有多大,我是记录了这个快的结尾位置,都一样。