基环树

定义

\(n\) 个点、\(n\) 条边组成的图。
可以理解为一棵树再加上一条边。
基环树不是树!
若干个基环树组成基环森林。
每一个基环树都有一个环,以这个环为根向外延伸出若干棵树。

无向基环树

\(n\) 个点、\(n\) 条边组成的无向图。
graph

外向基环树

每一个节点的入度为 \(1\)
graph (1)
优点是便于进行树上DP。

内向基环树

每一个节点的出度为 \(1\)
graph (2)
优点是便于找环。

处理

对于基环树,一般有两种处理方式:

  1. 将基环树视为一个环和若干个树,先计算树上的信息,在将环上的信息合并。
  2. 将基环树视为一棵树加一条边,先将这条边断掉进行计算,在考虑加上这一条边。

不同的题目会有不同的处理方式,选择最好写的一个就行。

例题

[ZJOI2008] 骑士\(^{louguP2607}\)

若这是一颗树,考虑树上DP。
\(f[x][0/1]\) 表示在以 \(x\) 为根的子树中,第 \(x\) 个点 选/不选 所获得的最大战力。
可以列出转移方程:

\[f[x][0]=\sum_{y\in son[x]}{max\{f[y][1],f[y][0]\}} \]

\[f[x][1]=\sum_{y\in sun[x]}{f[y][0]} \]

考虑环的问题。
我们先找到换上一边,将其中一条边为根进行树上DP,此时 \(f[x][0]\) 的值是正确的,但 \(f[x][1]\) 需要再绕环走一圈来进行更新。
使用前向星建立外向基环树,再记录每个节点的 \(fa\) 这样即有外向基环树方便DP的优点,也有内向基环树便于找环的优点。

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int NUM=1e6+10;

int fa[NUM],w[NUM],ans,f[NUM][2],n;
int h[NUM],nxt[NUM],to[NUM],tot;
bool vis[NUM],vis1[NUM];

void addedge(int x,int y){
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
	fa[y]=x;
}
void dfs(int x){
	vis[x]=1;
	f[x][1]=w[x];
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(vis[y]) continue;
		dfs(y);
		f[x][0]+=max(f[y][1],f[y][0]);
		f[x][1]+=f[y][0];
	}
}
void dfs1(int x){
	int root;
	for(root=x;vis1[root]==0;root=fa[root]) vis1[root]=1;
	dfs(root);
	x=fa[root];
	f[x][1]=f[x][0];
	for(x=fa[x];x!=root;x=fa[x]){
		f[x][1]=w[x];
		f[x][0]=0;
		for(int j=h[x];j;j=nxt[j]){
			int y=to[j];
			f[x][1]+=f[y][0];
			f[x][0]+=max(f[y][1],f[y][0]);
		}
	}
	f[root][1]=w[root];
	for(int i=h[root];i;i=nxt[i]){
		int y=to[i];
		f[root][1]+=f[y][0];
	}
	ans+=max(f[root][1],f[root][0]);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1,x;i<=n;++i){
		cin>>w[i]>>x;
		addedge(x,i);
	}
//	dfs1(1);
	for(int i=1;i<=n;++i){
		if(vis[i]==0) dfs1(i);
	}
	cout<<ans;
	return 0;
}

posted @ 2026-07-15 14:18  LZYXT  阅读(16)  评论(0)    收藏  举报