『学习笔记』树上dp
树上 dp,顾名思义,就是在树上进行动态规划。
既然 dp 是思想,那么就直接拿题来吧。
P1352 没有上司的舞会
题目大意
某大学有 \(n\) 个职员,编号为 \(1 \dots n\)。
他们间的从属关系可以构成一棵以校长为根的有根树。
现在需要选择一些人来参加一个宴会,第 \(i\) 个职员来了会为宴会增加 \(r_i\) 的快乐指数。但是,如果某个职员的直接上司来参加了宴会,这个人就一定不会来了。
这里的直接上司定义为某个职员的直接父亲节点。
求能够获得的最大快乐指数。
思路
显然,对于一个节点 \(u\),我们是可以通过求出他的子节点 \(v\) 的答案来求出他的答案的。于是考虑 dp。
每个职员有两种情况:来与不来。
所以我们定义状态:\(f_{i,j}(j \in \{0,1\})\) 表示第 \(i\) 个职员,来与不来的情况下,他与他的所有子节点能够达到的最大快乐指数。
转移方程很好想:
-
\(u\) 不来:显然,\(u\) 的直接下属可以自由选择来与不来,那么 \(f_{u,0}\) 就等于 \(\sum f_{v,0}+f_{v,1}\) 了。其中 \(v\) 分别表示 \(u\) 的一个子节点。若没有某个子节点,则相应的子节点值为 \(0\)。
-
\(u\) 来:那么下属就只能不来了。但是 \(u\) 来会获得快乐指数 \(r_u\),所以 \(f_{u,1}=r_u+\sum f_{v,0}\)。
那么转移方程就出来了:
对于初始条件,将 \(f_{i,j}\) 全部设为 \(0\) 即可。
时间复杂度 \(\mathcal{O}(n)\)。
答案即为 \(\min\{f_{root,0},f_{root,1}\}\)。
代码
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
const int N=6e3+5;
struct edge{
int to,nxt;
}e[N];
int n,u,v,rt;
int r[N],s[N]; // r 用于记录每个职员来的快乐指数,s 用于记录入度(判根节点)
int head[N],top;
int f[N][2]; // f[i][j]:第 i 个职员来(j=1)与不来(j=0)所能获得的最大快乐指数
void add(int u,int v){
top++;
e[top].to=v;
e[top].nxt=head[u];
head[u]=top;
}
void dp(int u){
// 初始化
f[u][0]=0,f[u][1]=r[u];
for(int i=head[u]; i; i=e[i].nxt){ // 遍历两个子节点
int v=e[i].to;
dp(v);
// 需要求出子树最优解后才能求当前节点的
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
}
int main(){
n=read();
for(int i=1; i<=n; i++)
r[i]=read();
for(int i=1; i<n; i++){
u=read(),v=read();
add(v,u);
s[u]++;
}
// 找根节点
for(int i=1; i<=n; i++)
if(!s[i]){
rt=i;
break;
}
dp(rt);
printf("%d\n",max(f[rt][0],f[rt][1]));
return 0;
}
P2016 战略游戏
题目大意
有一个城堡,其中的道路形成一棵无根树。当一个士兵站在其中某个节点上时,可以看守与这个节点有连边的节点。
现在给你这颗树,请你算出满足所有节点有士兵或被所有相邻节点的士兵看守,最少需要放多少士兵。
也就是说,一个节点要么是一个士兵,要么是一片空地,如果是空地,所有与这个空地相邻的节点都必须有士兵。
思路
显然,这题也可以通过某个节点子节点的解来求这个节点的解,可以 dp。
无脑设计状态:\(f_{i,j}(j \in \{0,1\})\) 表示第 \(i\) 个节点与它的所有后代满足要求,第 \(i\) 个节点放与不放士兵的最优解。
那么就来分别考虑两种情况(以 \(u\) 为父节点,\(v\) 为各个子节点):
-
不放士兵(\(j=0\)):不放的话,子节点就一定要放。故 \(f_{u,0}=\sum f_{v,1}\)。
-
放士兵(\(j=1\)):这种情况子节点就自由选择,但不确定子节点放还是不放的解更优,所以 \(f_{u,1}=\sum\max\{f_{v,0},f_{v,1}\}+1\),这里 \(+1\) 是因为 \(u\) 自己也要放一个士兵。
时间复杂度 \(\mathcal{O}(n)\)。
代码
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
const int N=1e4+5;
struct edge{
int to,nxt;
}e[N];
int n,k,u,v;
int head[N],top;
int f[N][2];
void add(int u,int v){
top++;
e[top].to=v;
e[top].nxt=head[u];
head[u]=top;
}
void dp(int u,int fa){
f[u][0]=0,f[u][1]=1; // 初始化
for(int i=head[u]; i; i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue; // 防止死递归
dp(v,u);
f[u][0]+=f[v][1];
f[u][1]+=min(f[v][0],f[v][1]);
}
}
int main(){
n=read();
for(int i=1; i<=n; i++){
u=read()+1,k=read();
// 把下标从 [0,n) 变成 [1,n]
while(k--){
v=read()+1; // 同上
add(u,v);
add(v,u);
}
}
// 根节点的父亲随便写(恶臭/bx/bx
dp(1,114514);
printf("%d\n",min(f[1][0],f[1][1]));
return 0;
}