树形动态规划做题记录

树的直径

SP1437 PT07Z - Longest path in a tree

#include<iostream>
#include<cstdio>

using namespace std;
int n,u,v,d1[100005],d2[100005],ans;
int head[100005],cnt;
struct edge{
	int nxt;
	int to;
}e[300005];

void addedge(int from,int to)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	head[from] = cnt;
	cnt ++;
	e[cnt].nxt = head[to];
	e[cnt].to = from;
	head[to] = cnt;
	return ;
}

void DP(int u,int fa)
{
	int v = 0;
	for(int i = head[u];i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == fa) continue;
		DP(v,u);
		if(d1[v] + 1 > d1[u])
		{
			d2[u] = d1[u];
			d1[u] = d1[v] + 1;
		}
		else if(d1[v] + 1 > d2[u]) d2[u] = d1[v] + 1;
	}
	ans = max(ans,d1[u]+d2[u]);
	return ;
}

int main()
{
	scanf("%d",&n);
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d",&u,&v);
		addedge(u,v);
	}
	DP(1,0);
	printf("%d",ans);
	return 0;
} 

P1273 有线电视网 (树上分组背包)

树上分组背包:P1273 有线电视网

在每一个结点处都做一次背包,设 f ( i , j ) f(i,j) f(i,j) 表示在结点 i i i j j j 个用户观看比赛的最大收益,那么对于以下情况:
在这里插入图片描述
儿子中的最优选择结果已经确定,现在只要看父亲决策的时候是否选择它,以及选择它的哪一种情况,于是就有:
f ( u , j ) = max ⁡ v ∈ s o n ( u ) { max ⁡ 1 ⩽ k ⩽ t o t ( v ) { f ( u , j − k ) + f ( v , k ) − v a l ( e u , v ) } } f(u,j) =\max_{v\in son(u)}\{ \max_{1\leqslant k\leqslant tot(v)}\{f(u,j-k) + f(v,k) -val(e_{u,v})\}\} f(u,j)=vson(u)max{1ktot(v)max{f(u,jk)+f(v,k)val(eu,v)}}

其实就是有儿子个数那么多组物品,同一儿子的每一种情况作为同一组中的物品相互排斥,于是就是分组背包的形式。

#include<iostream>
#include<cstdio>

using namespace std;
const int inf = 0xfffffff;
int n,m,x,y,z;
int head[3005],cnt,tmp,f[3005][3005],a[3005];
int ans,maxn[3005];
struct edge{
	int nxt;
	int to;
	int fee;
}e[80005];

void addedge(int from,int to,int cost)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].fee = cost;
	head[from] = cnt;
	return ;
}

void dfs_dp(int now)
{
	if(head[now] == 0)
	{
		f[now][1] = a[now];
		maxn[now] = 1;
		return ;
	}
	int son = 0;
	for(int i = head[now];i;i = e[i].nxt)
	{
		son = e[i].to;
		dfs_dp(son);
		maxn[now] += maxn[son];
		for(int j = maxn[now];j >= 0;j --)
			for(int k = 1;k <= maxn[son];k ++)
				if(j-k >= 0) f[now][j] = max(f[now][j],f[now][j-k]+f[son][k]-e[i].fee);
	}
	return ;
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= n-m;i ++)
	{
		scanf("%d",&x);
		for(int j = 1;j <= x;j ++)
		{
			scanf("%d%d",&y,&z);
			addedge(i,y,z);
		}
	}	
	for(int i = n-m+1;i <= n;i ++) scanf("%d",&a[i]);
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= m;j ++)
			f[i][j] = -inf;
	dfs_dp(1);
	for(int i = m;i >= 0;i --)
		if(f[1][i] >= 0)
		{
			ans = i;
			break;
		}
	printf("%d",ans);
	return 0;
}

P2585 [ZJOI2006]三色二叉树(朴素 DP)

染色问题?:P2585 [ZJOI2006]三色二叉树

