【基环树】笔记

姗姗来迟的总结

1. 什么是基环树

一般的,对于一个 N N N 个点, N N N 条边的连通无向图而言(也可称之为在一颗树上多了一条边,即非树边),我们就可以把它称之为基环树

就像这样:

基环树问题的解决方法和普通的树的问题的解决方法差不多,无非就是几步走:

首先,这一类问题的答案分为两种:环对答案作出了贡献和环没对答案作出贡献

对于情况 1 1 1,就是一个正常的树上的问题,而对于情况 2 2 2,我们要走一下的流程

首先,找到环,然后,按照题目要求,拆掉环上的一条边,然后,整棵基环树就成了一颗普通的树,求得另一个可行答案后,与情况 1 1 1 所得到的答案进行比较即可

很简单,不是吗

2. 关于找环

找环方法其实还不少,这里简单介绍两种:

2.1. Tarjan 找环

众所周知,Tarjan 可以用来缩点,那么,我们考虑在基环树上缩点。显然,环会被缩成一个大点,我们只需要将大点里面的小点找出来即可,这非常容易实现,因为每一个点所对应的大点编号都放在 Tarjan 中的scc数组里面,无需赘述

2.2. 并查集找环

正常来说,我们把树上的点塞入并查集时,是不会出现“有一组节点是在同一集合里面”的情况,但是,基环树由于有一条非树边,所出现这种情况,那么此时,我们就可以以其中一个点作为起点进行搜索(注意,这两个节点所连接的边时不能走的),用栈来维护搜素路径上的点,进入节点时塞入,回溯时弹出,直到遇到了另一个节点为止

3. 一些例题

3.1. 骑士

这就是一道经典的基环树最大带权独立点集问题,与没有上司的舞会高度相似.

首先,我们需要找到环上的一条边 ( x , y ) (x,y) (x,y),并将这条边拆掉,那么,问题就变成了没有上司的舞会

可是, ( x , y ) (x,y) (x,y) 毕竟也是一条边, x , y x,y x,y 至少有一个不选,而拆环后无法考虑这个情况,因此,我们还要将其分为两种情况:强制不选 x x x 和强制不选 y y y

所以,需要跑两次 DP,分别考虑这两种情况

另外注意:这道题可能会出现重边,而有重边的基环树就相当于一颗普通的树,需要特判

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#define re register
#define int long long
using namespace std;
const int N=2000005;
int n,m,x[N],y[N];
int a[N];
int ver[N],edge[N],Next[N],head[N],len;
void add(int x,int y,int z){
	ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int fa[N];
int xx,yy;
void make(int x){
	for(re int i=1;i<=x;i++){
		fa[i]=i;
	}
}
int find(int x){
	if(x==fa[x]){
		return x;
	}
	return fa[x]=find(fa[x]);
}//并查集
bool flag[N];
int dp[N][2];
int ans;
void dfs(int x,int No,int kkk){
	//用(kkk,No)代表那条被拆掉的边
	flag[x]=1;
	if(x==No){//强制不选的点需要单独初始化
		dp[x][0]=dp[x][1]=0;
	}else{
		dp[x][0]=0,dp[x][1]=a[x];
	}
	for(re int i=head[x];i;i=Next[i]){
		int y=ver[i],z=edge[i];
		if(y==No&&x==kkk&&z==1){//如果遍历到被拆掉的边,则跳过,除非它是重边
			continue;
		}
		if(!flag[y]){
			dfs(y,No,kkk);
			dp[x][0]+=max(dp[y][0],dp[y][1]);
			dp[x][1]+=dp[y][0];
		}
	}
}
int read(){
	int a=1,b=0;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		a=ch=='-'?-1:1,ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		b=(b<<1)+(b<<3)+ch-'0',ch=getchar();
	}
	return a*b;
} 
signed main(){
	n=read(),make(n);
	for(re int i=1;i<=n;i++){
		a[i]=read(),y[i]=read();
		x[i]=i;
	}
	//玄学(?)方法特判重边:2代表重边
	for(re int i=1;i<=n;i++){
		if(x[i]==y[y[i]]){
			add(x[i],y[i],2);
			add(y[i],x[i],2);
		}else{
			add(x[i],y[i],1);
			add(y[i],x[i],1);
		}
	}
	for(re int i=1;i<=n;i++){
		int xx=find(x[i]),yy=find(y[i]);
		if(xx^yy){
			fa[xx]=yy;
		}else{//找到了非树边
			xx=x[i],yy=y[i];
			dfs(yy,xx,yy);//第一次DP——强制不选xx
			int ans1=max(dp[yy][0],dp[yy][1]);
			for(re int i=1;i<=n;i++){
				flag[i]=0;
			}
			dfs(xx,yy,xx);//第二次DP——强制不选yy
			ans+=max(max(dp[xx][0],dp[xx][1]),ans1);
			for(re int i=1;i<=n;i++){
				flag[i]=0;
			}
		}
	}
	printf("%lld",ans);
	return 0;
}

