10086

my first title

my first paragraph

树形 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]

注意当 j=0 时直接赋值 fi,1,1,0,fi,0,0,01
树上背包给我们一种感觉,就是每次当前 fi 的信息,是已加入的所有儿子 v 以及 i 共同构成的。进而利用这个状态信息加入下一个儿子进行更新。这就与 [NOIP2022 建造军营] 类似了。

[NOIP2022]建造军营

tag:树形DP
首先就是简单的缩点,这不是这里的重点。在缩点后,就是一个树上的统计。在打模拟赛时的想法:
方向1:枚举点的选择并统计,拿到部分分。
方向2:统计钦定边个数确定的点选择方案。这似乎很难做,我并没有从这里很好地挣脱出来。
方向3:树形 DP 带着价值统计。想的时候状态有问题,变成了 fi,j 表示 i 这棵子数里选择连续的 j 个点。
其实我们不要刻意去关注这些点选出来的构型,我们关心的是点的选择与否以及边的确定与否。
那么设计状态 fi,0/1 表示 i 这棵子树里是否有点的方案数。
考虑转移。类似上一题,我们每次加入一棵子树 v,有转移:
fi,1fi,0×fv,1+fi,1×(2×fv,0+fv,1)
fi,0fi,0×(fv,0×2)
初始化:
fi,02边双联通分量 i 中边的数量
fi,12边双联通分量 i 中边的数量×(2边双联通分量 i 中点的数量1)
所以对于树上选择点的问题,可以考虑树形 DP。
同时可以利用这种加入子树的方式转移。
同时注意记进答案的细节:fu,1ans 的贡献,要钦定往父亲的那边不选,否则会重复统计。

[SHOI2015] 聚变反应炉

tag:树上背包

从部分分开始。对于 c 等于 10,通过手磨数据发现无论顺序如何,先处理 c1 的都是最优的。
故这 50 分直接贪心地先取 c1 的。

受贪心的启发,我们考虑点亮顺序。

定义 dp[u][0/1] 表示自己比父亲激发早还是晚,并将 u 这棵子树染完的最小花费;

定义辅助数组 g[s] 表示父节点 u 接受儿子们值为 s 的能量的最小花费。

那么我们就有转移:

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] 城市规划

仙人掌dp 树上独立集

这篇题解讲得很详细(来源: q779)。

这篇题解讲得很好。

[CEOI2007] 树的匹配 Treasury

树上独立集

这篇题解讲得很详细。

注意状态的设定,同“聚变反应炉”类似,我们在关心父子关系时,将其记入状态。

[POI2008] MAF-Mafia

基环树dp

其实这题正解是贪心,但是基环树的dp是可做的。

两种方法:

  • 对子树做完后再在环上做一遍dp
  • 拆环做dp
    具体实现:
#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;
}

反思

  1. 数据范围较小的,用暴力;对于具有特殊性质的数据,找规律
  2. 基环树 dp:① 拆环做树形dp;② 先树上再环上

F. Another Letter Tree - 暑期训练37

树形dp,差值

这题的关键在于看清了答案是可差分的。可以 O(nm2) 地算出从根节点到点 u,匹配模式串的范围为 [l,r] 的方案数。答案统计:对于 ab 的路径询问,记录 li,ri 表示前半段匹配了 [1,li],后半段匹配了 [ri,m] 的方案数。l,r 的计算就是将起点在 lca 以上的部分减去,并将起点在 lca 下,终点在 lca 上的乘一下减去。
从模式串的长度出发,我们得到了一个较为完备的信息 f,g,进而可以只记录到根节点的信息,可差分。

posted @   Luzexxi  阅读(19)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示