f ( u , c ) f(u,c) f(u,c) 表示以 u u u 为根结点且 u u u 的颜色为 c c c 的子树下绿色结点的个数
显然易得:
f max ⁡ ( u , c ) = max ⁡ { f m a x ( l s o n , ( c + 1 ) m o d    3 ) + f m a x ( r s o n , ( c + 2 ) m o d    3 ) + [ c = g r e e n ] f m a x ( l s o n , ( c + 2 ) m o d    3 ) + f m a x ( r s o n , ( c + 1 ) m o d    3 ) + [ c = g r e e n ] f m i n ( u , c ) = min ⁡ { f m i n ( l s o n , ( c + 1 ) m o d    3 ) + f m i n ( r s o n , ( c + 2 ) m o d    3 ) f m i n ( l s o n , ( c + 2 ) m o d    3 ) + f m i n ( r s o n , ( c + 1 ) m o d    3 ) + [ c = g r e e n ] f_{\max}(u,c) = \max \begin{cases} f_{max}(lson,(c+1)\mod3) + f_{max}(rson,(c+2)\mod3)+[c=green] \\ f_{max}(lson,(c+2)\mod3) + f_{max}(rson,(c+1)\mod3)+[c=green] \\ \end{cases} \\ \\ f_{min}(u,c) = \min \begin{cases} f_{min}(lson,(c+1)\mod 3) + f_{min}(rson,(c+2)\mod3) \\ f_{min}(lson,(c+2)\mod 3) + f_{min}(rson,(c+1)\mod 3) \\ \end{cases} +[c=green] fmax(u,c)=max{fmax(lson,(c+1)mod3)+fmax(rson,(c+2)mod3)+[c=green]fmax(lson,(c+2)mod3)+fmax(rson,(c+1)mod3)+[c=green]fmin(u,c)=min{fmin(lson,(c+1)mod3)+fmin(rson,(c+2)mod3)fmin(lson,(c+2)mod3)+fmin(rson,(c+1)mod3)+[c=green]
关注不同状态之间的转移方式。

#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>

using namespace std;
const int inf = 0xfffffff;
string s;
int n,cnt,fa[500005],lson[500005],rson[500005];
int fmax[500005][3],fmin[500005][3];//0 red,1 blue,2 green
bool vis[500005];

void dfs_dp(int now)
{
	if(s[now-1] == '0')
	{
		fmax[now][0] = fmin[now][0] = 0;
		fmax[now][1] = fmin[now][1] = 0;
		fmax[now][2] = fmin[now][2] = 1;
		return ;
	}
	if(s[now-1] == '1')
	{
		dfs_dp(lson[now]);
		fmax[now][0] = max(fmax[now][0],max(fmax[lson[now]][1],fmax[lson[now]][2]));
		fmax[now][1] = max(fmax[now][1],max(fmax[lson[now]][0],fmax[lson[now]][2]));
		fmax[now][2] = max(fmax[now][2],max(fmax[lson[now]][0],fmax[lson[now]][1])+1);
		fmin[now][0] = min(fmin[now][0],min(fmin[lson[now]][1],fmin[lson[now]][2]));
		fmin[now][1] = min(fmin[now][1],min(fmin[lson[now]][0],fmin[lson[now]][2]));
		fmin[now][2] = min(fmin[now][2],min(fmin[lson[now]][0],fmin[lson[now]][1]))+1;
		return ;
	}
	if(s[now-1] == '2')
	{
		dfs_dp(lson[now]);dfs_dp(rson[now]);
		fmax[now][0] = max(fmax[now][0],max(fmax[lson[now]][1] + fmax[rson[now]][2],fmax[lson[now]][2] + fmax[rson[now]][1]));
		fmax[now][1] = max(fmax[now][1],max(fmax[lson[now]][0] + fmax[rson[now]][2],fmax[lson[now]][2] + fmax[rson[now]][0]));
		fmax[now][2] = max(fmax[now][2],max(fmax[lson[now]][1] + fmax[rson[now]][0],fmax[lson[now]][0] + fmax[rson[now]][1])+1);
		fmin[now][0] = min(fmin[now][0],min(fmin[lson[now]][1] + fmin[rson[now]][2],fmin[lson[now]][2] + fmin[rson[now]][1]));
		fmin[now][1] = min(fmin[now][1],min(fmin[lson[now]][0] + fmin[rson[now]][2],fmin[lson[now]][2] + fmin[rson[now]][0]));
		fmin[now][2] = min(fmin[now][2],min(fmin[lson[now]][1] + fmin[rson[now]][0],fmin[lson[now]][0] + fmin[rson[now]][1]))+1;
		return ;
	}
}

void dfs_build(int now)
{
	vis[now] = 1;
	if(s[now-1] == '0') return ;
	if(s[now-1] == '2')
	{
		for(int i = now+1;i <= n;i ++)
			if(!vis[i])
			{
				lson[now] = i;fa[i] = now;
				dfs_build(i);
				break;
			}
		for(int i = now+1;i <= n;i ++)
			if(!vis[i])
			{
				rson[now] = i;fa[i] = now;
				dfs_build(i);
				break;
			}
		return ;
	}
	if(s[now-1] == '1')
	{
		for(int i = now + 1;i <= n;i ++)
			if(!vis[i])
			{
				lson[now] = i;fa[i] = now;
				dfs_build(i);
				break;
			}
		return ;
	}
}

int main()
{
	cin >> s;n = s.size();
	dfs_build(1);
	for(int i = 1;i <= n;i ++)
	{
		fmin[i][0] = fmin[i][1] = fmin[i][2] = inf;
	}
	dfs_dp(1);
	cout << max(fmax[1][0],max(fmax[1][1],fmax[1][2])) << ' ' << min(fmin[1][0],min(fmin[1][1],fmin[1][2])) << endl;
	return 0;
}

ZJOI2007时态同步

P1131 [ZJOI2007]时态同步
只能算半个 DP ,因为在想到 DP 之前就已经想到 dfs 暴力做法了
发现的重要性质是:对于一些叶子结点,在它们的公共祖先与其父亲的一条边上加操作显然比其它方案更优。
于是瞎搞一个有点像差分的东西,算出答案

#include<iostream>
#include<cstdio>

using namespace std;
typedef long long ll;
int n,s,x,y;
int head[500005],cnt,fa[500005];
ll d[500005],fmax[500005],add[500005],maxn,ans,z;
struct edge{
	int nxt;
	int to;
	ll dis;
}e[1000100];

void addedge(int from,int to,ll val)
{
	cnt ++;
	e[cnt].nxt = head[from];
	e[cnt].to = to;
	e[cnt].dis = val;
	head[from] = cnt;
	return ;
}

void dfs1(int u,int father)
{
	int v = 0;bool leaf = 1;
	for(int i = head[u];i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v != father)
		{
			fa[v] = u;leaf = 0;
			d[v] = d[u] + e[i].dis;
			dfs1(v,u);
			fmax[u] = max(fmax[u],fmax[v]);
		}
	}
	if(leaf) fmax[u] = d[u];
	maxn = max(maxn,fmax[u]);
	return ;
}

void dfs2(int u)
{
	int v = 0;
	for(int i = head[u];i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v != fa[u])
			dfs2(v);
	}
	add[u] -= add[fa[u]];
	ans += add[u];
	return ;
}

int main()
{
	scanf("%d",&n);
	scanf("%d",&s);
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d%lld",&x,&y,&z);
		addedge(x,y,z);
		addedge(y,x,z);
	}
	d[s] = 0;
	dfs1(s,0);
	for(int i = 1;i <= n;i ++) add[i] = maxn - fmax[i];
	dfs2(s);
	printf("%lld",ans);
	return 0;
}

P3177 [HAOI2015]树上染色 (考虑每条边的贡献)

单独考虑每条边对答案的影响,树上背包
考虑一条边对答案的贡献,设这一条边上方的黑点数为 a a a ,上方的白点数为 b b b 那么它对答案的贡献就是
v a l = w × ( ( k − a ) ⋅ a + ( n − k − b ) ⋅ b ) val=w\times((k-a)\cdot a + (n-k-b)\cdot b) val=w×((ka)a+(nkb)b)
我们知道,边是连在父子之间的,由此我们可以通过结点间的父子关系,统计所有边对答案的贡献。通过刚刚的分析,我们将 u u u v v v 一条边的下方划分为结点 v v v 为根的子树,那么上方就是剩余的所有结点。然后设计状态,设 f ( u , j ) f(u,j) f(u,j) 表示在结点 u u u 为根的子树内,有 j j j 个黑点的子树内边的贡献和。我们可以将其视为背包问题,如何选出这 k k k 个黑点,于是我们可以得到转移方程
f ( u , j ) = max ⁡ v ∈ s o n ( u ) { f ( v , p ) + f ( u , j − p ) + v a l ( e u → v ) } f(u,j) = \max_{v\in son(u)}\{f(v,p) + f(u,j-p)+val(e_{u\rightarrow v})\} f(u,j)=vson(u)max{f(v,p)+f(u,jp)+val(euv)}
其中, v a l ( e u → v ) val(e_{u\rightarrow v}) val(euv) 就是我们上面的贡献计算公式。
那么根据定义,最终的答案就是 f ( r t , k ) f(rt,k) f(rt,k)
在写代码的时候,注意 DP 的边界与转移细节,不要取到不可取的状态。

void DP(int u,int pre)
{
	f[u][0] = f[u][1] = 0;//初始化 
	siz[u] = 1;
	for(register int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		fa[v] = u;
		DP(v,u);
		siz[u] += siz[v];//统计子树大小 
	}
	for(register int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == fa[u]) continue;
		for(register ll j = min(k,siz[u]);j >= 0;j --)//背包 
			for(register ll p = 0;p <= min(siz[v],j);p ++)
				if(f[u][j - p] >= 0)//从可取状态转移 
				{
					eval = e[i].val*((k - p)*p + (n - k - siz[v] + p)*(siz[v] - p));
					f[u][j] = max(f[u][j],f[u][j - p] + f[v][p] + eval);
				}
	}
			
	return ;
}