3.2. Island

一句话题意:求基环树森林的直径和

解决了单颗基环树的直径问题,这道题也就迎刃而解了

显然,分为两种情况讨论

  1. 直径经过了环
  2. 直径没经过环

对于情况 1 1 1,我们只需要以环上的每一个节点作为根节点跑一次DP,然后比最大值(注意:此时的子树不应包含除根节点以外的环上节点)

对于情况 2 2 2,考虑:对于两个环上节点 i , j i,j i,j 而言,设dp[i],dp[j]分别表示以 i , j i,j i,j 为根节点的子树的直径,dis[i][j]表示从 i i i j j j 的最长距离,且若直径经过了这两节点,则直径为 d p [   i   ] + d i s [   i   ] [   j   ] + d p [   j   ] dp[\ i\ ]+dis[\ i\ ][\ j\ ]+dp[\ j\ ] dp[ i ]+dis[ i ][ j ]+dp[ j ]

.若我们设pre[i]为环上以某一结点为起点,到第 i i i 节点的前缀和,sum表示环上的权值总和,那么,上述式子可以改写为: max ⁡ ( p r e [   j   ] − p r e [   i   ] , s u m − p r e [   j   ] + p r e [   i   ] ) + d p [   i   ] + d p [   j   ] \max(pre[\ j\ ]-pre[\ i\ ],sum-pre[\ j\ ]+pre[\ i\ ])+dp[\ i\ ]+dp[\ j\ ] max(pre[ j ]pre[ i ],sumpre[ j ]+pre[ i ])+dp[ i ]+dp[ j ],更进一步的,则有: max ⁡ ( d p [   i   ] − p r e [   i   ] + d p [   j   ] + p r e [   j   ] , s u m + p r e [   i   ] + d p [   i   ] + d p [   j   ] − p r e [   j   ] ) \max(dp[\ i\ ]-pre[\ i\ ]+dp[\ j\ ]+pre[\ j\ ],sum+pre[\ i\ ]+dp[\ i\ ]+dp[\ j\ ]-pre[\ j\ ]) max(dp[ i ]pre[ i ]+dp[ j ]+pre[ j ],sum+pre[ i ]+dp[ i ]+dp[ j ]pre[ j ])

则通过环上的直径的值为:

a n s = max ⁡ { max ⁡ ( d p [   i   ] − p r e [   i   ] + d p [   j   ] + p r e [   j   ] , s u m + p r e [   i   ] + d p [   i   ] + d p [   j   ] − p r e [   j   ] ) } ans=\max\{\max(dp[\ i\ ]-pre[\ i\ ]+dp[\ j\ ]+pre[\ j\ ],sum+pre[\ i\ ]+dp[\ i\ ]+dp[\ j\ ]-pre[\ j\ ])\} ans=max{max(dp[ i ]pre[ i ]+dp[ j ]+pre[ j ],sum+pre[ i ]+dp[ i ]+dp[ j ]pre[ j ])}

此时,若直接枚举 i , j i,j i,j,则时间复杂度为 O ( n 2 ) O(n^2) O(n2),但是,我们可以用单调队列维护dp[j]+pre[j]dp[j]-pre[j]的最大值,这样,可以将时间复杂度降为 O ( n ) O(n) O(n)

同样注意:这道题可能有重边

