基环树浅谈

前言:怎么感觉基环树好像不是NOIP的知识点?(之前都没学过耶!

尽管如此,今天还是来学了学基环树。(开始复习!


一.概念:

其实基环树也没有多难,也就比普通的树多了一条边而已啦!

基环树,又叫环套树,是一种由点和边组成的图,含有一个环,而环上的每一个点又是一棵树的树根,即整颗树的根不是一个点而是一个环,所以称之为基环树。

基环树的处理主要分为两部分,大家应该也能想到,树的部分与环的部分(后者一般比较恶心),我们一般的操作就是断开环上的一条边,使其变成一棵树,然后跑DFS或DP。

基环树也分有向图和无向图,有向图中又有两种:内向树和外向树。

内向树指的是每个点只有一条出边的基环树

外向树指的是每个点只有一条入边的基环树

如下图就是一个内向树(外向树把所有边反一下

二.解决方法

1.找环:

无向图:拓扑排序

可以把度数为(1)的点放入一个队列,不断更新,最后度数还剩下(2)的点就在环中,代码如下:

queue <int> q;
inline void topo()
{
	for(;!q.empty();)q.pop();
	for(re int i=1;i<=n;i++)
		if(in[i]==1)q.push(i);
	for(re int u;!q.empty();)
	{
		u=q.front(),q.pop();
		for(re int i=hd[u],v;i;i=e[i].net)
			{
				v=e[i].v,in[v]--;
				if(in[v]==1)
					q.push(v);
			}
	}
}

有向图:DFS

这个更简单,直接爆搜,码量也很小!

void dfs(int u)
{
	vis[u]=1;
	for(re int v,i=hd[u];i;i=e[i].net)
	{
		v=e[i].v;
		if(vis[v])Sign=v;
		else dfs(v);
	}
}

(Sign) 就是环中的一个点。

2.断环:

我们处理基环树的方式一般就是断开一条环上的边,然后在处理,当然也可以枚举终点和起点,把环上的点放入一个数组并复制一遍,就可以拿一个长度为环上点的个数(n)的区间在上面枚举了。

三.例题:

1.P5022 旅行

本题为(2018)年提高(Day2)的题,其中(40)分,也就是(n=m)的情况,考了基环树。

因为(nle5000),可以(O(n^2))做,所以这题很适合断环,枚举环上的每一条边,打上标记不得走,就变成前(60\%)一样的题了。

讲完,上代码:

#pragma GCC optimize(2)
#include<bits/stdc++.h>
#define re register
using namespace std;
int n,m,len[5005],a[5005][5005],lon,ans[5005],in[5005],b[5005],KBL;
bool vis[5005],yor[5005],viss[5005][5005];
void dfs(int u)
{
	if(lon==n||KBL==-1)return;
	vis[u]=1,lon++;
	ans[lon]=u;
	if(ans[lon]<b[lon])KBL=1;
	else if(ans[lon]>b[lon]&&KBL==0){KBL=-1;return ;}
	for(re int i=1;i<=len[u];i++)
		if(vis[a[u][i]]==0&&viss[u][i]==0)dfs(a[u][i]);
}
queue <int> q;
inline void topo()
{
	for(;!q.empty();)q.pop();
	for(re int i=1;i<=n;i++)
		if(in[i]==1)q.push(i);
	for(re int u;!q.empty();)
	{
		u=q.front(),q.pop();
		for(re int i=1;i<=len[u];i++)
			if(in[a[u][i]])
			{
				in[a[u][i]]--;
				if(in[a[u][i]]==1)
					q.push(a[u][i]);
			}
	}
}
int main()
{
	memset(b,63,sizeof(b));
	scanf("%d%d",&n,&m);
	for(re int i=1,x,y;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		in[x]++,in[y]++;
		a[x][++len[x]]=y;
		a[y][++len[y]]=x;
	}
	for(re int i=1;i<=n;i++)sort(a[i]+1,a[i]+len[i]+1);
	if(n==m+1)
	{
		dfs(1);
		for(re int i=1;i<=lon;i++)
			b[i]=ans[i];
	}
	else if(n==m)
	{
		memset(b,63,sizeof(b));
		topo();
		for(re int i=1;i<=n;i++)
		{
			if(in[i]<2)continue;
			for(re int j=1;j<=len[i];j++)
			{
				if(in[a[i][j]]<2)continue;
				viss[i][j]=1,lon=0,KBL=0;
				memset(vis,0,sizeof(vis)); 
				dfs(1);
				viss[i][j]=0;
				if(KBL==1)
				for(re int k=1;k<=n;k++)
					b[k]=ans[k];
			}
		}
	}
	for(re int i=1;i<=n;i++)printf("%d ",b[i]);
	return 0;
}

这题是在本蒟蒻没学领接表的时候写的,所以存边的方式有点。。


2.P2607 骑士

分析一下题目,发现就是基环树森林版的没有上司的舞会,所以我们就可以枚举每一颗基环树,随便断开其中一条边,跑DP就行了,不过因为断了一条边,所以那一条边必须有一端不能取,所以各枚举两端强行不取的情况就完成了。

剩下的就和没有上司的舞会一样了。。

手起,码落:

#include<bits/stdc++.h>
#define max(x,y) ((x)>(y)?(x):(y))
#define re register
#define inf 1e18
using namespace std;
const int N=1000005;
struct edge{int v,net;}e[N];
int n,a[N],fa[N],rt,cnt,hd[N];
bool vis[N];
long long f[N][2],ans;
inline void add(int u,int v){e[++cnt].v=v,e[cnt].net=hd[u],hd[u]=cnt;}
void DP(int u)
{
	vis[u]=1,f[u][0]=0,f[u][1]=a[u];
	for(re int i=hd[u],v;i;i=e[i].net)
	{
		v=e[i].v;
		if(v==rt){f[v][1]=-inf;continue;}
		DP(v);
		f[u][1]+=f[v][0];
		f[u][0]+=max(f[v][0],f[v][1]);
	}
}
inline void work(int x)
{
	rt=x,vis[rt]=1;
	for(;vis[fa[rt]]==0;rt=fa[rt],vis[rt]=1);
	DP(rt);
	re long long mid=f[rt][0];
	rt=fa[rt];
	DP(rt);
	ans+=max(mid,f[rt][0]);
}
int main()
{
	scanf("%d",&n);
	for(re int i=1;i<=n;i++)
		scanf("%d%d",&a[i],&fa[i]),add(fa[i],i);
	for(re int i=1;i<=n;i++)
		if(vis[i]==0)work(i);
	printf("%lld",ans);
	return 0;
}
原文地址:https://www.cnblogs.com/jkzcr01-QAQ/p/13575211.html