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)\) 的,需要优化,故填表更方便,于是用填表来写式子:

\[\begin{align} \text{if}\ \ &i=el_{e_i} \\ &dp_{i,\ j,\ k} = \max_{x = 0}^{min(k,\ j)}\{dp_{i - 1,\ j,\ k - x} + x \times v_{e_i} \} \\ \\ \text{if}\ \ &i=er_{e_i} \\ &跳过这棵子树:\\ &dp_{i,\ j,\ k} = \max_{x = 0}^{min(j,\ siz_{e_i})}\{dp_{el_{e_i} - 1,\ j - x,\ k} + f_{e_i,\ x} \} \\ &其中,f_{u,\ i}\ 表示在\ u\ 的子树内选\ s_v\ 最大的\ i\ 个结点为关键点的权值和\\ &(可以使用贪心转移,不过感觉还没有树上背包打得简单)\\ \\ &不跳过这棵子树,且不选择\ u\ 为关键点:\\ &dp_{i,\ j,\ k} = \max_{x = 1}^{min(k,\ c_{e_i}-j)}\{dp_{i - 1,\ j,\ k - x} + x \times v_{e_i} \} \\ \\ &不跳过这棵子树,且选择\ u\ 为关键点:\\ &dp_{i,\ j,\ k} = \max_{x = 1}^{min(k,\ c_{e_i}-j)}\{dp_{i - 1,\ j - 1,\ k - x} + x \times v_{e_i} + s_{e_i} \} \\ \end{align} \]

于是我们果然得到了 \(\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;
}
posted @ 2026-11-13 21:58  千千的咲  阅读(2)  评论(0)    收藏  举报