算法随笔——高级树形DP

换根DP学习博客
https://www.luogu.com.cn/article/wdk0q56f

一个粗暴的模板。

void dfs(int x,int fa)
{
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs(y,x);
		
	}
}

树上背包

指在树上做类似背包的问题,是树形 DP 的一种。

P2014 [CTSC1997] 选课

P2014

题意:在树上选出 m 个点的联通块使得权值最大。

做法

f[x][k] 表示以 x 为根选 k 个点,且 x 必选。

可以发现转移过程中可以枚举子树选 j 个,以 x 为根选 i 个,

则有转移 tmp[i+j]=max{f[x][i]+f[y][j]},这个过程称为合并子树。

注意这里的 f[x][i] 也代表了从前面枚举的子树中选的结果。

与普通的背包类似。

CODE

// Problem: P2014 [CTSC1997] 选课
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P2014
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-09-23 18:40:54
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 2e3+5;

int n,m,val[N];

int f[N][N];

int siz[N];
vector<int> v[N];
int tmp[N*2];

void dfs(int x)
{
	f[x][1] = val[x]; //初始设置
	siz[x] = 1;
	for (auto y : v[x])
	{
		dfs(y);
		rep(i,1,siz[x] + siz[y]) tmp[i] = -INF; 
        //因为根必选 ,因此 i 从 1 开始枚举
		for (int i = 1;i <= siz[x];i++) //枚举到当前已合并完的树的siz
			for (int j = 0;j <= siz[y];j++)
				tmp[i+j] = max(tmp[i+j],f[x][i]+f[y][j]);
		rep(i,1,siz[x] + siz[y]) f[x][i] = tmp[i]; //将tmp复制回f
		siz[x] += siz[y]; //记录当前的树size
	}
}

int main()
{
	cin >> n >> m;
	rep(i,1,n)
	{
		int x = rd();
		val[i ]= rd();
		v[x].push_back(i);
	}
	
	dfs(0);
	cout << f[0][m+1] << endl;
	return 0;
}

关于时间复杂度

这样似乎是 O(n3) 的,但实际上它是 O(n2) 的。

简单证明一下:

当两个背包合并时,代价为 O(sizu×sizv) 的,即背包里每个点对都会贡献 1,因此整体来看共有 O(n2) 个点对,也就只会产生 O(n2) 的贡献了。

重要结论

做树上背包时:

  • 如果合并子树的代价为子树大小之积,时间复杂度 O(n2)
  • 如果合并子树的代价为 O(min(sizv,M),min(sizu,M)),则时间复杂度为 O(nm)

P1273

https://www.luogu.com.cn/problem/P1273

简单题意

叶子节点有权值,每条边有花费,问最多连接多少个叶子节点到根,使得权值总和大于花费总和。

做法

dpi,x,j 为在第 x 个节点,使用前 i 个子节点的子树,选j个最大能获得的钱数。

答案就为 最大的 j 使得 dpi,x,j 非负。

考虑转移

dpi,x,j=max{dpi1,x,jk+dpsiz(son),son,kwx,son}

这是一个分组背包,每个子节点都是一个组,通过背包的经验,可以将 i 这一维滚动掉。
于是就没了。
注意枚举 k 时不能超过 j,也不能超过当前子节点的 size,这一细节可以优化成 O(N2) 的复杂度。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 4005;

int n,m;
vector<PII> v[N];

int c[N];
//dp[i][x][j] 表示在第 x 个节点,使用前 i 个子节点的子树,选j个最大能获得的钱数。
int dp[N][N]; 
int siz[N]; //siz表示子树下有多少叶子

void dfs(int x)
{
	if (x >= n-m+1)
	{ //判断为叶子节点
		siz[x] = 1;
		dp[x][1] = c[x]; //选一个为自己
		return;
	}
	for (int i = 0;i < v[x].size();i++) //根据分组背包,先枚举组别
	{
		auto [y,w] = v[x][i];
		dfs(y);
		siz[x] += siz[y];
		//为了滚动,倒序枚举
		for (int j = siz[x];j >= 0;j--) //前 i 个j上界为当前的size
		{
			for (int k = 0;k <= siz[y];k++) //枚举决策
			{
				if (k > j) break;
				dp[x][j] = max(dp[x][j],dp[x][j-k] + dp[y][k] - w);
			}
		}
	}
}

