Tarjan求无向图割点,割边,双连通分量;求有向图强连通分量,缩点

为了与有向图尽可能保持一致,我们将无向图的一条无向边拆分成两条单向边。两条边互为反向边。

从图中一点作为起点,进行dfs搜索遍历图,这样会得到一棵树,我们称之为dfs搜索树,该树中的每一条边都来自原图,我们将这些边称为树边,其他图中的边称为非树边。

dfn数组:记录dfs序,也即时间戳、搜索顺序。
low数组:记录每个点经过一条非树边能回到的所有结点的最小dfn值。

割点和割边都是无向图才有的

tarjan求割点以及点双连通分量

x是割点

case1:x不是root&&x有儿子&&low[y]>=dfn[x] 

case2:x是root&&x有>=2个儿子(这里的儿子指的是深度搜索树里面的儿子,不是vec[u].size())

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5, M = 1e5+5;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];
bool cut[N];
int root;
void add(int a,int b)
{
	e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}
int num=0;
void tarjan(int u){
	low[u] = dfn[u] = ++timestamp;
	stk[++top] = u;
	int cnt = 0;
	for(int i = h[u];~i;i=ne[i])
	{
		int j = e[i];
		if(!dfn[j])
		{
			tarjan(j);
			low[u] = min(low[u],low[j]);
			// 看j是不是能连到比u还高的地方
			if(dfn[u]<=low[j])//j最高比u高度低 说明j是u一个新的分支(如果把u删掉 多一个j连通块)
			{
				cnt++;
				// 判断u是否割点 如果不是根节点-只要有一个分支他就是割点 || 如果是根节点 需要有两个分支才是割点
				//    root            /
				//    / \          非root(自带上面一个边,所以只要一个下分支)
				//                   /
				if(u==root&&cnt>=2||u!=root&&cnt>=1)cut[u] = true;
				++dcc_cnt;
				int y;
				do{
					y = stk[top--];
					dcc[dcc_cnt].push_back(y);
				}while(y!=j);//注意弹出栈不是弹到u为止 而是弹到j为止(u仍保留在stk中)
				// 🔺 开新分支 == u一定和新分支j组成一个dcc 也和旧连通块组成dcc
				// 那么当前最高点u还要被用在更高的包含u的旧连通块
				// 所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里
				dcc[dcc_cnt].push_back(u);
			}
		}
		else low[u] = min(low[u],dfn[j]);
	}
}
vector<int>ans;
int main(){
	memset(h,-1,sizeof h);
	memset(dfn,0,sizeof dfn);
	memset(cut,0,sizeof cut);
	cin>>n>>m;
	while(m--)
	{
		int a,b;
		cin >> a >> b;
		add(a,b),add(b,a);
	}
	for(root=1;root<=n;root++){
		if(!dfn[root]){
			tarjan(root);
		}
	}
	for(int i=1;i<=n;i++){
		if(cut[i]){
			num++;
			ans.push_back(i);
		}
	}
	cout<<"总共有"<<num<<"个割点"<<"\n";
	for(int i:ans){
		cout<<i<<"\n";
	}
	cout<<"总共有"<<dcc_cnt<<"个点双连通块"<<"\n";
	for(int i=1;i<=dcc_cnt;i++){
		for(int j:dcc[i]){
			cout<<j<<" ";
		}
		cout<<"\n";
	}
}

tarjan求割边以及边双连通分量

x->y是割边

case:low[y]>dfn[x] 

无向图里面儿子到父亲的边不处理,即不用儿子到父亲的边来更新儿子的low(不然的话无向图是不可能有割边的因为用儿子到父亲的这条边边更新就可以让low[y]==dfn[x]了)

