tarjan好神奇啊。
一、最大强连通分量问题
1、一些定义:
<1>强连通:在有向图G中,设有两个点u、v,发现由u有一条路可以到v,由v也有一条路可以到u,则u、v强连通。
<2>强连通图:每两个点都强连通的有向图叫强连通图。
<3>强连通分量:有向图的极大强连通子图。
2、思路:
当在一个有向图中找到了一条从u到u的回路,则在该路径上的所有点和u可以构成一个强连通子图,因为环上任意两点都可以互相到达,那么所有从u到u的回路上的所有点和u构成一个强连通分量。 (personal view)
3、算法:最简单的想法是dfs找回路,记录路径上的点,但图太大时时间空间上不够,需要加一些优化。tarjan算法就是基于dfs,使用栈,标记数组等来进行优化,使得时间复杂度在O(n)。
用到的两个辅助数组:
dfn[u]:记录点u是第几个遍历到的(只是标记遍历先后顺序)【时间戳】因为回路上的点本来是没有顺序可言的,dfn人为加了一个顺序。
low[u]:记录点u所在回路中可以到达的点中最小的时间戳
用栈储存强联通分量,时间戳越小越早入栈。
用vis[u]数组来记录u是否在栈中。
算法的具体步骤:
tarjan(u){//当遍历到u点时
dfn[u]=low[u]=++ind;//先初始化u点的dfn和low都等于当前的时间戳
s.push(u);//将u压进栈
vis[u]=1;//标记u在栈中
for(v:Edge(u)){//搜索u可以到达的所有点v
if(!dfn[v]) tarjan(v);//如果v点还没有被遍历过,遍历v点
if(vis[v]) low[u]=min(low[u],low[v]);//如果v点还在栈中,更新low[u]的值
//因为如果v还在栈中则代表v在u的一条回路上,那么v可以到达的最早遍历的点(low[v])u点也可以到达
//所以low[u]在low[u]、low[v]中取一个最小的
//而如果v已经不在栈中了,说明v点到不了v点之前的任何点了,已经找完了v点的所以回路并且都已出栈
}
if(low[u]==dfn[u])//u所能到达的最早遍历的点就是u本身,而且u的回路全部都搜索完了
//那么u点及栈中在u点上(比u后入栈)的点构成一个极大强连通分量
do{
vis[s.top()]=0;//标记不在栈中了
s.pop();//出栈
}while(vis[u]);//直到u点出栈为止
//如果low[u]!=dfn[u],代表u是dfn[]==low[u]的点的回路上的一点,而这个点还没有拓展完所有回路
}
T1:洛谷P1726 上白泽慧音
题目链接
题目描述
在幻想乡,上白泽慧音是以知识渊博闻名的老师。春雪异变导致人间之里的很多道路都被大雪堵塞,使有的学生不能顺利地到达慧音所在的村庄。因此慧音决定换一个能够聚集最多人数的村庄作为新的教学地点。人间之里由N个村庄(编号为1…N)和M条道路组成,道路分为两种一种为单向通行的,一种为双向通行的,分别用1和2来标记。如果存在由村庄A到达村庄B的通路,那么我们认为可以从村庄A到达村庄B,记为(A,B)。当(A,B)和(B,A)同时满足时,我们认为A,B是绝对连通的,记为<A,B>。绝对连通区域是指一个村庄的集合,在这个集合中任意两个村庄X,Y都满足<X,Y>。现在你的任务是,找出最大的绝对连通区域,并将这个绝对连通区域的村庄按编号依次输出。若存在两个最大的,输出字典序最小的,比如当存在1,3,4和2,5,6这两个最大连通区域时,输出的是1,3,4。
输入格式:
第1行:两个正整数N,M
第2…M+1行:每行三个正整数a,b,t, t = 1表示存在从村庄a到b的单向道路,t = 2表示村庄a,b之间存在双向通行的道路。保证每条道路只出现一次。(N <= 5000且M <= 50000)
输出格式:
第1行: 1个整数,表示最大的绝对连通区域包含的村庄个数。
第2行:若干个整数,依次输出最大的绝对连通区域所包含的村庄编号。
裸的求最大强连通分量的题(最适合我这种菜鸡练手了)
代码:
#include <iostream>
#include <cmath>
#include <algorithm>
#include <stack>
#define LL long long
#define _for(i,j,k) for(int i=j;i<=k;i++)
#define for_(i,j,k) for(int i=j;i>=k;i--)
using namespace std;
const int maxn = 5e3+5;
const int maxm = 5e4+5;
int n,m,cnt,head[maxn],net[maxm],e[maxm];
int vis[maxn],dfn[maxn],low[maxn],tar[maxn],ma,mark,ind,mi,mar;
stack<int> s;
void add_edge(int u,int v){//邻接表存图
e[++cnt]=v;
net[cnt]=head[u];
head[u]=cnt;
}
void tarjan(int u){
dfn[u]=low[u]=++ind;
s.push(u);
vis[u]=1;
for(int i=head[u];i;i=net[i]){
if(!dfn[e[i]]) tarjan(e[i]);
if(vis[e[i]]) low[u]=min(low[u],low[e[i]]);
}
if(low[u]==dfn[u]){
mark++;//mark强连通分量编号
int tcnt=0;//当前强连通分量包含的点数
do{
tar[s.top()]=mark;//tar[i]代表i是编号为tar[i]的强连通分量的点
vis[s.top()]=0;
s.pop();
tcnt++;
}while(vis[u]);
if(tcnt>ma||(tcnt==ma&&u<mi)){
ma=tcnt;//最大强连通分量包含的点数
mi=u;//最大强连通分量包含的点中最小的点
mar=mark;//最大强连通分量的编号
}
}
}
int main(){
cin>>n>>m;
int u,v,t;
_for(i,1,m){
cin>>u>>v>>t;
add_edge(u,v);
if(t==2) add_edge(v,u);
}
mi=n;
_for(i,1,n){
if(!dfn[i]) tarjan(i);
}
int kase=0;
cout<<ma<<"\n";
_for(i,1,n){
if(tar[i]==mar){
if(kase) cout<<" ";else kase++;//控制输出格式
cout<<i;
}
}
return 0;
}
二、割点、割边(桥)
割点: 对于一个连通的无向图,删去某一个点(连同与改点相连的边)后,图变得不连通了,该点就为一个割点。
方法是使用tarjan算法:
dfn[u]记录点u是第几个遍历到的,low[u]记录点u所在回路中可以到达的点中最小的时间戳。
假设dfs到u时,u存在一条边可以到达v,而v能到达的最早的点的时间戳比u的时间戳要大或等于(即low[v]>=dfn[u]),那么删去后,就存在包括v在内的一个联通块与u点前面一部分不联通,整个图就不联通了。
但是存在一个特殊情况:u的前面没有点,也就是说u是第一个遍历到的。这种情况下,如果存在两个以上的点满足low[v]>=dfn[u](v是与u相连的点),那么删去u以后图是不联通的。
具体步骤:
isc[maxn];//标记点是否为割点
cnt=0;//记录根结点分支数
tarjan(u,rt){//当遍历到u点时,rt点前联通块的根(最新遍历的点)
dfn[u]=low[u]=++ind;//先初始化u点的dfn和low都等于当前的时间戳
for(v:Edge(u)){//搜索u可以到达的所有点v
if(!dfn[v]){
tarjan(v,rt);//如果v点还没有被遍历过,遍历v点
low[u]=min(low[u],low[v]);//更新low[u]的值
if(u==rt) cnt++;
if(low[v]>=dfn[u]&&u!=rt) isc[u]=1;//u是割点
}
low[u]=min(low[u],dfn[v]);//更新low[u]
}
if(u==rt&&cnt>=2) isc[u]=1;//根结点的特判
}
割边(桥): 对于一个连通的无向图,删去某一条边后,图变得不连通了,该点就为一个割点。
同样用的tarjan算法,套用上面割点的模板,把 low[v]>=dfn[u] 条件判断的等号去掉(low[v]>dfn[u]),记录边的编号就可以了。(如果用的链表形式的邻接表那么记录边的编号就灰常方便了)
三、双连通分量
点双连通: 若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。
边双连通: 若一个无向图中的去掉任意一条边都不会改变此图的连通性,即不存在割边(桥),则称作边双连通图。
点(边)连通图、点(边)连通分量和上面的强连通图、强连通分量类似。
TBC…