int main()
{
	n = fstread();k = fstread();
//	scanf("%lld%lld",&n,&k);
	for(int i = 1;i < n;i ++)
	{
		x = fstread();y = fstread();z = fstread();
//		scanf("%lld%lld%lld",&x,&y,&z);
		addedge(x,y,z);	
	}
	for(int i = 0;i <= n;i ++)//初始化 
		for(int j = 0;j <= k;j ++)
			f[i][j] = -1;//一开始定义所有状态不可取 
	DP(1,0);
	printf("%lld",f[1][k]);
	return 0;
}

P3478 [POI2008]STA-Station(换根 DP)

P2986 [USACO10MAR]Great Cow Gathering G(换根 DP)

这两题基本一样,这里放在一起讲。

首先看简单一点的 P3478[POI2008]STA-Station

要观察到换根的时候,答案怎么变化。

在这里插入图片描述

当偏离根 1 个深度的时候,发现新的根上方的所有结点的深度都增加了 1 ,新根下方所有结点的深度都减少了 1.

f ( u ) f(u) f(u) 表示以 u u u 为根的深度总和,有 f ( u ) = f ( f a ( u ) ) + n − s i z ( u ) − s i z ( u ) f(u) = f(fa(u)) + n-siz(u)-siz(u) f(u)=f(fa(u))+nsiz(u)siz(u)

