求强连通分量(拓扑排序)的两种算法

强连通分量分解,拓扑排序

两遍dfs

dfs后序遍历,此时标号最大的是头部(也就是开始搜索的根部)
将边反向后,根据强连通分量的性质,边反向后对强连通分量无影响,分量内各个点之间仍然相互可达,而对于不同的强连通分量之间的边反向后再搜索就不可达了,至此,再进行第二次dfs(反向),可以遍历一个强连通分量中的所有顶点,能依次将强连通分量划分出来。
具体参考白书p320—p322
下面是模板

int V;//图的顶点数
vector<int> G[maxn];//邻接表建图
vector<int> rG[maxn];//反向后的图
vector<int> vs;//后续遍历后点的顺序
bool used[maxn];//标记是否用过
int cmp[maxn]; //顶点的拓扑序
void init() {
 for (int i = 0; i < V; i++) {
     G[i].clear();
     rG[i].clear();
 }
//    vs.clear();
}
void add_edge(int from, int to) {//建边
 G[from].push_back(to);
 rG[to].push_back(from); //反边
}

void dfs(int v) { //后续遍历
 used[v] = true;
 for (int i = 0; i < (int)G[v].size(); i++) {
     if (!used[G[v][i]]) dfs(G[v][i]);
 }
 vs.push_back(v);
}
void rdfs(int v, int k) { 
 used[v] = true;
 cmp[v] = k;
 for (int i = 0; i < (int)rG[v].size(); i++) {
     if (!used[rG[v][i]]) rdfs(rG[v][i], k);
 }
}
int scc() {   //返回强连通分量的数量
 memset(used, 0, sizeof(used));
 vs.clear();
 for (int v = 0; v < V; v++) { //第一次dfs,因为不能保证图是连通的,要每个顶点都遍历
     if (!used[v]) dfs(v);
 }
 memset(used, 0, sizeof(used));
 int k = 0;
 for (int i = vs.size() - 1; i >= 0; i--) {
     if (!used[vs[i]]) rdfs(vs[i], k++); //遍历强连通分量
 }
 return k;
}

tarjian算法

example:
在这里插入图片描述

Tarjan算法是用来求强连通分量的,它是一种基于DFS(深度优先搜索)的算法,每个强连通分量为搜索树中的一棵子树。并且运用了数据结构栈。
先引入两个非常重要的数组:dfn[ ]low[ ]

dfn[ ]:就是一个时间戳(被搜到的次序),一旦某个点被DFS到后,这个时间戳就不再改变(且每个点只有唯一的时间戳)。所以常根据dfn的值来判断是否需要进行进一步的深搜。

low[ ]:该子树中,且仍在栈中的最小时间戳,像是确立了一个关系,low[ ]相等的点在同一强连通分量中
  注意:low标记相同则一定在强连通分量中,但在同一强连通分量中,low标记不一定相同

初始化时 dfn[ ] = low[ ] = ++cnt.

思路:
  首先这个图不一定是一个连通图,所以跑Tarjan时要枚举每个点,若dfn[ ] == 0,进行深搜。(这个和两次dfs求拓扑序相似)

然后对于搜到的点寻找与其有边相连的点,判断这些点是否已经被搜索过,若没有,则进行搜索。若该点已经入栈,说明形成了环,则更新low.
在不断深搜的过程中如果没有路可走了(出边遍历完了),那么就进行回溯,回溯时不断比较low[ ],取最小的low值。如果dfn[x]==low[x]则x可以看作是某一强连通分量子树的根,也说明找到了一个强连通分量,然后对栈进行弹出操作,直到x被弹出。