int main()
{
	memset(dp,-INF,sizeof dp);
	cin >> n >> m;
	for (int i = 1;i <= n-m;i++) 
	{
		int k = read();
		for (int j = 1;j <= k;j++)
		{
			int x = read(),c= read();
			v[i].push_back({x,c});	
		}
		dp[i][0] = 0; //初始化,选0个代价为0
	}
	
	for (int i = 1;i <= m;i++) c[i+n-m] = read();
	
	dfs(1);
	
	for (int k = m;k >= 0;k--)
	{
		if (dp[1][k] >= 0)
		{
			cout << k << endl;
			return 0;
		}
	}
	return 0;
}

P1272 重建道路

题意:求分离出一个大小为 p 的联通块最少需要断掉几条边

dpi,p 代表 分离出以 i 为根大小为 p 的联通块的代价。

乍一看不太好 dp,可以这样想:我们树形 dp 的过程就是一个合并子树的过程,那么最开始根节点是没有接任何子树的,

相当于一个孤点。则初值设为 fi,1=0

又因为 fi,0 就表示当前 i 这个子树相当于直接不要了,所以要把他和他父亲的这条边断掉,即 fi,0=1

初值设完后,dp 就很简单了,转移为:

fx,i+fy,jfx,i+j

取最小值即可。

比较板子的一题。

// Problem: P1272 重建道路
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1272
// Memory Limit: 125 MB
// Time Limit: 1000 ms
// Author: Eason
// Date:2024-09-23 21:50:57
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 155;

int n,p;
vector<int> v[N];

int f[N][N];
int siz[N];
int tmp[N*2];

void dfs(int x)
{
	f[x][0] = 1;
	f[x][1] = 0;
	siz[x] = 1;
	for (auto y : v[x])
	{
		dfs(y);
		
		for (int i = 1;i <= siz[x] + siz[y];i++) tmp[i] = INF;
		
		for (int i = 1;i <= siz[x];i++)
			for (int j = 0;j <= siz[y];j++)
				tmp[i+j] = min(tmp[i+j],f[x][i] + f[y][j]);
		for (int i = 1;i <= siz[x] + siz[y];i++) f[x][i] = tmp[i];
		siz[x] += siz[y]; 
	}
}


int main()
{
	cin >> n >> p;
	rep(i,1,n-1)
	{
		int x = rd(),y = rd();
		v[x].push_back(y);
	}
	memset(f,INF,sizeof f);
	dfs(1);
	
	int ans = INF;
	rep(i,1,n) ans = min(ans,f[i][p] + (i!=1));
	
	cout << ans << endl;
	
	return 0;
}

P4516 [JSOI2018] 潜入行动

树形DP无敌好题。

=P2014 + P2279

需要较为细心的分析和讨论。

题目分析

题意:给定一棵树,选择一个点会给与其相连的所有点染色(不包括自己),问选择 k 个点使得所有点都染上色的方案数

n105,k100

做法

dpx,k,0/1,0/1 表示:

考虑以 x 为根的子树,选择 k 个点,0/1 是否被染色,0/1 是否选择该点。

先考虑比较简单的两种情况,该点被其他点染色时,其儿子可选可不选,直接做树上背包转移即可。


//初始值
f[x][0][1][0] = 1;
f[x][1][1][1] = 1;


// 被染色且选
rep(i,0,k) tmp[i] = 0;

