QOJ #14579 [集训队互测] 火花 题解
QOJ #14579 [集训队互测] 火花 题解
QOJ upvote 榜第三名的 dp 好题 ,看完题解令人叹为观止
题目大意
给定一棵 \(n\) 个点、根为 1 的树。第 \(i\) 个点上有 \(c_i\) 个物品,权值均为 \(val_i\) 。要求先选择恰好 \(t\) 个不同的点,对于选的每个点,在它的每个祖先(包括自己)上各取一个物品。(本题保证 \(t< \min_{i = 1}^n{c_i}\) ,因此不会存在没有物品可取的情况)。接下来可以取至多 \(m\) 个物品,要求所有在这一步被取了物品的点形成一个含根的连通块。最大化整个过程中取的物品权值之和。
思路
首先考虑 \(t = 0\) 的情况,发现问题本质上变成一个经典树上依赖背包:树上的依赖关系体现为,一个节点不选,则其子树全不选。处理这类问题的一个经典方式是按照 \(dfn\) 序 \(dp\) ,状态定义为 \(dp_{i,\ j}\) 表示 \(dfn\) 序上前 \(i\) 个结点选了 \(j\) 个物品,一棵子树不选即 \(dfn_u \to dfn_u + siz_u\) 。除此之外,转移简单。
那么,\(t>0\) 呢?
先约定第一步选定的 \(t\) 个点为关键点,\(h_u\) 为 \(u\) 的子树内关键点的个数,\(s_u\) 为 \(1\to u\) 路径上的点权和。
容易发现若只有第一步是简单的,直接暴力选取前 \(t\) 大的即可。难点在第二步。故考虑第一步对第二步的影响。此时,每个点可选择的物品的数量变为 \(c_u-h_u\) ,不难想到把 \(h_u\) 记录在状态中。但,哪怕暴力的转移,似乎都有点困难?(至少本人不会) 于是我们需要天马行空一会儿,然后惊奇地发现,由于一棵子树在 \(dfn\) 序上是连续的,我们可以用差分的方式表示出 \(h_u\) ,即 \(h_u=suf-pre\) ,于是 \(c_u-h_u\) 变成 \(c_u-(suf-pre)=pre+(c_u-suf)\) 。由题,\(suf \le t < c_u \to c_u-suf > 0\) ,且 \(pre \ge 0\) ,故我们可以在状态中记录前缀关键点的个数,进入子树时选取物品范围为 \([0,\ pre]\) ,离开子树时选取物品范围为 \([1,\ c_u - suf]\) (因为从 \(c_u-suf > 0\) ,所以在离开子树时强制选择)。于是我们自然地想到把 \(dfn\) 序变成括号序(不知道为什么,网上大部分题解不是说 \(dfs\) 序就是说欧拉序,我寻思这也能搞混?),定义 \(e_i\) 表示括号序第 \(i\) 个位置表示原树上第 \(e_i\) 个结点,结点 \(u\) 在进入、离开时在括号序上的位置分别是 \(el_u,\ er_u\) 。
考虑 \(dp\) ,我们定义 \(dp_{i,\ j,\ k}\) 表示括号序前 \(i\) 个结点选了 \(j\) 个关键点,第二类操作选了 \(k\) 个物品。
考虑转移之前,我们先预测一下朴素转移多半是 \(\mathcal{O}(m)\) 或者 \(\mathcal{O}(t)\) 的,需要优化,故填表更方便,于是用填表来写式子:
于是我们果然得到了 \(\mathcal{O}(nm^2t+nmt^2)\) 的做法。考虑优化。对于 \(\mathcal{O}(nm^2t)\) 的部分,优化是显然的,直接上经典的单调队列优化背包即可(甚至本题都不需要余数分组,因为重量都是 1);对于 \(\mathcal{O}(nmt^2)\) 的部分,我们仔细观察一下,发现 \(f_{u,\ i}\) 是一个上凸函数,所以这部分转移的转移点具有单调性。上分支优化 \(dp\) 即可。
于是时间复杂度都是 \(\mathcal{O}(nmt\log n)\) ,空间复杂度 \(\mathcal{O}(nmt)\) 。
Code
#include<bits/stdc++.h>
#define LL long long
#define PLI pair<LL, int>
#define PIL pair<int, LL>
#define PLL pair<LL, LL>
#define PII pair<int, int>
using namespace std;
const int N = 1e4;
const LL inf = 1e18;
int n, ecnt, m, t, fa[N + 5], siz[N + 5], el[N + 5], er[N + 5], e[N * 2 + 5];
int q[N + 5], l, r;
LL c[N + 5], val[N + 5], s[N + 5], tmp[N + 5];
inline void trans(LL& x, const LL y) { x = (x >= y ? x : y); }
vector<int> g[N + 5];
vector<vector<LL> > f;
vector<vector<vector<LL> > > dp;
void dfs0(int u = 1) {
el[u] = ++ecnt;
e[ecnt] = u;
s[u] = s[fa[u]] + val[u];
siz[u] = 1;
f[u][0] = 0;
for (auto v : g[u]) {
dfs0(v);
fill(tmp + 1, tmp + t + 1, -inf);
for (int j = min(siz[v], t); j >= 0; j--)
for (int k = min(t - j, siz[u]); k >= 0; k--) trans(tmp[j + k], f[u][k] + f[v][j]);
siz[u] += siz[v];
for (int j = 0; j <= min(t, siz[u]); j++) f[u][j] = tmp[j];
}
for (int i = t; i >= 1; --i) trans(f[u][i], f[u][i - 1] + s[u]);
er[u] = ++ecnt;
e[ecnt] = u;
}
inline void knapsack(const vector<LL>& a, vector<LL>& b, const int dw, const int up, const LL v, const LL ad = 0) {
l = 1; r = 0;
for (int i = dw; i <= m; i++) {
while (r - l + 1 >= 1 && (i - q[l]) > up) l++;
while (r - l + 1 >= 1 && a[q[r]] + (i - q[r]) * v <= a[i - dw] + dw * v) r--;
q[++r] = i - dw;
trans(b[i], a[q[l]] + (i - q[l]) * v + ad);
}
}
void dfs(const int d, const int k, const int l, const int r, const int optl, const int optr) {
if (l > r) return;
int mid = (l + r) >> 1, opt = -1;
for (int i = optl; i <= min(mid, optr); i++) {
LL v = dp[el[e[d]] - 1][i][k] + f[e[d]][mid - i];
if (opt == -1 || v > dp[el[e[d]] - 1][opt][k] + f[e[d]][mid - opt]) {
trans(dp[d][mid][k], v);
opt = i;
}
}
dfs(d, k, l, mid - 1, optl, opt);
dfs(d, k, mid + 1, r, opt, optr);
}
inline void solve() {
cin >> n >> m >> t; f.resize(n + 5, vector<LL>(t + 5, -inf));
dp.resize(n * 2 + 5, vector<vector<LL> >(t + 5, vector<LL>(m + 5, -inf)));
for (int i = 1; i <= n; i++) cin >> c[i] >> val[i];
for (int i = 2; i <= n; i++) cin >> fa[i], g[fa[i]].push_back(i);
dfs0(); dp[0][0][0] = 0;
for (int i = 1; i <= 2 * n; i++) {
for (int j = 0; j <= t; j++) {
int up = (i == el[e[i]] ? j : c[e[i]] - j);
knapsack(dp[i - 1][j], dp[i][j], (i == er[e[i]]), up, val[e[i]]);
if (i == er[e[i]] && j) knapsack(dp[i - 1][j - 1], dp[i][j], 1, up, val[e[i]], s[e[i]]);
}
if (i == er[e[i]]) for (int k = 0; k <= m; k++) dfs(i, k, 0, t, 0, t);
}
LL ans = 0;
for (int k = 0; k <= m; k++) trans(ans, dp[2 * n][t][k]);
cout << ans << "\n";
}
signed main() {
freopen("spark.in", "r", stdin);
freopen("spark.out", "w", stdout);
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
int tt = 1;
//cin >> tt;
while (tt--) solve();
return 0;
}
浙公网安备 33010602011771号