求割边时需要判断是不是用了父亲到儿子的反边更新,所以需要用到前式链向星

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=2e5+5;
const int inf=1e9+7;
const int mod=1e9+7;
int h[maxn],e[maxn],ne[maxn];
int from[maxn];
int idx;
void add(int a,int b){
	from[idx]=a;
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int dfn[maxn],low[maxn];
int cnt=0;
int is_bridge[maxn];
stack<int>stk;
int dcc_cnt;
int color[maxn];
int k=0;
void tarjan(int u,int from){
	dfn[u]=low[u]=++cnt;
	stk.push(u);
	for(int i=h[u];i!=-1;i=ne[i]){
		int v=e[i];
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
			if(dfn[u]<low[v]){
				k+=1;
				is_bridge[i]=is_bridge[i^1]=true;
			}
		}
		else{
			if(i!=(from^1)){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	//双连通分量
	if(dfn[u]==low[u]){
		color[u]=++dcc_cnt;
		while(stk.top()!=u){
			color[stk.top()]=dcc_cnt;
			stk.pop();
		}
		stk.pop();
	}
}
void solve(){
	memset(h,-1,sizeof(h));
	int n,m;
	cin>>n>>m;
	while(m--){
		int u,v;
		cin>>u>>v;
		add(u,v);
		add(v,u);
	}
	tarjan(1,-1);
	cout<<"有"<<k<<"条割边"<<"\n";
	for(int i=0;i<idx;i+=2){  //相差为1的就是反向边,所以i+=2
		if(is_bridge[i])cout<<from[i]<<" "<<e[i]<<'\n';
	}
	cout<<"有"<<dcc_cnt<<"个双连通分量"<<"\n";
	for(int i=1;i<=n;i++){
		cout<<"点"<<i<<"属于"<<"联通分量";
		cout<<color[i]<<"\n";
	}
}
signed main(){
	int t=1;
	//cin>>t;
	while(t--){
		solve();
	}
}
/*
5 5
1 2
2 3
3 4
2 4
4 5
 */

tarjan求解无向图双连通分量

无向图双连通分量分为两类:

(1)边双联通分量(e-DCC​);

(2)点双联通分量(v-DCC);

  • 对于(1)边双联通分量的定义,我们需要引入桥的概念,桥是指连通图中的一条边,这条边满足:如果删除这条边,整个图会变的不连通,则这条边被称为桥。极大的不含有桥的连通区域称为边连通分量。根据定义可知e-DCC有以下性质:

① 在e-DCC中,无论删除哪条边,该e-DCC仍是连通的;

在e-DCC中,任意两点之间至少存在两条不相交(边是严格不相交的,点可以相交)的路径

  • 对于(2)点双联通分量的定义,我们需要引入割点的概念,割点是指连通图中的一个点,这个点满足:如果删除这个点以及该点相关联的所有边,整个图会变的不连通,则这个点被称为割点。极大的不含有割点的连通区域称为点连通分量。根据定义可知v-DCC有以下性质:

① 每个割点至少属于两个v-DCC

在v-DCC中,任意两点之间任意两点都同时包含在至少一个简单环中,并且至少存在两条不相交(点不相交)的路径

边双连通分量

与前面tarjan求无向图割边类似,需要注意的是这里不能用父亲到儿子,儿子又回到父亲的边,即反边更新儿子的low,但是可以用儿子回到父亲是可以的,如图,fa通过edge1到达son,son不能用edge1更新自己的low,但是son可以用edge2更新自己的low,所以得用链式前向星,from记录来的边,当前边为i, i!=(from^1)即i不是from的反边时就可以更新

 这里要求两条路径没有一条重合的道路就可以知道路径必须是双连通的,那么先缩点,求出各个双连通分量,要让这些双联通分量之间添加最少边使得他们联通,那么各个双连通分量之间组成的就是一棵树,要让树成为双连通分量要添加的边就是(叶子节点的数量-1)/2+1,要求叶子节点的数量可以对于各条割边,让他们指向的各个双连通分量的值+1,最后叶子节点的值为1,其他都大于1

如图对双连通分量做完缩点后 只剩桥和点
          o                  
         /  \
       o    o
      /  \   /  \
     o  o o  o
    /  \  
  o    o            

after.....                                          

          o
         /  \
        o   o
      /  \   /   \
     o  o-o  o
    / \       |  |
   o   o__|  |
   |_____ _|               
   可以发现对左右两个叶子节点连通后,根节点连向左右叶子节点的边就可以删去了
   同理 再把第2个和第4个叶子节点连通后,根节点连向第2个和第4个叶子节点的边也可以删去
   第3个叶子节点随便连

#include<iostream>
#include<cstring>
#include<algorithm>
#include<stack>
using namespace std;
const int N = 5010,M = 20010;
int h[N],ne[M],e[M],idx;
int dnt[N]; //dnt[u]表示当前到达u
int low[N]; //low[u]表示从u节点出发能遍历到的最小时间戳
int id[N];  //记录节点i所在的双连通分量的编号
int d[N]; //记录节点i的度
bool is_bridge[N]; //记录边是否是桥边
stack<int> sta;
int dcc_cnt; //为双连通分量的编号
int timestamp; //时间戳
int n,m;

void add(int a,int b){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

void tarjan(int u,int from){   //from记录的是当前节点由哪条边过来的(防止反向遍历)

    low[u] = dnt[u] = ++timestamp;

    sta.push(u);

    //先将图的所有节点的low和dnt都预处理出来
    for(int i = h[u];i != -1;i = ne[i]){
        int j = e[i];

        if(!dnt[j]){

            tarjan(j,i); 

            low[u] = min(low[u],low[j]);

            //表示j节点永远都走不到u节点(u,j这两个点只能从u走到j),所以边u-j(w[i])是一条桥边
            if(dnt[u] < low[j]){  
                is_bridge[i] = true;
                is_bridge[i ^ 1] = true; //反向边同样是桥边
                //因为一对正向边和反向边的编号都是成对出现,所以直接^1即可
                // 这里i==idx 如果idx==奇数 则反向边=idx+1 = idx^1
                //            如果idx==偶数 则反向边=idx-1 = idx^1
            } 

        //from ^ 1是边from的反向边,防止再遍历回去
        }else if(i != (from ^ 1)){

            //j节点有可能是之前遍历过的点
            low[u] = min(low[u],dnt[j]);
        }
    }


    //预处理完之后判断哪个节点的low[u] = dnt[u],说明是双联通分量的最高点
    if(low[u] == dnt[u]){

        int y;
        ++dcc_cnt;
        do{
            y = sta.top();
            sta.pop();
            id[y] = dcc_cnt;
        }while(y != u);
    }

}

int main(){
    memset(h,-1,sizeof h);
    cin >> n >> m;
    for(int i = 0;i < m;++i){
        int a,b;
        cin >> a >> b;
        add(a,b); add(b,a);
    }

    for(int i = 1;i <= n;++i){
        if(!dnt[i]) tarjan(i,-1);  //引入from防止反向遍历回遍历过的边
    }

    //遍历所有边,如果边i是桥边,在其所连的出边的点j所在双连通分量的度+1
    for(int i = 0;i < idx;++i){   //包含正向边和反向边,所以度为1的节点一定是叶子节点
        if(is_bridge[i]){
            d[id[e[i]]]++;
        }
    }

    int cnt;
    //枚举所有双连通分量,需要加的边数就是:(度为1的节点个数 + 1 ) / 2
    for(int i = 1;i <= dcc_cnt;++i){
        if(d[i] == 1) cnt++;
    }
    cout << (cnt + 1) / 2 << endl;
    return 0;
}

点双连通分量

求解点双连通分量时,当dfn[u]<=low[j],不能把u点出队,因为u是割点,可能会包含在好多个点双连通分量里面,所以要从top出栈到j为止,然后再把当前u加入这个联通分量里面,并且加入u后不把u出栈

思路:
1 无割点                   放2个出口 方案数 *= C[cnt][2] = cnt*(cnt-1)/2
2 有割点 V-DCC==1 放1个出口 方案数 *= C[cnt-1][1] = cnt-1 (不包含割点)
3 有割点 V-DCC>=2 放0个出口 方案数 *= 1

#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

typedef unsigned long long ULL;

const int N = 1010, M = 1010;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];
bool cut[N];
int root;

void add(int a,int b)
{
    e[idx] = b,ne[idx] = h[a],h[a] = idx++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++timestamp;
    stk[++top] = u;
    // 1 u是孤立点-自称一个dcc
    if(u==root && h[u]==-1)//u是根节点且没有邻边
    {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
    // 2 u不孤立
    int cnt = 0;
    for(int i = h[u];~i;i=ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u],low[j]);
            // 看j是不是能连到比u还高的地方
            if(dfn[u]<=low[j])//j最高比u高度低 说明j是u一个新的分支(如果把u删掉 多一个j连通块)
            {
                cnt++;
                // 判断u是否割点 如果不是根节点-只要有一个分支他就是割点 || 如果是根节点 需要有两个分支才是割点
                //    root            /
                //    / \          非root(自带上面一个边,所以只要一个下分支)
                //                   /
                if(u!=root||cnt>1)cut[u] = true;
                ++dcc_cnt;
                int y;
                do{
                    y = stk[top--];
                    dcc[dcc_cnt].push_back(y);
                }while(y!=j);//注意弹出栈不是弹到u为止 而是弹到j为止(u仍保留在stk中)
                // 🔺 开新分支 == u一定和新分支j组成一个dcc 也和旧连通块组成dcc
                // 那么当前最高点u还要被用在更高的包含u的旧连通块
                // 所以如果这个时候出栈了 回溯到比u高的点的时候 u就加不进旧连通块里
                dcc[dcc_cnt].push_back(u);
            }
        }
        else low[u] = min(low[u],dfn[j]);
    }
}

int main()
{
    int T = 1;
    while(cin >> m,m)
    {
        for(int i=1;i<=dcc_cnt;i++)dcc[i].clear();
        idx = n = timestamp = top = dcc_cnt = 0;
        memset(h,-1,sizeof h);
        memset(dfn,0,sizeof dfn);
        memset(cut,0,sizeof cut);
        while(m--)
        {
            int a,b;
            cin >> a >> b;
            n = max(n,b),n = max(n,a);//第二个n=漏了
            add(a,b),add(b,a);
        }

        for (root = 1; root <= n; root ++ )
            if (!dfn[root])
                tarjan(root);

        int res = 0;
        ULL num = 1;
        for(int i = 1;i<=dcc_cnt;i++)
        {
            int cnt = 0;
            for(int j= 0;j<dcc[i].size();j++)//j< 写成了i<
            {
                if(cut[dcc[i][j]])
                    cnt++;
            }
            // 无割点
            if(cnt == 0)//cnt写成了cut
            {
                if(dcc[i].size()>1)res+=2,num*=dcc[i].size()*(dcc[i].size()-1)/2;
                else res++;
            }
            else if(cnt==1)res++,num*=dcc[i].size()-1;
        }
        printf("Case %d: %d %llu\n", T ++, res, num);
    }
    return 0;
}

 tarjan求有向图强连通分量

强连通分量就是每个分量中的顶点都是两两都是含有路径可以互相达到的,强连通分量的用处不多,而且它也是相对于有向图来说的,无向图没有这一概念,它的作用是缩小图的规模,从而减小图的复杂度。

按照dfs序把各个节点入栈,vis标记为1,dfn记录dfs序,low记录自己或子节点能够回溯到的最小的dfs序是多少(这里的low与前文的low不同,前文的low是经过一条非树边能更新的最小dfs序这里的low不一定是一条,只要求是最小,所以前文用low[u]=min(low[u],dfn[v]),这里用low[u]=min(low[u],low[v])),当dfn[u]==low[u]时代表现在栈顶到u为止的各个元素都是一个强连通分量里面的,把他们vis标记为0代表出栈

https://www.luogu.com.cn/record/73976293

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
vector<int>vec[maxn];
int color[maxn],dfn[maxn],low[maxn],vis[maxn];
int cnt;
stack<int>stk;
vector<int>ans[maxn];
int sum;
void tarjan(int u){
	dfn[u]=++cnt;
	low[u]=cnt;
	vis[u]=1;
	stk.push(u);
	for(int v:vec[u]){
		if(!dfn[v]){   //代表还没有没访问过
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{   //被访问过
			if(vis[v]){   //表示这个点是不是在stk栈里
				low[u]=min(low[u],low[v]);
			}
		}
	}
	if(dfn[u]==low[u]){  //代表已经确定这一段强连通分量了
		color[u]=++sum;
		vis[u]=0;  //代表u不再在栈里面了
		while(stk.top()!=u){
			color[stk.top()]=sum;			
			vis[stk.top()]=0;
			stk.pop();
		}
		stk.pop();
	}
}
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int from,to;
		cin>>from>>to;
		vec[from].push_back(to);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		ans[color[i]].push_back(i);
	}
	int p=0;
	for(int i=1;i<=sum;i++){
		if(ans[i].size()>=2){
			p++;
		}
	}
	cout<<p<<"\n";
}
/*
5 4
2 4
3 5
1 2
4 1
*/

