图的连通性问题

ps:本人太菜了,强连通分量是最近才开始上手写题的,勿喷。

Part1:判断图是否联通

相信不用多说:dfs一遍或者并查集

如果你是普及选手:

dfs做法:

从任意一个点开始遍历整个图,如果所有点都被遍历一遍,就是联通的。

并查集:

把每个边的两个点并在一起,看看是否所有点被并到一个集合里了。


计算连通块也可以用以上方法解决。


Part2:强连通分量

①:

什么是强连通分量?

“有向图强连通分量:在有向图G中,如果两个顶点 \(v_i\),\(v_j\) 间(\(v_i\) \(\neq\) \(v_j\))有一条从 \(v_i\)\(v_j\) 的有向路径,同时还有一条从 \(v_j\)\(v_i\) 的有向路径,则称两个顶点强连通 \((strongly\) \(connected)\)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量 \((strongly\) \(connected\) \(components)\)。”

说的通俗易懂一些:

一张有向图,其中有一个子图,这个子图需要满足:图中任意两点 \(v_i\), \(v_j\) (\(v_i\neq v_j\)),有一条路径可以从 \(v_i→v_j\),还有一条路径可以从 \(v_i←v_j\)

强连通分量就是这张图中满足需求且最大的子图。


②:

如何求取强连通分量?

DFS

可能会出现以下四种边:

1.树边:DFS森林中某棵树上的边。

2.后向边:由森林中某棵树上的一个结点指向其祖先结点的点(又被称为返祖边)。

3.前向边:由森林中某棵树上的一个结点指向其后代结点的边(一般来讲删除这种边以后并不会影响结果)。

4.交叉边:是在两个不构成“祖先—后代”关系的结点之间的边(又称横插边)。

接下来引入两个数组:

\(dfn\) 数组以及 \(low\) 数组

\(dfn\) 就是 \(dfs\) 序,\(dfn[i]\) 记录的是顶点 \(i\) 第几个被遍历。

\(low\) 数组表示的是每个点能回到的 \(dfn\) 序最小的结点是哪个。

\(dfn\)\(low\) 数组需要用 \(dfs\) 来求取.

在计算 \(dfn\)\(low\) 的同时,我们也要用栈来存储遍历的点,当某个点 \(dfn[x]==low[x]\) 时,把从这个点到栈顶的点全部取出,这就是一个强连通分量

这就是tarjan算法

代码:

const int maxn=1e4+10;
vector<int> edge[maxn];
int dfn[maxn],low[maxn];
int cnt;
int s[maxn],top;
bool instack[maxn];
int ans,scc[maxn];
void tarjan(int u)
{
    s[++top]=u;//把这个点扔进栈 
    instack[u]=1;	
    dfn[u] =low[u] =++cnt;
    for(int i=0;i<edge[u].size();i++)
	{
        int v=edge[u][i];
        if(!dfn[v])
		{	//如果下一个点没有被搜过,
            tarjan(v);//继续搜
            low[u]=min(low[u],low[v]);//更新low
        }
        else if(instack[v])
		{ //如果下一个点已经被搜过,看他在不在栈里
            low[u]=min(low[u],dfn[v]);//更新low
        }
    }
    if(dfn[u]==low[u])
	{
        ans++;//答案++,第几个强连通分量
        int v;
        while(1)
		{
			v=s[top--];
            scc[v]=ans;	//打标记
            instack[v]=0;//出栈
			if(v==u)
				break;
		}
    }
}

Part3:tarjan的其他应用

1.求割边割点

割边定义:在无向连通图 ,若删除边 \(u->v\) 后,原图变成了两个联通分量,则称这条边是⼀条割边。

割点定义:在无向连通图 上,若删除节点 \(u\) 和所有与其相连的边的集合 之后,原图变成了两个以上的连通分量,则称节点 \(u\) 是⼀个割点。

割点的求法:当我们遍历到一个点时,如果他不经过他的父亲边时,他的 \(low\) 的值要大于等于他父亲节点的 \(dfn\) ,这个点就是割点。

这里只放上割点的代码:

#include <bits/stdc++.h>