for (int i = 1;i <= min(siz[x],k);i++)
    for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
        (tmp[i+j] += 1ll*f[x][i][1][1] * (f[y][j][1][1]+f[y][j][1][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][1] = tmp[i];

// 被染色且不选
rep(i,0,k) tmp[i] = 0;

for (int i = 0;i <= min(siz[x],k);i++)
    for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
        (tmp[i+j] += 1ll*f[x][i][1][0] * (f[y][j][0][1]+f[y][j][0][0])%mod)%=mod;
rep(i,0,k) f[x][i][1][0] = tmp[i];

然后是 x 点没有被染色的情况,先考虑其初值的设置。

还是用合并子树的思路,最开始时可以看成只有 x 一个孤点,所以选不选都无法染自己,则有:

f[x][1][0][1] = 0;
f[x][0][0][0] = 0;

考虑合并子树的过程,假设考虑到 y 这棵子树:

  1. y 不选,则转移为 :f[x][i][0][0/1]×f[y][j][0/1][0]f[x][i+j][0][0/1]

  2. y 选,则此时 x 相当于被染色了,于是转移有:f[x][i][1][0/1]×f[y][j][0/1][1]f[x][i+j][0][0/1]

Code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 1e5+5,M = 105,mod = 1e9 +7;

int n,k;

vector<int> v[N];

int f[N][M][2][2];

int siz[N];

int tmp[N*2];


void dfs(int x,int fa)
{
	siz[x] = 1;
	f[x][0][0][0] = 0;
	f[x][0][1][0] = 1;
	f[x][1][0][1] = 0;
	f[x][1][1][1] = 1;
	
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs(y,x);
		
		
		// 没被染色且不选
		rep(i,0,k) tmp[i] = 0;
		
		for (int i = 0;i <= min(siz[x],k);i++)
			for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
			{
				(tmp[i+j] += 1ll*f[x][i][0][0] * f[y][j][0][0]%mod) %= mod;
				if (j >= 1)
				(tmp[i+j] += (1ll*f[y][j][0][1] *f[x][i][1][0])%mod)%=mod;
			}
		
		rep(i,0,k) f[x][i][0][0] = tmp[i];
		
		// 没被染色且选
		
		rep(i,0,k) tmp[i] = 0;
		
		for (int i = 1;i <= min(siz[x],k);i++)
			for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
			{
				(tmp[i+j] += 1ll*f[x][i][0][1] * f[y][j][1][0]%mod)%=mod;
				if (j >= 1)
					(tmp[i+j] += 1ll*f[y][j][1][1] *f[x][i][1][1]%mod)%=mod;
			}
		
		rep(i,0,k) f[x][i][0][1] = tmp[i];
		
		
		
		
		// 被染色且不选
		rep(i,0,k) tmp[i] = 0;
		
		for (int i = 0;i <= min(siz[x],k);i++)
			for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
				(tmp[i+j] += 1ll*f[x][i][1][0] * (f[y][j][0][1]+f[y][j][0][0])%mod)%=mod;
		rep(i,0,k) f[x][i][1][0] = tmp[i];
		
		// 被染色且选
		rep(i,0,k) tmp[i] = 0;
		
		for (int i = 1;i <= min(siz[x],k);i++)
			for (int j = 0;j <= min(siz[y],k) && (i+j<=k);j++)
				(tmp[i+j] += 1ll*f[x][i][1][1] * (f[y][j][1][1]+f[y][j][1][0])%mod)%=mod;
		rep(i,0,k) f[x][i][1][1] = tmp[i];
		
		
		
		
		
		
		
		siz[x] += siz[y];
	}
	//print(x);
}

int main()
{
	cin >> n >> k;
	rep(i,1,n-1)
	{
		int x = rd(),y = rd();
		v[x].push_back(y);
		v[y].push_back(x);
	}
	
	dfs(1,0);
	
	cout << (f[1][k][0][1] + f[1][k][0][0])%mod <<endl;
	
	return 0;
}																								

注意这里 y 是否被染色的状态都取决于 x 是否选的状态。

还有就是应该先处理 x 没有被染色的情况,因为这种情况需要用到 x 以及前面已经合并的子树的 f[x][i][1][0/1],不包含当前子树。

总结

树形背包是一种将树形DP 与经典背包问题结合起来的问题,做这种题的时候一定要熟悉合并子树的思路,即每次考虑一棵子树的过程可以看出是合并了一棵子树的过程。有利于简化问题,理清思路。

换根dp

又称二次扫描,是树形dp的一种。

特点

image

一般比较方便求出以一个点为根的答案,但题目要求求出以所有点为根的每个答案,暴力求是 O(N2) 的,可以用换根dp优化。

通常用两次 dfs,第一次求出以 1 为根的答案并预处理一些方便转移的东西,第二次换根,求出每个点为根的答案。

Tree Painting

例题:Tree Painting

简明题意

fiisize,求出 maxfi

做法

可以求出以 1 为根的答案,考虑从 以 fa 为根转移到 x的贡献。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 2e5+5;

int n;

vector<int> v[N];

int siz[N];
ll ans[N];

void dfs1(int x,int fa)
{
	siz[x] = 1;
	for (auto y : v[x])
	{
		if (y != fa)
		{
			dfs1(y,x);
			siz[x] += siz[y];
		}
	}
}

void dfs2(int x,int fa)
{
	if (x != 1)
	ans[x] = ans[fa] + (n-siz[x]) - siz[x];
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs2(y,x);
	}
}

