树形 DP
Update on 11.3
之前写的太拉了,加入了思维方法和总结。
[JSOI2018] 潜入行动
tag:树上背包
考虑状态的设计:考虑树上的关系。在不考虑父亲节点的前提下,一个点关键在于是否放监听器和是否被监听。
简陋的转移:
f[i][j][0/1][0/1]
表示 i 这棵子树内选了 j 个,不考虑父亲节点
这个点有没有放,这个点有没有被覆盖
f[i][j][0][0] <- f[i][j-siz][0][0] * f[v][siz][0][1]
f[i][j][0][1] <- f[i][j-siz][0][0] * f[v][siz][1][1]
<- f[i][j-siz][0][1] * f[v][siz][0][0/1]
f[i][j][1][0] <- f[i][j-siz][1][0] * f[v][siz][0][0/1]
f[i][j][1][1] <- f[i][j-siz][1][0] * f[v][siz][1][0/1]
<- f[i][j-siz][1][1] * f[v][siz][0/1][0/1]
注意当
树上背包给我们一种感觉,就是每次当前
[NOIP2022]建造军营
tag:树形DP
首先就是简单的缩点,这不是这里的重点。在缩点后,就是一个树上的统计。在打模拟赛时的想法:
方向1:枚举点的选择并统计,拿到部分分。
方向2:统计钦定边个数确定的点选择方案。这似乎很难做,我并没有从这里很好地挣脱出来。
方向3:树形 DP 带着价值统计。想的时候状态有问题,变成了
其实我们不要刻意去关注这些点选出来的构型,我们关心的是点的选择与否以及边的确定与否。
那么设计状态
考虑转移。类似上一题,我们每次加入一棵子树
初始化:
所以对于树上选择点的问题,可以考虑树形 DP。
同时可以利用这种加入子树的方式转移。
同时注意记进答案的细节:
[SHOI2015] 聚变反应炉
tag:树上背包
从部分分开始。对于
故这
受贪心的启发,我们考虑点亮顺序。
定义
定义辅助数组
那么我们就有转移:
void dfs(int u,int fa){
for(int v : e[u])if(v ^ fa)dfs(v,u);
int sum = 0,now = 0;
memset(g,0x3f,sizeof g);
g[0] = 0;
for(int v : e[u])if(v ^ fa){
for(int i = sum;~i;--i)
g[i+c[v]] = min(g[i+c[v]],g[i]+f[v][0]),g[i] += f[v][1];
sum += c[v];
}
f[u][1] = f[u][0] = inf;
for(int i = 0;i<=sum;++i)
f[u][0] = min(f[u][0],max(g[i],g[i]-i+d[u])),
f[u][1] = min(f[u][1],max(g[i],g[i]-i+d[u]-c[fa]));
}
[SDOI2010] 城市规划
仙人掌 树上独立集
这篇题解讲得很详细(来源: q779)。
这篇题解讲得很好。
[CEOI2007] 树的匹配 Treasury
树上独立集
这篇题解讲得很详细。
注意状态的设定,同“聚变反应炉”类似,我们在关心父子关系时,将其记入状态。
[POI2008] MAF-Mafia
基环树
其实这题正解是贪心,但是基环树的
两种方法:
- 对子树做完后再在环上做一遍
- 拆环做
具体实现:
#include<bits/stdc++.h>
#define print(a) cout << #a"=" << a << endl
#define debug() cout << "Line:" << __LINE__ << endl
#define sign() puts("----------")
using namespace std;
// 基环树拆环 DP
const int N = 1000010,inf = 0x3f3f3f3f;
int n,to[N],d[N];
vector<int> e[N];
int mx,mn;// 最大存活人数 最小存活人数
int cir[N],len;
bool incir[N];
int f[N][2];
void dfs(int u){
f[u][0] = 0, f[u][1] = 1;
bool flag = 1;
for(int v : e[u])if(!incir[v]){
dfs(v); flag = 0;
// 这里 f[u][0/1] 是已经扫过的子树得到的结果
f[u][0] = max(f[u][0],f[u][1] - 1) + max(f[v][0],f[v][1]);
// 那么这里 f[u][1] - 1 是因为这一枪让当前的 v 来崩
if(f[v][0] != -inf)f[u][1] += f[v][0];
else f[u][1] = -inf;// 若存在一个儿子,他必须存活,那么当前 u 不能存活
}
if(flag)f[u][0] = -inf; // 叶子结点一定存活
}
void dfs(int u,int del,int rt){
if(u != del)f[u][0] = 0, f[u][1] = 1;
bool flag = 1;
for(int v : e[u]){
if(u == del && v == rt)continue;
dfs(v,del,rt), flag = 0;
f[u][0] += max(f[v][1],f[v][0]);
if(f[v][0] == -inf)f[u][1] = -inf;
else f[u][1] += f[v][0];
}
if(flag && u != del)f[u][0] = -inf;
}
void solve(int s){
len = 0;
bool flag = 1;
for(int now = s;d[now];now = to[now])
cir[++len] = now, incir[now] = 1, d[now] = 0, flag &= ((int)e[now].size() == 1);
if(flag){// 只有一个环
++mn;
if(len == 1)--mn;// 只有一个自环
mx += len / 2;
return ;
}
if(len == 1){// 以自环为根的一棵树
dfs(s);
mx += max(f[s][1] - 1, f[s][0]);// 由于自环自己必死,故 -1
return ;
}
// 删 s -> to[s] 这条边
// 这里令 to[s] 必死
int res = -inf;
f[to[s]][1] = -inf;
f[to[s]][0] = 0;
dfs(s,to[s],s);
res = max(res,f[s][0]);
res = max(res,f[s][1]);
// 令 to[s] 必活
f[to[s]][1] = 1;
f[to[s]][0] = -inf;
dfs(s,to[s],s);
res = max(res,f[s][0]);
mx += res;
}
signed main(){
scanf("%d",&n);
for(int i = 1;i<=n;++i)scanf("%d",&to[i]),++d[to[i]],e[to[i]].push_back(i);
queue<int> q;
for(int i = 1;i<=n;++i)if(!d[i])q.push(i),++mn;
while(!q.empty()){
int now = q.front(); q.pop();
if(--d[to[now]] == 0)q.push(to[now]);
}
for(int i = 1;i<=n;++i)if(d[i])solve(i);
printf("%d %d",n-mx,n-mn);
return 0;
}
反思
- 数据范围较小的,用暴力;对于具有特殊性质的数据,找规律
- 基环树 dp:① 拆环做树形dp;② 先树上再环上
F. Another Letter Tree - 暑期训练37
树形dp,差值
这题的关键在于看清了答案是可差分的。可以
从模式串的长度出发,我们得到了一个较为完备的信息
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!