缩点

对于同一个强连通分量里面的点来说所有的点对于其他外面的点都是等价的,所以可以把一个强连通分量里面的点缩为一个点,即用tarjan求出来强连通分量然后用sd来代表缩点之后原来的i现在相当于sd[i]这个点,重现建一遍图即可

https://www.luogu.com.cn/problem/P3387
 

#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m;
vector<int>vec[maxn];
int dfn[maxn],low[maxn],vis[maxn];
int cnt;
stack<int>stk;
int w[maxn];   //1权值
int sd[maxn];  //代表x现在相当于什么点
vector<pair<int,int>>edge;
int dist[maxn];
int in[maxn];
void tarjan(int u){
	dfn[u]=++cnt;
	low[u]=cnt;
	vis[u]=1;
	stk.push(u);
	for(int v:vec[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],low[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		while(1){
			int now=stk.top();
			stk.pop();
			sd[now]=u;
			vis[now]=0;
			if(now==u){
				break;
			}
			w[u]+=w[now];
		}
	}
}
int topo(){
	queue<int>q;
	int tot=0;
	for(int i=1;i<=n;i++){
		if(sd[i]==i&&!in[i]){
			q.push(i);
			dist[i]=w[i]; //dist代表以i为起点的最大权值路径值
		}
	}
	while(!q.empty()){
		int now=q.front();
		q.pop();
		for(int v:vec[now]){
			dist[v]=max(dist[v],dist[now]+w[v]);
			in[v]--;
			if(in[v]==0){
				q.push(v);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		ans=max(ans,dist[i]);
	}
	return ans;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>w[i];
	}
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		edge.push_back({u,v});
		vec[u].push_back(v);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		vec[i].clear();
	}
	for(auto[u,v]:edge){
		u=sd[u],v=sd[v];
		if(u!=v){
			vec[u].push_back(v);
			in[v]++;
		}
	}
	cout<<topo()<<"\n";
}

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值