int main()
{
	cin >> n;
	for (int i = 1;i <= n-1;i++)
	{
		int x = read(),y = read();
		v[x].push_back(y);
		v[y].push_back(x);
	}
	dfs1(1, 0);
	for (int i = 1;i <= n;i++) ans[1] += siz[i];
	dfs2(1,0);
	ll maxn = 0;
	for (int i = 1;i <= n;i++) maxn = max(maxn,ans[i]);
	cout << maxn << endl;
	
	return 0;
}

CF771C

一道换根 dp 好题。
题意为求出 ijdis[i][j]k
维护一下 x 到子树的点距离模 k 为 c的数量和x 到整棵树树的点距离模 k 为 c的数量。
加上一些容斥便可以完成转移。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 2e5+5;

int n;

vector<int> v[N];
int k;
int cnt1[N][15],cnt2[N][15];
int ans[N];
int dep[N];


void dfs1(int x,int fa)
{
	dep[x] = dep[fa] + 1;
	cnt1[x][0]++;
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs1(y,x);
		for (int i = 0;i < k;i++) cnt1[x][i] += cnt1[y][(i-1+k)%k];
	}
}

void dfs2(int x,int fa)
{
	if (x != 1)
	{
		ans[x] = ans[fa] -cnt1[x][0] + cnt2[fa][0] - cnt1[x][k-1];
//	cout << x << ' ' << ans[x] << ' ' << cnt1[x][0] << ' ' << cnt2[fa][0] << ' ' << cnt1[x][k-1] << endl;
		for (int i = 0;i < k;i++) cnt2[x][i] = cnt2[fa][(i-1+k)%k] - cnt1[x][(i-2+2*k)%k] + cnt1[x][i];
	}
	//cout << x << endl;

	
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs2(y,x);
	}
}

signed main()
{
	cin >> n >> k;
	for (int i = 1;i <= n-1;i++)
	{
		int x = read(),y = read();
		v[x].push_back(y);
		v[y].push_back(x);
	}
	
	dep[0 ] = -1;
	dfs1(1,0);

	for (int i = 1;i <= n;i++) ans[1] += (dep[i]+k-1)/k;
//	cout << ans[1] << endl;
	for (int i = 0;i < k;i++) cnt2[1][i] = cnt1[1][i];
	dfs2(1,0);
	int res = 0;
	for (int i = 1;i <= n;i++) res += ans[i];
	cout << res / 2 << endl;
	return 0;
}

P6419 [COCI2014-2015#1] Kamp

题意:给定一棵树,边带权,有 k 个关键点,问从每个 st[1,n] 出发,到达每个点至少一次的路径和最小。

n,k5×105

我们可以先考虑到每个点最后回到 st 的答案 。设 g[x] 表示考虑 x 为根的子树,到达子树所有关键点在回到 x 的最小路径和。

转移很简单:g[x]=(2×c+g[son])(siz[son]>0)

其中 cxson 的边权。

做完这个后,我们可以发现答案就是 g[st] 减去 st 到某一个关键点的最长链。

于是一个粗暴的做法就出来了:第一次 dfs 算出 g[x],第二次用线段树维护最长链,做完了。

其中线段树维护需要转化为 dfn ,然后子树加就转化为 [dfnx,dfnx+sizx1] 区间加。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 5e5+5;

int n,k;
vector<PII> v[N];

int pos[N];
int siz[N];
int g[N];
bool fk[N];
int dep[N];
struct node
{
	int mx,tag;
}t[N<<2];

void stf(int k,int v)
{
	t[k].mx += v;
	t[k].tag += v;
}

void pushdown(int k)
{
	if (t[k].tag)
	{
		stf(k<<1,t[k].tag);
		stf(k<<1|1,t[k].tag);
		t[k].tag = 0;
	}
}

void modify(int k,int l,int r ,int x,int y,int v)
{
	if (x > r || y <l) return;
	if (x <=l && r <= y)
	{
		stf(k,v);
		return;
	}
	
	int mid =l +r >> 1;
	pushdown(k);
	modify(k<<1,l,mid,x,y,v);
	modify(k<<1|1,mid+1,r,x,y,v);
	t[k].mx = max(t[k<<1].mx,t[k<<1|1].mx);
	
}


int dfn[N],id[N],tot;
int sz[N];

void dfs1(int x,int fa,int c)
{
	if (fk[x]) siz[x] = 1;
	dep[x] = dep[fa] + c;
	dfn[x] = ++tot;
	id[tot] = x;
	sz[x] = 1;
	for (auto [y,c] : v[x])
	{
		if (y == fa) continue;
		dfs1(y,x,c);
		siz[x] += siz[y];
		if (siz[y])
		g[x] += c * 2 + g[y];
		sz[x] += sz[y];
	}
}

int ans[N];

void dfs2(int x,int fa,int len)
{
	if(x!=1)
	{
		modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,-2*len);
		modify(1,1,n,1,n,len);
		if (siz[x] > 0)
		{
			 if(k-siz[x] == 0) g[x] = g[fa] - 2 * len;
			 else g[x] = g[fa];
		}
		else g[x] = 2 * len +g[fa];	
		ans[x] = g[x] - t[1].mx;
	}
	
	for (auto [y,c] : v[x])
	{
		if (y == fa) continue;
		dfs2(y,x,c);
	}
	modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,2*len);
	modify(1,1,n,1,n,-len);
}