然后第一次 DFS 预处理以某一结点 u u u 为根时的深度总和,计算以这一结点为根时,所有子树大小,第二次 DFS 给 f ( u ) f(u) f(u) 赋初值,从 u u u 开始 DP 即可。

void DFS1(int u,int pre)//预处理
{
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		dep[v] = dep[u] + 1; //计算深度
		DFS1(v,u);
		siz[u] += siz[v];//计算子树大小
		depsum[u] += depsum[v];//计算深度总和
	}
	siz[u] ++;depsum[u] += dep[u];
	return ;
}

void DFS2(int u,int pre)//换根 DP
{
	if(u != 1)
	{
		f[u] = f[pre] + n - siz[u] - siz[u];
		if(ans < f[u])
		{
			ans = f[u];
			ansv = u;
		}
	}
		
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		DFS2(v,u);
	}
	return ;
}

int main()
{
	scanf("%lld",&n);
	for(int i = 1;i < n;i ++)
	{
		scanf("%lld%lld",&x,&y);
		addedge(x,y);
	}
	DFS1(1,0);//以 1 为根
	ans = depsum[1];ansv = 1;f[1] = depsum[1];//初始化
	DFS2(1,0);//换根 DP
	printf("%lld",ansv);
	return 0;
}

然后看到第二题 P2986 [USACO10MAR]Great Cow Gathering G ,这题多了点权和边权,其实第一题就是所有点权为 1,边权均为 1 的情况,那么同理,偏离原根距离为 l l l 的时候,新根上方的所有结点对答案的贡献 + l × ∑ w i +l\times \sum w_i +l×wi ,新根下方的所有结点对答案的贡献为 − l × ∑ w i -l\times \sum w_i l×wi