using namespace std;
vector<int> edge[20010];
int n,m;
int low[20010];
int dfn[20010];
bool vis[20010];
int cnt;
int ans[20010];
int c;
void dfs(int u,int root)
{
	dfn[u]=++cnt;
	low[u]=cnt;
	int sz=0;
	for(int i=0;i<edge[u].size();i++)
	{
		int v=edge[u][i];
		if(!dfn[v])
		{
			dfs(v,root);
            low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]&&u!=root)
				ans[++c]=u;
			if(u==root)
				sz++;			
		}
		low[u]=min(low[u],dfn[v]);
	}
	if(sz>1&&u==root)
		ans[++c]=u;
}
int main()
{
	cin>>n>>m;
	int u,v;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&u,&v);
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	for(int i=1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,i);
		}
	}
	sort(ans+1,ans+1+c);
	int c1=0;
	for(int i=1;i<=c;i++)
	{
		if(ans[i]!=ans[i-1])
			c1++;
	}
	cout<<c1<<endl;
	for(int i=1;i<=c;i++)
	{
		if(ans[i]!=ans[i-1])
			printf("%d ",ans[i]);		
	}
	printf("\n");
	return 0;
}

求割边与求割点差不多,可以自己想一想。


2.缩点

将一整个联通分量缩成一个点之后跑拓扑排序

const int maxn=10000+10;
int n,m;
int a[maxn];
int bl[maxn],dfn[maxn],low[maxn];
bool instack[maxn];
int s[maxn],top; 
int c;
vector<int> edge[maxn],tp[maxn];
void tarjan(int u)
{
	low[u]=dfn[u]=++c;
	s[++top]=u;instack[u]=1;
	for(int i=0;i<edge[u].size();i++)
	{
		int v=edge[u][i];
		if(!dfn[v])
		{
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
	    else if(instack[v])
	    	low[u]=min(low[u],low[v]);
	}
	if(dfn[u]==low[u])
	{
		int v;
		while(v=s[top--])
		{
			bl[v]=u;
			instack[v]=0;
			if(u==v)
				break;
			a[u]+=a[v];//缩起来
		}
	}
}
int cnt[maxn],dis[maxn];
int topo()
{
	queue<int> q;
	int tot=0;
	for(int i=1;i<=n;i++)
		if(bl[i]==i&&!cnt[i])
		{
			q.push(i);
	        dis[i]=a[i];
		}
	while(!q.empty())
	{
		int u=q.front();
		q.pop();
		for(int i=0;i<tp[u].size();i++)//tp数组记录的是强联通分量之间的连边情况
		{
			int v=tp[u][i];
			dis[v]=max(dis[v],dis[u]+a[v]);
			cnt[v]--;
			if(cnt[v]==0)
				q.push(v);
		}
	}
    int ans=0;
    for(int i=1;i<=n;i++)
    	ans=max(ans,dis[i]);
    return ans;
}

下面是强联通分量之间连边的代码:

	for(int i=1;i<=m;i++)
	{
		int u=bl[tmp[i].u],v=bl[tmp[i].v];
		if(u!=v)
		{
			tp[u].push_back(v);
			cnt[v]++;
		}
	}

3.点双/边双连通分量

点双连通:若对于一个无向图,其任意一个节点对于这个图本身而言都不是割点,则称其点双连通。也就是说,删除任意点及其相关边后,整个图仍然属于一个连通分量。

边双连通分量:边双连通分量没有割边。

边双联通分量:

void tarjan(int u,int fa)
{
	dfn[u]=low[u]=++cnt;
	for(int i=head[u];i;i=nxt[i])
	{
		int v=ver[i];
		if(!dfn[v])
		{
			tarjan(v,i);
			if(low[v]>dfn[u])//如果不经过他与他父亲的这条边,就无法回到dfn更小的点,那这条边就是割边
			{
				int tmp=1ll*u*100000000+1ll*v;//我这里用的是暴力map存储
				cut[tmp]=1;
				tmp=1ll*v*100000000+1ll*u;
				cut[tmp]=1;
			}
			low[u]=min(low[u],low[v]);
		}
		else if(i!=(fa^1))
			low[u]=min(low[u],dfn[v]);
	}
}
int bl[maxn];
void dfs(int u)//dfs简单计算一下边双联通分量
{
	bl[u]=c;
	dcc[c].push_back(u);
	for(int i=head[u];i;i=nxt[i])
	{
		int v=ver[i];
		int tmp=1ll*u*100000000+1ll*v;
		
		if(bl[v]||cut[tmp])
			continue;
		dfs(v);
	}
}

点双:

void tarjan(int u)
{
	dfn[u]=low[u]=++cnt;
	s[++top]=u;
	instack[u]=1;
	int tmp=0;
	for(int i=0;i<edge[u].size();i++)
	{
		int v=edge[u][i];
		if(!dfn[v])
		{
			tmp++;
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				c++;
				while(s[top+1]!=v) dcc[c].push_back(s[top--]);
				dcc[c].push_back(u);
			}
		} 
		else if(instack[u])
			low[u]=min(low[u],dfn[v]);
	}
	if(root==u&&tmp==0)
		dcc[++c].push_back(u);
}