void tarjan(int now)  //这里的邻接表存图用的是链式前向星
{
    dfn[now]=low[now]=++cnt;  //初始化
    stack[++t]=now;       //入栈操作
    v[now]=1;            //v[]代表该点是否已入栈
    for(int i=f[now];i!=-1;i=e[i].next)  //邻接表存图
        if(!dfn[e[i].v])           //判断该点是否被搜索过
        {
            tarjan(e[i].v);
            low[now]=min(low[now],low[e[i].v]); //回溯时更新low[ ],取最小值
        }
        else if(v[e[i].v]) //遍历到过并且在栈中
            low[now]=min(low[now],dfn[e[i].v]); //一旦遇到已入栈的点,就将该点作为连通量的根
                             //这里用dfn[e[i].v]更新的原因是:这个点可能
                             //已经在另一个强连通分量中了但暂时尚未出栈,所
                             //以now不一定能到达low[e[i].v]但一定能到达
                             //dfn[e[i].v].
    if(dfn[now]==low[now])
    {
    	//sccn++; //求拓扑时用到			
        int cur;
        do//一直出栈直到now也出栈
        {
            cur=stack[t--];                
            v[cur]=false;                       //不要忘记出栈
  			//cmp[cur] = sccn;//拓扑序
            //这里出栈的所有节点都属于同一个强连通分量!!!
            
        }while(now!=cur);
    }
}

在main函数内要枚举每个点
for(int i=1;i<=n;++i)
        if(!dfn[i]) tarjan(i); //若dfn没有值(为0)就表示未被搜索到过

最后这里附上一题前天刚打的CF的E题(水题。。当时是想到了思路,但不知道咋实现,,,好笨啊。。。。)
题目
E. Directing Edges
time limit per test3 seconds
memory limit per test256 megabytes
inputstandard input
outputstandard output
You are given a graph consisting of n vertices and m edges. It is not guaranteed that the given graph is connected. Some edges are already directed and you can’t change their direction. Other edges are undirected and you have to choose some direction for all these edges.

You have to direct undirected edges in such a way that the resulting graph is directed and acyclic (i.e. the graph with all edges directed and having no directed cycles). Note that you have to direct all undirected edges.

You have to answer t independent test cases.

Input
The first line of the input contains one integer t (1≤t≤2⋅104) — the number of test cases. Then t test cases follow.

The first line of the test case contains two integers n and m (2≤n≤2⋅105, 1≤m≤min(2⋅105,n(n−1)2)) — the number of vertices and the number of edges in the graph, respectively.

The next m lines describe edges of the graph. The i-th edge is described with three integers ti, xi and yi (ti∈[0;1], 1≤xi,yi≤n) — the type of the edge (ti=0 if the edge is undirected and ti=1 if the edge is directed) and vertices this edge connects (the undirected edge connects vertices xi and yi and directed edge is going from the vertex xi to the vertex yi). It is guaranteed that the graph do not contain self-loops (i.e. edges from the vertex to itself) and multiple edges (i.e. for each pair (xi,yi) there are no other pairs (xi,yi) or (yi,xi)).

It is guaranteed that both sum n and sum m do not exceed 2⋅105 (∑n≤2⋅105; ∑m≤2⋅105).

Output
For each test case print the answer — “NO” if it is impossible to direct undirected edges in such a way that the resulting graph is directed and acyclic, otherwise print “YES” on the first line and m lines describing edges of the resulted directed acyclic graph (in any order). Note that you cannot change the direction of the already directed edges. If there are several answers, you can print any.

Example
inputCopy
4
3 1
0 1 3
5 5
0 2 1
1 1 5
1 5 4
0 5 2
1 3 5
4 5
1 1 2
0 4 3
1 3 1
0 2 3
1 2 4
4 5
1 4 1
1 1 3
0 1 2
1 2 4
1 3 2
outputCopy
YES
3 1
YES
2 1
1 5
5 4
2 5
3 5
YES
1 2
3 4
3 1
3 2
2 4
NO
Note
Explanation of the second test case of the example:
在这里插入图片描述
Explanation of the third test case of the example:

在这里插入图片描述
思路:
拓扑排序,也就是强连通分量分解
先将有向边建图,跑一边强连通,若有环(即分量个数不等于顶点个数)那么就不可能达到要求,否则一定可以
无向边u,v .若dep[u] < dep[v],根据拓扑序,可以将无向边的方向定为u->v,这样就不可能形成环!这里用的是跑两遍dfs

