[模板]树上背包|[CTSC1997] 选课 题解

[CTSC1997] 选课 题解

题目链接:洛谷 | LOJ

(符号约定:\(n\) 课程总数,\(m\) 选择的课程门数)

树上背包典题,值得深入分析。

图论建模

可以把题目中的选课关系抽象成一棵有根树森林:每个节点代表一门课程,节点的父亲代表这门课程的先修课。为了避免对多个连通块操作带来的不便,可以新增一个虚拟的 \(0\) 号节点作为原森林中所有根节点的父节点,点权为 \(0\)。由于我们一定会选择 \(0\) 号节点,所以要使 \(m \leftarrow m + 1\)。于是,我们的目标就是:在树上选择一个包含根节点且大小为 \(m\)​​ 的连通块,使得连通块中的点权之和最大

dp 的初步实现

\(f_{u, i}\) 表示在 \(T(u)\) 中选择 \(i\) 个节点的最大收益。我们想从 \(u\) 的子节点转移过来。由于一定会选择 \(i\) 节点,所以要在 \(u\) 的子节点的子树中选择 \(i-1\) 个节点。

\(u\) 的子节点的个数 \(|son(u)| = l\),子节点的集合 \(son(u) = \{v_1, v_2, \cdots, v_l\}\)。假设我们从 \(u\) 的第 \(j\) 个子节点的子树中选择 \(a_j\) 个节点,那么转移方程即为:

\[f_{u, i} = \max_{\sum_{j=1}^{l} a_j = i-1} (\sum_{j=1}^{l} f_{v_j, a_j}) + val_u \]

实质上可以把求 \(f_{u, i}\) 的过程看作一个分组背包问题:有 \(l\) 类物品,每类物品有若干个。第 \(a\) 的第 \(b\) 个物品的价值就是 \(f_{v_a, b}\)。要从这 \(l\) 类物品中的每一类中选择一个物品,使得总价值最大。

显然,无法直接使用上面的转移方程,因为枚举满足 \(\sum a_j = i-1\) 的数值组合 \((a_1, a_2, \cdots, a_l)\) 是困难的。

另辟蹊径,设 \(f_{u, c, i}\) 表示在 \(u\) 的前 \(c\) 棵子树中选择 \(i\) 个节点的最大收益。这样就可以顺次枚举 \(u\) 的子节点,每次求当前子节点的答案时都可以由前面的结果合并过来。设 \(blo\) 表示加入第 \(c\) 个子节点之前 \(u\) 和它的子树构成的连通块的大小(实际上是 \(1 + \sum_{k=1}^{c-1} size_{k}\)),那么转移方程为:

\[f_{u, c, i} = \max_{0 \le j \le \min(size_v, i) \and 0 \le i-j \le blo} f_{u, c-1, i-j} + f_{v, |son(v)|, j} \]

翻译:枚举 \(j\),从新的子树 \(T(v_c)\) 中选 \(j\) 个点,从原先的连通块(\(u\) 与前 \(c-1\) 个子树形成的)中选 \(i-j\) 个点,此时的收益为 \(f_{u, c-1, i-j} + f_{v, |son(v)|, j}\)

这里枚举的边界很重要。

  1. \(0 \le j \le \min(size_v, i)\):在 \(T(v_c)\) 中,最少选 \(0\) 个点,最多不能超过 \(size_v\)\(i\)
  2. \(0 \le i-j \le blo\):在加入第 \(c\) 个子节点之前 \(u\) 和它的子树构成的连通块中,最少选 \(0\) 个点,最多选 \(blo\) 个点。

综上,可以得出 \(j\) 的上界和下界:

\[\max(0, i - blo) \le j \le \min(size_v, i) \]

(我就是因为边界问题没搞明白,调了一道树上背包一周多……)

初始化:\(f_{u, 0, 1} = val(u); \forall 1 \le c \le |son(u)|,f_{u, c, 0} = 0\)

代码:

void dfs(int u)
{
	sz[u] = 1, f[u][0][1] = val[u];
	
	int &cnt = son[u];
	for(int v : G[u])
	{
		cnt++;
		dfs(v);
		sz[u] += sz[v];
		
		for(int i = 1; i <= min(m, sz[u]); i++)
		{
			for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
				f[u][cnt][i] = max(f[u][cnt][i], f[v][son[v]][j] + f[u][cnt-1][i-j]);
		}
	}
}

代码中的 sz[u] - sz[v] 就相当于上文中的 \(blo\)

提交记录

滚动数组优化

因为 \(f_{u, c, [\cdot]}\) 只会从 \(f_{u, c-1, [\cdot]}\) 转移过来,所以可以把第二个维度滚掉。空间复杂度从 \(O(n^2m)\) 降为 \(O(nm)\)
代码:

void dfs(int u)
{
	sz[u] = 1, f[u][0][1] = val[u];
	
	int now = 0, lst;
	for(int v : G[u])
	{
		now ^= 1, lst = now ^ 1, son[u]++;
		dfs(v);
		sz[u] += sz[v];
		
		for(int i = 1; i <= min(m, sz[u]); i++)
		{
			f[u][now][i] = 0; // 这里不清零或许也是对的 
			for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
				f[u][now][i] = max(f[u][now][i], f[v][son[v] & 1][j] + f[u][lst][i-j]);
		}
	}
}

提交记录

滚动数组优化 II

这次我们可以真正彻底地去掉第二维。

因为 \(i, j \ge 0\),所以 \(j, i-j \le i\)​。因此可以倒序枚举

虽然这样做并没有空间复杂度上的优化,但我们确实减少了空间占用。

代码:

void dfs(int u)
{
	sz[u] = 1, f[u][1] = val[u];
	
	for(int v : G[u])
	{
		dfs(v);
		sz[u] += sz[v];
		
		for(int i = min(m, sz[u]); i >= 1 ; i--)
		{
			for(int j = max(0, i + sz[v] - sz[u]); j <= min(i-1, sz[v]); j++)
				f[u][i] = max(f[u][i], f[v][j] + f[u][i-j]);
		}
	}
}

提交记录

时间复杂度

时间复杂度是 \(O(nm)\),但我不会证明。

咕咕咕

posted @ 2024-06-27 21:17  DengStar  阅读(18)  评论(0编辑  收藏  举报