f ( u ) f(u) f(u) 为以 u u u 为根时的答案,有 f ( u ) = f ( f a ( u ) ) + Δ l × ( ∑ i ∉ s o n ( u ) w i − ∑ i ∈ s o n ( u ) w i ) f(u)=f(fa(u))+\Delta l\times(\sum_{i\notin son(u)}w_i-\sum_{i\in son(u)}w_i) f(u)=f(fa(u))+Δl×(i/son(u)wiison(u)wi)

void DFS1(int u,int pre)
{
	f[1] += dis[u] * w[u];
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		dis[v] = dis[u] + e[i].val; 
		DFS1(v,u);
		siz[u] += siz[v];
		sum[u] += sum[v];
	}
	siz[u] ++;sum[u] += w[u];
	return ;
}

void DFS2(int u,int pre)
{
	if(u != 1)
	{
		f[u] = f[pre] + (sum[1] - sum[u] - sum[u])*(dis[u] - dis[pre]);
		if(ans > f[u])
		{
			ans = f[u];
			ansv = u;
		}
	}
		
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		DFS2(v,u);
	}
	return ;
}

int main()
{
	scanf("%lld",&n);
	for(int i = 1;i <= n;i ++) scanf("%lld",&w[i]);
	for(int i = 1;i < n;i ++)
	{
		scanf("%lld%lld%lld",&x,&y,&z);
		addedge(x,y,z);
	}
	DFS1(1,0);
//	ans = depsum[1];ansv = 1;f[1] = depsum[1];
	ans = f[1];ansv = 1;
	DFS2(1,0);
	printf("%lld",ans);
	return 0;
}

CF1092F Tree with Maximum Cost 三倍经验(换根 DP)

CF1092F Tree with Maximum Cost

同上两题一模一样。

P3047 [USACO12FEB]Nearby Cows G (换根 DP)

P3047 [USACO12FEB]Nearby Cows G
考虑设计简单状态 f ( u , k ) f(u,k) f(u,k) 表示走 k k k 步恰好走到 u u u 的所有结点的权值和,我们发现很难转移,于是考虑如何走到 u u u ,分类讨论:

  1. 结点 v v v u u u 的祖先,它一直往下走走到 u u u
  2. 结点 v v v u u u 的儿子,它一直往上走走到 u u u
  3. 结点 v v v u u u 在不同子树内,它一直往上走走到 u u u 的某个祖先,然后一直往下走走到 u u u

然后计算这三种情况,
f ( u , k , 1 ) = f ( f a ( u ) , k − 1 , 1 ) + f ( f a ( u ) , k − 1 , 3 ) f ( u , k , 2 ) = ∑ v ∈ s o n ( u ) f ( v , k − 1 , 2 ) f ( u , k , 3 ) = f ( f a ( u ) , k − 1 , 2 ) − f ( u , k − 2 , 2 ) \begin{aligned} f(u,k,1) &= f(fa(u),k-1,1)+f(fa(u),k-1,3)\\[2ex] f(u,k,2) &= \sum_{v\in son(u)} f(v,k-1,2)\\[2ex] f(u,k,3)&=f(fa(u),k-1,2)-f(u,k-2,2) \end{aligned} f(u,k,1)f(u,k,2)f(u,k,3)=f(fa(u),k1,1)+f(fa(u),k1,3)=vson(u)f(v,k1,2)=f(fa(u),k1,2)f(u,k2,2)
现在解释这三条方程:
首先我们多记了一个状态 s s s ,令 f ( u , k , s ) f(u,k,s) f(u,k,s) 表示以 s s s 状态走 k k k 步恰好走到 u u u 的点的权值和。
那么对于 u u u 的父亲到达 u u u 的点,即 s = 1 s = 1 s=1 有:
在这里插入图片描述

所以可能是从祖先过来的,也可能是从其它子树过来的。

对于通过儿子到达 u u u 的点,即 s = 2 s = 2 s=2 有:
在这里插入图片描述
所以只可能是从儿子或者儿子的儿子过来。

