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

[CTSC1997] 选课 题解

题目链接:洛谷 | LOJ

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

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

图论建模

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

dp 的初步实现

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

u 的子节点的个数 |son(u)|=l,子节点的集合 son(u)={v1,v2,,vl}。假设我们从 u 的第 j 个子节点的子树中选择 aj 个节点,那么转移方程即为:

fu,i=maxj=1laj=i1(j=1lfvj,aj)+valu

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

显然,无法直接使用上面的转移方程,因为枚举满足 aj=i1 的数值组合 (a1,a2,,al) 是困难的。

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

fu,c,i=max0jmin(sizev,i)0ijblofu,c1,ij+fv,|son(v)|,j

翻译:枚举 j,从新的子树 T(vc) 中选 j 个点,从原先的连通块(u 与前 c1 个子树形成的)中选 ij 个点,此时的收益为 fu,c1,ij+fv,|son(v)|,j

这里枚举的边界很重要。

  1. 0jmin(sizev,i):在 T(vc) 中,最少选 0 个点,最多不能超过 sizevi
  2. 0ijblo:在加入第 c 个子节点之前 u 和它的子树构成的连通块中,最少选 0 个点,最多选 blo 个点。

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

max(0,iblo)jmin(sizev,i)

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

初始化:fu,0,1=val(u);1c|son(u)|,fu,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

提交记录

滚动数组优化

因为 fu,c,[] 只会从 fu,c1,[] 转移过来,所以可以把第二个维度滚掉。空间复杂度从 O(n2m) 降为 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,j0,所以 j,iji​。因此可以倒序枚举

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

代码:

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 @   DengStar  阅读(27)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示