#include<iostream>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<vector>
#include<cstdio>
#include<map>
using namespace std;
typedef long long ll;
#define eps 1e-9
#define ls (rt << 1)
#define rs (rt << 1 | 1)
#define SIZE 1010
#define IOS ios_base::sync_with_stdio(0); cin.tie(0);
const int INF = 0x3f3f3f3f;
const int maxn = 2e5 + 10;
int n, m, V;
vector<int> G[maxn];
vector<int> rG[maxn];
vector<int> vs;
bool used[maxn];
int cmp[maxn];
void init() {
    for (int i = 0; i < V; i++) {
        G[i].clear();
        rG[i].clear();
    }
//    vs.clear();
}
void add_edge(int from, int to) {
    G[from].push_back(to);
    rG[to].push_back(from);
}

void dfs(int v) {
    used[v] = true;
    for (int i = 0; i < (int)G[v].size(); i++) {
        if (!used[G[v][i]]) dfs(G[v][i]);
    }
    vs.push_back(v);
}
void rdfs(int v, int k) {
    used[v] = true;
    cmp[v] = k;
    for (int i = 0; i < (int)rG[v].size(); i++) {
        if (!used[rG[v][i]]) rdfs(rG[v][i], k);
    }
}
int scc() {
    memset(used, 0, sizeof(used));
    vs.clear();
    for (int v = 0; v < V; v++) {
        if (!used[v]) dfs(v);
    }
    memset(used, 0, sizeof(used));
    int k = 0;
    for (int i = vs.size() - 1; i >= 0; i--) {
        if (!used[vs[i]]) rdfs(vs[i], k++);
    }
    return k;
}
int p[maxn][2];
int ans[maxn][2];
int main () {
    int t;
    scanf("%d", &t);
    while (t--) {
        scanf("%d%d", &n, &m);
        V = n;
        init();
        int cnt = 0;
        int ccnt = 0;
        for (int i = 0, t, u, v; i < m; i++) {
            scanf("%d%d%d", &t, &u, &v);
            --u;
            --v;
            if (t == 1) {
                add_edge(u, v);
                ans[ccnt][0] = u;
                ans[ccnt++][1] = v;
            }
            else {
                p[cnt][0] = u;
                p[cnt++][1] = v;
            }
        }
        int k = scc();
        if (k < n) printf("NO\n");
        else {
            for (int i = 0; i < cnt; i++) {
                int u = p[i][0];
                int v = p[i][1];
//                printf("%d %d\n", u, v);
                if (cmp[u] < cmp[v]) {
                    ans[ccnt][0] = u;
                    ans[ccnt++][1] = v;
                }
                else {
                    ans[ccnt][0] = v;
                    ans[ccnt++][1] = u;
                }
            }
            printf("YES\n");
            for (int i = 0; i < ccnt; i++) {
                printf("%d %d\n", ans[i][0] + 1, ans[i][1] + 1);
            }
        }

    }
    return 0;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拓扑排序、割点与割边以及强连通分量是图论中的重要概念和算法。 1. 拓扑排序(Topological Sorting): 拓扑排序是对有向无环图(DAG)进行排序的一种算法拓扑排序可以得到一个顶点的线性序列,使得对于任意一条有向边(u, v),在序列中顶点u都排在顶点v的前面。拓扑排序常用于表示任务之间的依赖关系,例如在工程项目中确定任务的执行顺序。 2. 割点与割边(Cut Vertex and Cut Edge): 割点是指在无向连通图中,如果移除该顶点以及与该顶点相连的所有边,会导致图不再连通,则该顶点被称为割点。割边是指在无向连通图中,如果移除该边,会导致图不再连通,则该边被称为割边。割点和割边的存在可以影响到图的连通性,因此在网络设计、通信等领域有着重要的应用。 3. 强连通分量(Strongly Connected Component): 强连通分量是指在有向图中,如果对于图中任意两个顶点u和v,存在从u到v和从v到u的路径,那么称u和v在同一个强连通分量中。强连通分量可以将有向图的顶点划分成若干个子集,每个子集内的顶点之间互相可达。强连通分量可以用于分析网络中的关键节点,寻找网络的可靠性,以及在编译器设计中进行代码优化等领域。 这些概念和算法在图论中都有着广泛的应用,并且还有许多相关的算法和扩展。深入理解和掌握这些概念和算法,可以帮助我们更好地理解和解决各种与图相关的问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值