对于与 u u u 同一父亲,但是不同子树走到 u u u 的,即 s = 3 s = 3 s=3 ,有:
在这里插入图片描述
所以一定是作为父亲的儿子走上父亲,然后减去 u u u 走上父亲的一段。
小结:对于树上问题,常常要考虑父子之间的关系,将大问题拆解成父子间的小问题,然后要分类讨论,如何通过题目条件、父子关系计算贡献与答案。这一题计算的答案与整棵树有关,要考虑使用合适时间复杂度的做法。

	for(int i = 1;i <= n;i ++)//提前处理第1步
	{
		f[i][1][1] = c[fa[i]];
		for(int j = head[i];j;j = e[j].nxt)
			if(e[j].to != fa[i])
				f[i][1][0] += c[e[j].to];
	}
	for(int i = 2;i <= k;i ++)//从第二步开始 DP
		for(int j = 1;j <= n;j ++)
		{
			f[j][i][1] = f[fa[j]][i - 1][1] + f[fa[j]][i - 1][2];
			for(int p = head[j];p;p = e[p].nxt)
				if(e[p].to != fa[j])
					f[j][i][0] += f[e[p].to][i - 1][0];
			if(fa[j] > 0)
			{
				if(i > 2) f[j][i][2] = f[fa[j]][i - 1][0] - f[j][i - 2][0];//注意f[fa[j]][i-1][0]是怎么计算来的
				else f[j][i][2] = f[fa[j]][i - 1][0] - c[j];//
			}
				
		}
			
	for(int i = 1;i <= n;i ++)
		for(int j = 1;j <= k;j ++)
			ans[i] += f[i][j][0] + f[i][j][1] + f[i][j][2];
	for(int i = 1;i <= n;i ++)
		printf("%lld\n",ans[i]+c[i]);
	return 0;
}

换根 DP

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。

容斥

发现能到达 u u u 的情况有两种

  1. 从父亲走到 u u u
  2. 从儿子走到 u u u

考虑通过两次 DFS 计算答案。
从儿子推到父亲
f ( i , j ) f(i,j) f(i,j) 表示 i i i 的子树中,与 i i i 的距离为 j j j 的结点的权值和
f ( i , j ) = ∑ f ( k , j − 1 ) f(i,j) = \sum f(k,j-1) f(i,j)=f(k,j1)
从父亲推到儿子
由于上一次 DFS 计算后,父亲的状态中包含着儿子状态,所以要容斥
f ′ ( i , j ) = f ( i , j ) + f ( f a ( i ) , j − 1 ) − f ( i , j − 2 ) f'(i,j) = f(i,j) + f(fa(i),j-1)-f(i,j-2) f(i,j)=f(i,j)+f(fa(i),j1)f(i,j2)

非容斥

考虑第一次统计,统计儿子对父亲的贡献:
f ( i , j ) = ∑ f ( k , j − 1 ) f(i,j) = \sum f(k,j-1) f(i,j)=f(k,j1)
然后换根 DP 的过程中,定义操作 link(u,v) v v v 的贡献加到 u u u 上,cut(u,v) u u u 中属于 v v v 的贡献减掉

void cut(int now,int pre)
{
	for(int i=1;i<=m;i++)
		f[now][i]-=f[pre][i-1];
}
void link(int now,int pre)
{
	for(int i=1;i<=m;i++)
		f[now][i]+=f[pre][i-1];
}
void dfs(int now,int fa)
{
	for(int i=0;i<=m;i++)
		ans[now]+=f[now][i];
	for(int i=head[now];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa)continue;
		cut(now,v);link(v,now);//先减去自己对儿子的影响,然后统计答案
		dfs(v,now);
		cut(v,now);link(now,v);
	}
}

CF708C Centroids (换根 DP)

CF708C Centroids

考虑某个结点在什么情况下可以通过至多一次改造成为重心。

首先,如果这个结点是重心,那么不用改造。

