连通性问题大杂烩

前言

连通性问题确实时一大比较难啃得蛋糕,每次都要先学习一遍,还不如一次学到通

无向图的连通性问题

求割点

连通图:连通图内的所有点都可以互相到达
割点:将割点删掉后整张图不连通

定理1:

一个点s是割点,当且仅当s作为该连通图的根时,会把连通图分为不相连的几部分

定理2:

一个非根节点u是割点,当且仅当u存在一个子节点v不存在一条边连向u的祖先

一开始我认为只要存在一条边连向祖先就不算割点,但这里举出反例:

实现

考虑dfs序,就是将节点编号为dfs遍历到的顺序
我们设一个dfn表示dfs序,设low表示当前节点能回到的最大祖先的编号,只要存在子节点v的\(low[v]>=dfn[u]\) 就可以了

代码

#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=2e5+5;
int n,m,u,v,tot,ans;
int dfn[N],low[N],vis[N],dot[N];
vector<pii>b[N];
void dfs(int x,int nxt){
	dfn[x]=low[x]=++tot;
	vis[x]=1;
	bool cnt=0;
	int num=0;
	for(auto i:b[x]){
		if(nxt==i.second)  continue;//这里大抵是没有用的
		int k=i.first;//要开局部变量,不要像我一样手懒开全局变量寄掉
		if(!vis[k]){
			num++;
			dfs(k,i.second);
			low[x]=min(low[x],low[k]);
			if(low[k]>=dfn[x])  cnt=1;
		}
		else{
			low[x]=min(low[x],dfn[k]);//无向图中也可以写low
		}
	}
	if(!nxt){
		if(num>1)  dot[++ans]=x;//注意特判根节点的情况
	}
	else if(cnt){
		dot[++ans]=x;
	}  
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		b[u].push_back({v,i});
		b[v].push_back({u,i});
	}
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			dfs(i,0);
		}
	}
	sort(dot+1,dot+ans+1);
	printf("%d\n",ans);
	for(int i=1;i<=ans;i++){
		printf("%d ",dot[i]);
	}
}

割边

只需要把 \(low[v]>=dfn[u]\) 改成 \(low[v]>dfn[u]\) 就行了,因为u的所有后代都到达不了v,从u->v的边一定是割边

边双连通分量

在边双连通分量中,两点间存在至少两条边不重复的路径
类似于割点的办法,但有所不同
将遍历到的点放入栈,若一个点满足 \(low[v]>=num[u]\),则将这个点作为这个边双连通分量中的头,将栈中在它上面的点依次弹出,这之中的点就构成了点双连通分量

点双连通分量

同理两点间存在至少两条点不重复的路径
把边双改一下栈中存的是边就可以了
因为点双中一个点可能在多个点双之中
不信,你画个8试试

有向图的连通性问题

tajan

其实和边双写法一样,我记得以前有人说回溯边只能用dfn不能用low,但是我证实了是可以的!不过也不保准
为什么要用vis数组记录是否在栈中呢?
因为不在栈中的点代表已经单独构成强连通分量了,不可能再与这个点构成强连通分量了,若有横叉边,就是从这个强连通分量到另一个强连通分量,但是这条边并不会到达它的祖先不符合low的定义,反而一定会影响其的low值,所以只能用栈中的点更新
可以用此图直观表示一下:

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int dfn[N],low[N],vis[N],s[N],col[N],en[N],a[N],num[N],vi[N],dp[N];
int cnt,colnum,n,m,u,v,top,ans;
vector<int>b[N];
vector<int>newb[N];
void dfs1(int x){
	dfn[x]=low[x]=++cnt;
	s[++top]=x;
	vis[x]=1;
	for(auto i:b[x]){
		if(!dfn[i])  dfs1(i);
		if(vis[i])  low[x]=min(low[x],low[i]);
	}
	if(low[x]==dfn[x]){
		colnum++;
		while(s[top]!=x){
			col[s[top]]=colnum;
			num[colnum]+=a[s[top]];
			vis[s[top]]=0;
			top--;
		}
		col[x]=colnum;
		num[colnum]+=a[x];
		vis[x]=0;
		top--;
	}
}
void dfs2(int x){
	vi[x]=1;
	for(auto i:newb[x]){
//		printf("神金%d %d\n",x,i);
		if(!vi[i])  dfs2(i);
//		printf("他妈%d %d\n",x,i);
		dp[x]=max(dp[x],dp[i]+num[x]);
		ans=max(dp[x],ans);
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d",&u,&v);
		b[u].push_back(v);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i])  dfs1(i);
	}
	for(int i=1;i<=colnum;i++){
		dp[i]=num[i];
		ans=max(dp[i],ans);
	}
	for(int i=1;i<=n;i++){
		for(auto j:b[i]){
			if(col[i]!=col[j]){
				newb[col[i]].push_back(col[j]);
//				printf("草%d %d\n",col[i],col[j]); 
				en[col[j]]++;
			}  
		}
//		printf("傻逼%d %d\n",i,col[i]);
	}
	for(int i=1;i<=colnum;i++){
		if(!en[i]){
//			printf("dfs=%d\n",i);
			dfs2(i);
		}
	}
	printf("%d",ans);
}

ybtoj练习题

3.4.2

一个强连通分量里的牛都是互相喜欢的,所以我们先缩点,然后判断那个点出度为0,因为他不喜欢任何的牛,说明他被所有奶牛喜欢,但如果有两个出度为0的点,就说明这两头牛都不互相喜欢,就没有解

3.4.3

口胡了
缩完点后,跑一遍拓扑排序,然后考虑dp转移,若u的答案可以更新v,则可以将v的答案更新为 \(dp[u]+num[v]\),然后v的答案个数为 $ans[u]
若u的答案等于v,则可以将v答案的个数加上u答案个数,就做完了

3.4.4

首先考虑将所有限制转为同一限制,发现所有限制都可以表示成 \(a+k\leq b\),因为 \(a<b => a+1\leq b\),然后所以就将每一条限制转化为a向b连上一条边权为k的边,然后缩点,若一个强连通分量中有边权为1的边则一定不合法,否则从入度为0的点值为1开始递推即可

3.4.5

缩点后跑最短路即可,注意不用判重边

3.4.6

某个傻逼缩完点后跑了一遍最小生成树,然后发现样例过不了,手模了一下,开始怀疑最小生成树的正确性,忽然想到它是有向图。。。。。。

缩完点后,它是一张0号点可以到达任意点的图,因为它本身就满足1号点可以到任意点,考虑删去不优的边,最终成一个类似于树的图,且要求边权相加最短

考虑树(和树不同,是一张由父节点指向子节点的图)的性质,就是每个节点除了根节点,入度都为1,所以每个节点贪心的保留入度的边边权最小的就行

3.4.7

考虑缩点,然后找到一条最长链即可

3.4.8

我们把有约束关系的连一条有向边,如果出现环,就说明这环内的所有点必须选或不选,然后树上背包即可

这里留一个问题:为什么不能在缩点之前建虚拟根呢?

3.4.9

口胡了一半,但是没有发现只有n个点有宝藏这个题目的要求

首先我们只需要对有宝藏的点互相连边即可

其次我口胡了出来,对于横竖连边的点,我们只需要针对一行或一列建一个虚拟点,这个点和这一行/列所有有宝藏的点连边即可

然后因为我们只针对有宝藏的点连边,所以我们按一下方式重新建图,map存一下宫室位置即可

posted @ 2024-11-02 16:30  daydreamer_zcxnb  阅读(26)  评论(1编辑  收藏  举报