Part4:好题

P1262 间谍网络

题目描述

由于外国间谍的大量渗入,国家安全正处于高度的危机之中。如果 A 间谍手中掌握着关于 B 间谍的犯罪证据,则称 A 可以揭发 B。有些间谍收受贿赂,只要给他们一定数量的美元,他们就愿意交出手中掌握的全部情报。所以,如果我们能够收买一些间谍的话,我们就可能控制间谍网中的每一分子。因为一旦我们逮捕了一个间谍,他手中掌握的情报都将归我们所有,这样就有可能逮捕新的间谍,掌握新的情报。

我们的反间谍机关提供了一份资料,包括所有已知的受贿的间谍,以及他们愿意收受的具体数额。同时我们还知道哪些间谍手中具体掌握了哪些间谍的资料。假设总共有 \(n\) 个间谍(\(n\) 不超过 \(3000\)),每个间谍分别用 \(1\)\(3000\) 的整数来标识。

请根据这份资料,判断我们是否有可能控制全部的间谍,如果可以,求出我们所需要支付的最少资金。否则,输出不能被控制的一个间谍(要求编号最小)。

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

这就是一张有向图,我们把每一个强连通分量缩成一个点,然后看看这些强连通分量之间的互相连边的情况,记录每个入度为 \(0\) 的强连通分量,这些强连通分量必须要买下。

无解就是有至少一个间谍不能被贿赂也不能被揭发。

弱化版:P2002 消息扩散

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

[SDOI2010]所驼门王的宝藏

题目描述

在宽广的非洲荒漠中,生活着一群勤劳勇敢的羊驼家族。被族人恭称为“先知”的Alpaca L. Sotomon是这个家族的领袖,外人也称其为“所驼门王”。所驼门王毕生致力于维护家族的安定与和谐,他曾亲自率军粉碎河蟹帝国主义的野蛮侵略,为族人立下赫赫战功。所驼门王一生财宝无数,但因其生性节俭低调,他将财宝埋藏在自己设计的地下宫殿里,这也是今天Henry Curtis故事的起点。Henry是一个爱财如命的贪婪家伙,而又非常聪明,他费尽心机谋划了这次盗窃行动,破解重重机关后来到这座地下宫殿前。

整座宫殿呈矩阵状,由R×C间矩形宫室组成,其中有N间宫室里埋藏着宝藏,称作藏宝宫室。宫殿里外、相邻宫室间都由坚硬的实体墙阻隔,由一间宫室到达另一间只能通过所驼门王独创的移动方式——传送门。所驼门王为这N间藏宝宫室每间都架设了一扇传送门,没有宝藏的宫室不设传送门,所有的宫室传送门分为三种:

  1. “横天门”:由该门可以传送到同行的任一宫室;

  2. “纵寰门”:由该门可以传送到同列的任一宫室;

  3. “任意门”:由该门可以传送到以该门所在宫室为中心周围8格中任一宫室(如果目标宫室存在的话)。

深谋远虑的Henry当然事先就搞到了所驼门王当年的宫殿招标册,书册上详细记录了每扇传送门所属宫室及类型。而且,虽然宫殿内外相隔,但他自行准备了一种便携式传送门,可将自己传送到殿内任意一间宫室开始寻宝,并在任意一间宫室结束后传送出宫。整座宫殿只许进出一次,且便携门无法进行宫室之间的传送。不过好在宫室内传送门的使用没有次数限制,每间宫室也可以多次出入。

现在Henry已经打开了便携门,即将选择一间宫室进入。为得到尽多宝藏,他希望安排一条路线,使走过的不同藏宝宫室尽可能多。请你告诉Henry这条路线最多行经不同藏宝宫室的数目。

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

\(\text{ }\)

我们在每一行上建一个虚点,把横天边都连在这个点上;每列上建一个虚点,然后把每个纵寰边连在这个点上;任意门可以暴力连。

然后缩点跑拓扑即可

posted @ 2022-11-09 19:47  Y2y7m  阅读(136)  评论(0)    收藏  举报