如果这个结点不是重心,那么以它作为整棵树的根的时候,不能有超过一个子树的大小大于 n 2 \dfrac{n}{2} 2n ,否则就需要多次改造了。当它只有一个以 v v v 为根的子树的大小大于 n 2 \dfrac{n}{2} 2n 的时候,还要满足这个子树中存在一个以 v ′ v' v 为根的子树,使得 s i z ( v ) − s i z ( v ′ ) ⩽ n 2 siz(v)-siz(v')\leqslant \dfrac{n}{2} siz(v)siz(v)2n s i z ( v ′ ) ⩽ n 2 siz(v')\leqslant \dfrac{n}{2} siz(v)2n

我们可以发现,当我们以重心为根的时候,剩余的结点下方的所有子树都不超过 n 2 \dfrac{n}{2} 2n ,有且仅有上方一颗大小大于 n 2 \dfrac{n}{2} 2n 的子树,于是我们考虑对每个结点计算最大的 s i z ( v ′ ) siz(v') siz(v),显然 v ′ v' v 在上方的子树中,我们考虑换根 DP。

f ( u ) f(u) f(u) 表示 u u u 上方的子树中,大小不超过 n 2 \dfrac{n}{2} 2n 的最大的子树大小。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-draSM6wI-1635061099865)(C:\Users\47328\Desktop\信息学\notes\CF708C(1)].png)

我们可以发现,贡献来自父亲和兄弟子树,于是有
f ( u ) = max ⁡ { max ⁡ v ∈ s o n ( f a ( u ) ) , v ≠ u { s i z ( v ) ∣ s i z ( v ) ⩽ n 2 } , f ( f a ( u ) ) } f(u) = \max\{\max_{v\in son(fa(u)),v\neq u}\{siz(v)|siz(v)\leqslant \dfrac{n}{2}\},f(fa(u))\} f(u)=max{vson(fa(u)),v=umax{siz(v)siz(v)2n},f(fa(u))}
所以我们先找出重心,然后 DFS 预处理 max ⁡ v ∈ s o n ( f a ( u ) ) , v ≠ u { s i z ( v ) ∣ s i z ( v ) ⩽ n 2 } \max_{v\in son(fa(u)),v\neq u}\{siz(v)|siz(v)\leqslant \dfrac{n}{2}\} maxvson(fa(u)),v=u{siz(v)siz(v)2n} ,然后才 DP ,DP 过程中注意保证式子中 v ≠ u v\neq u v=u,最后判断是否有 n − s i z ( u ) − f ( u ) ⩽ n 2 n-siz(u)-f(u)\leqslant \dfrac{n}{2} nsiz(u)f(u)2n 即可。


void DFS1(int u,int pre)//找重心
{
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		DFS1(v,u);
		siz[u] += siz[v];
		mx[u] = max(mx[u],siz[v]);
			
	} 
	siz[u] ++;mx[u] = max(mx[u],n - siz[u]);
		
	return ; 
}


void Build(int u,int pre)//重新计算子树大小,并记录最大合法子树
{
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		Build(v,u);
		siz[u] += siz[v];
		if(siz[v] <= hn)
		{
			if(mxsub1[u] < siz[v])
			{
				mxsub2[u] = mxsub1[u];
				mxsub1[u] = siz[v];
			}
			else if(mxsub2[u] < siz[v]) mxsub2[u] = siz[v];
		}
			
	} 
	siz[u] ++;
	if(n - siz[u] <= hn)
	{
		if(mxsub1[u] < n - siz[u])
		{
			mxsub2[u] = mxsub1[u];
			mxsub1[u] = n - siz[u];
		}
		else if(mxsub2[u] < n - siz[u]) mxsub2[u] = n - siz[u];
	}
	return ;
}

void DFS2(int u,int pre)//DP
{
	int tmpmx = mxsub1[pre];
	if(siz[u] == mxsub1[pre]) tmpmx = mxsub2[pre];//不能自己更新自己
	f[u] = max(f[pre],tmpmx);
	for(int i = head[u],v = 0;i;i = e[i].nxt)
	{
		v = e[i].to;
		if(v == pre) continue;
		fa[v] = u;
		DFS2(v,u);
	}
	return ;
}

int main()
{
	scanf("%d",&n);hn = n >> 1;
	for(int i = 1;i < n;i ++)
	{
		scanf("%d%d",&x,&y);
		addedge(x,y);
	}
    //找重心
	DFS1(1,0);
	for(int i  = 1;i <= n;i ++)
		if(mx[i] < minn)
		{
			minn = mx[i];
			ctr = i;
		}
    // DP
	for(int i = 1;i <= n;i ++) siz[i] = 0;
	Build(ctr,0);
	DFS2(ctr,0);
    // 判断答案
	for(int i = 1;i <= n;i ++)
	{
		if(n - siz[i] - f[i] <= hn) printf("1 ");
		else printf("0 ");
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值