signed main()
{
	cin >> n >> k;
	
	rep(i,1,n-1)
	{
		int x = rd(),y = rd(),z = rd();
		v[x].push_back({y,z});
		v[y].push_back({x,z});
	}
	
	rep(i,1,k)
		pos[i] = rd(),fk[pos[i]] = 1;
	
	dep[0] = 0;
	dfs1(1,0,0);
	modify(1,1,n,1,n,-INF);
	
	rep(i,1,k) modify(1,1,n,dfn[pos[i]],dfn[pos[i]],INF + dep[pos[i]]);
	ans[1] = g[1] - t[1].mx;
	dfs2(1,0,0);
	rep(i,1,n) cout << ans[i] << endl;
	return 0;
}

P2279 [HNOI2003] 消防局的设立

一道较复杂的树形dp。
设出 fi,st 代表答案。
st=0 表示依赖自己,自己选。
st=1 表示依赖儿子,自己不选。
st=2 表示依赖孙子,自己不选。
st=3 表示依赖爸爸,自己不选。
st=4 表示依赖爷爷,自己不选。

然后转移一下。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
int read()
{
	int f=1,k=0;char c = getchar();
	while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
	return k*f;
}

const int N = 1005;

vector<int> v[N];

int f[N][5];

void dfs(int x,int fa)
{
	if (x != 1 && v[x].size() == 1)
	{
		f[x][0] = 1;
		f[x][1] = INF;
		f[x][2] = INF;
		f[x][3] = 0;
		f[x][4] = 0;
		return;
	}
	f[x][2] = 0;
	f[x][1] = 0;
	f[x][0] = 1;
	f[x][3] = 0;
	f[x][4] = 0;
	for (auto y : v[x])
	{
		if (y == fa) continue;
		dfs(y,x);
		int minn = INF;
		for (int i = 0;i < 4;i++) minn = min(f[y][i],minn);
		f[x][0] += minn;
		minn = INF;
		for (int i = 0;i < 5;i++) if (i != 3) minn = min(f[y][i],minn);
		f[x][3] += minn;
		minn = INF;
		for (int i = 0;i< 3;i++) minn = min(minn,f[y][i]);
		f[x][4] += minn;
		f[x][1] += min(f[y][0],min(min(f[y][1],f[y][2]),f[y][4]));
		f[x][2] += min(f[y][1],f[y][2]);
	}
	int sum1 = f[x][1];
	int sum2 = f[x][2];
	f[x][1] = INF,f[x][2] = INF;
	for (auto y : v[x])
	{
		if (y == fa) continue;
		f[x][1] = min(f[x][1],sum1-min(f[y][0],min(min(f[y][1],f[y][2]),f[y][4])) + f[y][0]);
		if (!(y != 1 && v[y].size() == 1))
		f[x][2] = min(f[x][2],sum2-min(f[y][1],f[y][2]) + f[y][1]);
	}
}


signed main()
{
	int n;
	cin >> n;
	for (int i = 2;i <= n;i++) 
	{
		int x = read();
		v[i].push_back(x);
		v[x].push_back(i);
	}
	memset(f,INF,sizeof f);
	dfs(1,0);
	
	cout << min(min(f[1][0],f[1][1]),f[1][2]) << endl;
	
	return 0;
}

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