题解:P2014 CTSC 1997 选课
lalaji2010 · · 题解
P2014 [CTSC1997] 选课
题解区一堆 dalao 泛化物品优化直接用,是不是只有我这种蒟蒻根本看不懂 qaq。
本文讲得比较详细,只要耐心读下去再稍加理解应该可以读懂。
这题做了很久,非常好的一道题目。
这是一道经典的多叉树上资源分配问题,又称树形背包问题。
首先,二叉树上资源分配问题上个题就已经会做了,所以这道题可以多叉转二叉再左右儿子枚举分配数量记忆化一下来做。
但是这里放这道题的初衷还是为了介绍一下树形背包及其优化。
Part1 n,m \leq 300
有依赖关系的结构求固定个数元素最大和。可以考虑将依赖结构表现为一棵树,
建好之后是一个森林,考虑建立超级源点,将无依赖的结点作为超级源点的儿子,超级源点本身价值设为
设
当求解完
容易发现其实也可以像 01 背包一样(用一个不用滚动的滚动数组)压掉第二维(前
时间复杂度
代码应该是相当好懂的。
AC CODE
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dp[305][305];
vector<int> e[305];
void dfs(int u,int fa){
for(int i=0;i<e[u].size();i++){
int v=e[u][i];
if(v==fa) continue;
dfs(v,u);
for(int j=m;j>0;j--){
for(int k=0;k<j;k++){
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
}
}
}
}
int main(){
cin>>n>>m;
m++;
for(int i=1;i<=n;i++){
int u=0;
cin>>u>>dp[i][1];
e[u].push_back(i);
e[i].push_back(u);
}
dfs(0,-1);
printf("%d",dp[0][m]);
return 0;
}
但是,我们怎么能把打一个暴力,切一道水题
Part2 n,m \leq 5000
我们需要时间复杂度
这里介绍一种树形背包的常用优化——泛化物品优化。
泛化物品:同一个物品,给它准备的重量限制不同,其价值也不同。来看一看怎么做的。
下文所说的子树不包含其根结点。
肯定是状态设得好,AC 没烦恼让其转移更便捷,设
我们就把 dfs 序先于
一个结点
-
可以是其父亲
u 传下来的dp[u][i] ,表示先于v 遍历的兄弟结点和u 的旁路点里选i 个再选上v (v 自己一定不是自己的旁路点)即:dp[v][i]=dp[u][i]+val[v] -
也可以是选上
v 的某个儿子w 然后让w 再选i-1 个其旁路点和子树内的点(先于它遍历的兄弟结点也是其旁路点!),共选了i 个v 的子树内的点和v 的旁路点。对于w 以后的每个儿子,w 的贡献都是应当被他们考虑在内的(并在上面进行背包问题类似的累加操作)。说白了,将子结点w 的值尝试与前面的子结点(或旁路点)的值作背包,将w 得到的答案一律保存到父亲v 中,方便爷爷u 和后面遍历兄弟取用,有:
在每一个父子结点块之间做的操作完全是基于背包的思想,只是利用了一些树形结构的特性,正确性显然。
子结构的问题会做了,我们整个问也就会做了,时间复杂度
非常神奇地减掉了一层无用的状态枚举。
这道题同时告诉我们:
-
按照子树的遍历顺序,可以将根结点的值不断更新,从而起到压掉 dp 数组一维的作用。
-
状态的设计是动态规划中最为关键的一环。
如果还有不懂的可以看一下代码感性理解一下。
AC CODE
#include<bits/stdc++.h>
using namespace std;
int n,m;
int dp[5005][5005];
int c[5005];
vector<int> e[5005];
void dfs(int u,int fa){
for(int i=0;i<e[u].size();i++){
int v=e[u][i];
if(v==fa) continue;
for(int j=0;j<=m;j++) dp[v][j]=max(dp[v][j],dp[u][j]+c[v]);//u 对 v 的贡献
dfs(v,u);
for(int j=1;j<=m;j++) dp[u][j]=max(dp[u][j],dp[v][j-1]);
//v 对 u 的贡献,等同于将 v 视为后面访问到 dp[u][i] 的值里 i 的一部分
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
int u=0;
cin>>u;
e[u].push_back(i);
e[i].push_back(u);
cin>>c[i];
}
c[0]=0;
dfs(0,-1);
printf("%d",dp[0][m]);//超级源点到根节点(自己)之外的所有点选了 m 个的最大和
return 0;
}