#include<cstdio>
#include<algorithm>
#define int long long
using namespace std;
const int N=4000005;
int n,x[N],y[N],z[N];
int ver[N],edge[N],Next[N],head[N],len;
void add(int x,int y,int z){
	ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int fa[N];
void make(int x){
	for(int i=1;i<=x;i++){
		fa[i]=i;
	}
}
int find(int x){
	if(x==fa[x]){
		return x;
	}
	return fa[x]=find(fa[x]);
}
int xx,yy;
int Ring[N],Ring_len,Ring_z,Ring_OK;
int Ring_z_pre[N];
bool Is_Ring[N];
void Find_Ring(int x,int fa){//找环
	Ring[++Ring_len]=x;
	if(x==yy){
		Ring_OK=1;
		return ;
	}
	for(int i=head[x];i;i=Next[i]){
		int y=ver[i],z=edge[i];
		if(y^fa){
			Ring_z+=z;
			Ring_z_pre[Ring_len+1]=Ring_z_pre[Ring_len]+z;
			Find_Ring(y,x);
			if(Ring_OK){
				return ;
			}
			Ring_z-=z;
		}
	}
	Ring_len--;
}
int dp[N][3];
int Stop_x,Stop_y;
void DP(int x,int fa){//求单棵树的直径 
	int zzz=0;
	for(int i=head[x];i;i=Next[i]){
		int y=ver[i],z=edge[i];
		if(y==fa||Is_Ring[y]){//不能回溯,不能去环上跑DP 
			continue;
		}
		if(x==Stop_x&&y==Stop_y){//重边特判:找到权值最大的一条边,所以不急着DP 
			zzz=max(zzz,z);
			continue;
		}
		DP(y,x);
		dp[x][2]=max(dp[x][2],dp[y][2]);
		int tot=dp[y][0]+z;
		if(dp[x][0]<tot){
			dp[x][1]=dp[x][0];
			dp[x][0]=tot;
		}else if(dp[x][1]<tot){
			dp[x][1]=tot;
		}
	}
	if(x==Stop_x){//特判 
		for(int i=head[x];i;i=Next[i]){
			int y=ver[i];
			if(y==Stop_y){
				int z=zzz;
				DP(y,x);
				dp[x][2]=max(dp[x][2],dp[y][2]);
				int tot=dp[y][0]+z;
				if(dp[x][0]<tot){
					dp[x][1]=dp[x][0];
					dp[x][0]=tot;
				}else if(dp[x][1]<tot){
					dp[x][1]=tot;
				}
				break;
			}
		}
	}
	dp[x][2]=max(dp[x][2],dp[x][0]+dp[x][1]);
}
int ans,sum;
int q[N],Head=1,Tail;
int q1[N],Head1=1,Tail1;
signed main(){
	scanf("%lld",&n);
	make(n);
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",&y[i],&z[i]);
		x[i]=i;
		add(x[i],y[i],z[i]),add(y[i],x[i],z[i]);
	}
	for(int kkk=1;kkk<=n;kkk++){
		int tot_x=find(x[kkk]),tot_y=find(y[kkk]);//并查集判环 
		if(tot_x==tot_y){
			xx=x[kkk],yy=y[kkk];
			Ring_z+=z[kkk];
		}else{
			fa[tot_x]=fa[tot_y];
		}
		if(!xx){
			continue;
		}
		Find_Ring(xx,yy);//判环 
		if(!Ring_len){//特判:重边 
			Stop_x=xx,Stop_y=yy;
			DP(xx,0);
			ans+=dp[xx][2];
			Ring_z=0;
			xx=yy=0;
			for(int i=1;i<=n;i++){
				dp[i][0]=dp[i][1]=dp[i][2]=0;
			}
			continue;
		}
		for(int i=1;i<=Ring_len;i++){
			Is_Ring[Ring[i]]=1;
		}
		for(int i=1;i<=Ring_len;i++){//不经过环的情况
			DP(Ring[i],0);
			sum=max(sum,dp[Ring[i]][2]);
		}
		for(int i=Ring_len-1;i>=1;i--){
			//单调队列优化DP
			while(Head<=Tail&&dp[Ring[q[Tail]]][0]+Ring_z_pre[q[Tail]]<=dp[Ring[i+1]][0]+Ring_z_pre[i+1]){
				Tail--;
			}
			q[++Tail]=i+1;
			while(Head1<=Tail1&&dp[Ring[q1[Tail1]]][0]-Ring_z_pre[q1[Tail1]]<=dp[Ring[i+1]][0]-Ring_z_pre[i+1]){
				Tail1--;
			}
			q1[++Tail1]=i+1;
			sum=max(sum,max(Ring_z+Ring_z_pre[i]+dp[Ring[i]][0]+dp[Ring[q1[Head1]]][0]-Ring_z_pre[q1[Head1]],
				                dp[Ring[i]][0]-Ring_z_pre[i]+dp[Ring[q[Head]]][0]+Ring_z_pre[q[Head]]));
		}
		ans+=sum;
		sum=0;
		for(int i=1;i<=Ring_len;i++){
			dp[Ring[i]][0]=dp[Ring[i]][1]=dp[Ring[i]][2]=0;
			Is_Ring[Ring[i]]=0;
			Ring_z_pre[i]=0;
			Ring[i]=0;
		}
		Head=Head1=1,Tail=Tail1=0;
		Ring_len=Ring_z=Ring_OK=0;
		xx=0,yy=0;
		//注意这道题的初始化有点多
	}
	printf("%lld",ans);
	return 0;
}

.

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值