前言
由于能力限制,本文只介绍树形 DP 这个分类。
前置芝士:存图
不要以为是树就不用存图了
说道存图嘛,哎嘿嘿
链式前向星
没错又是它(阴魂不散)
这玩意已经介绍 \(n\) 遍了,这里就简单提一嘴。
这里的链式前向星相较于图论那块的少了 \(dis\),因为这个边权没有用,其他的按照原来照搬就行。
正文
到了我们的树形 DP 了,一般的 DP 例如背包,区间,都是线形 DP,与树形 DP 不一样的是,线形 DP 题目一般不会给前驱和后继(一般都是按下标决定),而树形呢,你不存图试试,所以,树形 DP 就可以简化成线形 DP 加存图。
拿树形 DP 经典题 P1352 举例。
分析题目发现一个人有两种情况:
- 上司去或自己不想导致不去
- 上司没去或自己想导致去
由于有两种情况,所以状态转移方程,就有两个,现在我们用 \(f_{i,0}\) 表示一个人不去其下属活跃值的最大值,\(f{i,1}\) 表示一个人去,其自己活跃值,可得状态转移方程:
\[\begin{cases}& f_{i,0}=\sum \max(f_{x,1},f_{x,0})\\ & f_{i,1}=\sum f_{x,0} + a_i \end{cases} \]
所以代码为:
using namespace std;
int n,f[6001][2];
int head[6001],cnt;
int vis[6001],isgen[6001];
struct Node{
int to,next;
}edge[6001];
void add(int t1,int t2) {
edge[++cnt].to=t1;
edge[cnt].next=head[t2];
head[t2]=cnt;
}
void dfs(int root) {
vis[root]=1;
for (int i=head[root];i;i=edge[i].next) {
int x=edge[i].to;
if (!vis[x]) {
dfs(x);
f[root][1]+=f[x][0];
f[root][0]+=max(f[x][0],f[x][1]);
}
}
}
int main() {
cin>>n;
for (int i=1;i<=n;i++) {
cin>>f[i][1];//用 f[i][1] 表示 i 的活跃值
}
for (int i=1;i<n;i++) {
int t1,t2;
cin>>t1>>t2;
add(t1,t2);
isgen[t1]=1;//不是根
}
for (int i=1;i<=n;i++) {
if (!is_gen[i]) {//判断是否为根
dfs(i);
cout<<max(f[i][0],f[i][1]);
return 0;
}
}
}
\(finally\)
树形 DP 的扩展应用还有很多,因为他表示的是一种主次关系,所以还是很实用的。