P4138
[JOISC2014] 挂饰
题目描述
JOI君有N个装在手机上的挂饰,编号为1...N。 JOI君可以将其中的一些装在手机上。
JOI君的挂饰有一些与众不同——其中的一些挂饰附有可以挂其他挂件的挂钩。每个挂件要么直接挂在手机上,要么挂在其他挂件的挂钩上。直接挂在手机上的挂件最多有1个。
此外,每个挂件有一个安装时会获得的喜悦值,用一个整数来表示。如果JOI君很讨厌某个挂饰,那么这个挂饰的喜悦值就是一个负数。
JOI君想要最大化所有挂饰的喜悦值之和。注意不必要将所有的挂钩都挂上挂饰,而且一个都不挂也是可以的。
\(1\leq N\leq 2000\)
\(0\leq Ai\leq N(1\leq i\leq N)\)
\(-10^6\leq Bi\leq 10^6(1\leq i\leq N)\)
大概是用到了一点构造思想的简单 dp?
挂饰可以看成一棵树,针对树,仍然从“菊花图/链/拓扑序”来下手。
菊花图不必多说:假定一开始手机上就有一个挂钩即可。接下来考虑菊花图下多出来菊花,很显然可以将这个小菊花合并到大菊花上面去。由此可以发现:我们不关心树的形态,父子关系,仅仅关心“往下可以接多少点”和“权和”。也许可以看成是“对于要构造信息的简化”?
接一个挂钩,会让可挂的数量加上 \(A_i - 1\),同时权加上 \(B_i\)。如果开始默认挂钩 \(0\) 个最后只需要 \(A_i - 1\) 和大于等于 \(-1\)(因为一开始还有一个挂钩没有计算进去)。这就是简单的 01 背包了。
然而还有一个小细节:\((A_i-1)\) 总和很大!不过这没关系,当 \((A_i - 1)\) 在某个阶段的和达到了 \(N\) 以后再变大其实也没有用了,因为此后无论如何都不会使得 \((A_i-1)\) 和小于 \(-1\)。将这些状态合并即可。相应的,实现中利用刷表法更容易。类似的细节处理出现在飞扬的小鸟中。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2000, V = 4000;
const ll inf = 1e10;
int n;
ll f[N + 10][V + 10], A[N + 10], B[N + 10];
int main() {
cin >> n;
for(int i = 1; i <= n; i++)
cin >> A[i] >> B[i], A[i]--;
for(int i = 0; i <= n; i++)
for(int j = 0; j <= V; j++)
f[i][j] = -inf;
f[0][N] = 0;
ll maxn = -inf;
for(int i = 0; i < n; i++) {
for(int j = 0; j <= V; j++)
f[i + 1][j] = f[i][j];
for(int j = 0; j <= V; j++) {
int T = min((int)j + (int)A[i + 1], V);
if(T < 0) continue;
f[i + 1][T] = max(f[i + 1][T], f[i][j] + B[i + 1]);
}
for(int j = 0; j <= V; j++)
if(j >= N - 1)
maxn = max(maxn, f[i][j]);
}
for(int j = 0; j <= V; j++)
if(j >= N - 1)
maxn = max(maxn, f[n][j]);
cout << maxn << endl;
}