1/13 关于SCC/Tarjan/Kosaraju

本文深入解析了计算有向图强连通分量的两种经典算法:Kosaraju算法与Tarjan算法。通过详细的步骤说明和伪代码展示,帮助读者理解这两种算法的工作原理及其实现细节。

参考网址:

  1. 浅析强连通分量(Tarjan和kosaraju)
  2. Tarjan算法||配图很详细
  3. Kosaraju算法
  4. Kosaraju算法代码参考||关于如何输出逆后序的函数||关于证明其正确性的详细说明
  5. 全网最详细Tarjan算法
  6. 强连通法的个人理解||内有C++代码

强连通量性质:
在这里插入图片描述
reflexive:自反性
symmetric:对称性
transitive:传递性


Kosaraju算法

在计算科学中,Kosaraju的算法(又称为–Sharir Kosaraju算法)是一个线性时间(linear time)算法找到的有向图的强连通分量。它利用了一个事实,逆图(与各边方向相同的图形反转, transpose graph)有相同的强连通分量的原始图。

  1. Kosaraju也是基于深度优先搜索的算法。这个算法牵扯到两个概念,发现时间st,完成时间et。发现时间是指一个节点第一次被遍历到时的次序号,完成时间是指某一结点最后一次被遍历到的次序号。
  2. 在加边时把有向图正向建造完毕后再反向加边建一张逆图。
  3. 先对正图进行一遍dfs,遇到没访问过的点就让其发现时间等于目前的dfs次序号。在回溯时若发现某一结点的子树全部被遍历完,就让其完成时间等于目前dfs次序号。正图遍历完后将节点按完成时间入栈,保证栈顶是完成时间最大的节点,栈底是完成时间最小的节点。然后从栈顶开始向下每一个没有被反向遍历过的节点为起点对逆图进行一遍dfs,将访问到的点记录下来(或染色)并弹栈,每一遍反向dfs遍历到的点就构成一个强连通分量。

Kosaraju 算法过程伪代码:

首先,创建一个空表,存储已经遍历完的节点。
在这里插入图片描述

三种颜色代表含义:

  • Gray : 正在遍历
  • White:未遍历
  • Black:遍历结束

assigned:相当于一个flag,标志是否遍历过,递归后每当检验到一个没有被标志的节点,强连通量SCC就加一。所以互相连通的节点时在同一个强连通分量里的。

SCC:计算连通分量。


先访问1(gray),到6(gray),到8(gray),再到7(black)(因为唯一的邻接点已经是gray),此时7遍历结束,放进L中。然后逆序回到8,回到6,回到1,将他们依次标为black。
在这里插入图片描述
寻找到第二个入度为0的点,此时选择3->5->2->4,没有下一个没遍历过的邻接点,所以依次将4->2->5>3标为black。
在这里插入图片描述
得到的连通分量:
在这里插入图片描述

  • transpose:转置。

Kosaraju算法正确性证明:

引理:
在这里插入图片描述
在这里插入图片描述
总结一下就是只有两种情形

①u的开始和结束都包含在v的开始和结束中,此时他们是连通的。
②u的开始和结束都包含在v的开始和结束外,此时他们没有关系。



Tarjan 算法

Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度。
定义:如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。

在有向图G中,如果两点互相可达,则称这两个点强连通,如果G中任意两点互相可达,则称G是强连通图。

定理:
1、一个有向图是强连通的,当且仅当G中有一个回路,它至少包含每个节点一次。
2、非强连通有向图的极大强连通子图,称为强连通分量(SCC即Strongly Connected Componenet)

理解:
1.Tarjan算法是基于对图深度优先搜索DFS的算法,每个强连通分量为搜索树中的一棵子树。总的来说, Tarjan算法基于一个观察,即:同处于一个SCC中的结点必然构成DFS树的一棵子树。 我们要找SCC,就得找到它在DFS树上的根。
算法思想如下:
2. dfn[u]表示dfs时达到顶点u的次序号(时间戳),low[u]表示以u为根节点的dfs树中次序号最小的顶点的次序号,所以当dfn[u]=low[u]时,以u为根的搜索子树上所有节点是一个强连通分量。 先将顶点u入栈,dfn[u]=low[u]=++idx,扫描u能到达的顶点v,如果v没有被访问过,则dfs(v),low[u]=min(low[u],low[v]),如果v在栈里,low[u]=min(low[u],dfn[v]),扫描完v以后,如果dfn[u]=low[u],则将u及其以上顶点出栈。

时间戳(timestamp),通常是一个字符序列,唯一地标识某一刻的时间。

伪代码:

tarjan(u){

  DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值

  Stack.push(u)   // 将节点u压入栈中

  for each (u, v) in E // 枚举每一条边

    if (v is not visted) // 如果节点v未被访问过

        tarjan(v) // 继续向下找

        Low[u] = min(Low[u], Low[v])

    else if (v in S) // 如果节点u还在栈内

        Low[u] = min(Low[u], DFN[v])

  if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根

  repeat v = S.pop  // 将v退栈,为该强连通分量中一个顶点

  print v

  until (u== v)

}

代码实现:

题目描述 原题来自:USACO 2003 Fall 每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛,给你 M 对整数 (A, B),表示牛 A 认为 牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认 为牛 C 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。 输入格式 第一行两个数 N, M; 接下来 M 行,每行两个数 A, B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出 现多个 A, B)。 输出格式 输出被除自己之外的所有牛认为是受欢迎的牛的数量。 样例 样例输入 3 3 1 2 2 1 2 3 样例输出 1 样例说明 只有第三头牛被除自己之外的所有牛认为是受欢迎的。 限制与提示 对于全部数据, 。#include <bits/stdc++.h> using namespace std; const int N = 1e4 + 7; int low[N], dfn[N], cnt = 0, scc = 0,ru[N],n,m,ans[N]; // low[u]: u 能回溯到的最早节点,dfn[u]: u 的 DFS 序 ru 缩点后每个点的入度 ans 每个牛的受欢迎程度 bool vis[N]; // vis[u]: u 是否在栈中 vector<int> adj[N]; // 原图的邻接表 stack<int> stk; // Tarjan 算法用的栈 vector<vector<int>> all_sccs; // 存储所有 SCC vector<int> component(N); // component[u] = u 所属的 SCC 编号 vector<unordered_set<int>> new_adj(all_sccs.size()); // 缩点后的邻接表(去重) // Tarjan 算法SCC void tarjan(int u) { low[u] = dfn[u] = ++cnt; // 初始化 dfn 和 low stk.push(u); // 将 u 压入栈 vis[u] = true; // 标记 u 在栈中 for (auto &v : adj[u]) { // 遍历 u 的所有邻接点 v if (!dfn[v]) { // 如果 v 未被访问过(未分配 dfn) tarjan(v); // 递归处理 v low[u] = min(low[u], low[v]); // 更新 u 的 low 值(子树的最小 low) } else if (vis[v]) { // 如果 v 已被访问且在栈中(说明是回边) low[u] = min(low[u], dfn[v]); // 更新 u 的 low 值(取 dfn[v] 和 low[u] 的最小值) } } // 如果 u 是当前 SCC 的根节点(low[u] == dfn[u]) if (low[u] == dfn[u]) { vector<int> scc_nodes; // 存储当前 SCC 的所有节点 int v; do { v = stk.top(); // 弹出栈顶节点 stk.pop(); vis[v] = false; // 标记 v 不在栈中 scc_nodes.push_back(v); // 加入当前 SCC } while (v != u); // 直到弹回 u 为止 sort(scc_nodes.begin(), scc_nodes.end()); // 排序(可选) all_sccs.push_back(scc_nodes); // 保存当前 SCC } } void toposort(){ memset(ans ,0,sizeof(ans)); queue<int> q; bool vis2[N]; memset(vis2,0,sizeof(vis2)); for(int i = 1;i <= n;i++){ if(!ru[i]){ q.push(i);vis2[i] = 1;//初始化 } } while(!q.empty()){ int now = q.front(); q.pop(); for(int v : new_adj[now]){ if(!vis2[v]){ ru[v]--;ans[v] = ans[now] + 1; if(ru[v] == 0){ q.push(v),vis2[v] = 1; } } } } return ; } int main() { freopen("popular.in","r",stdin); freopen("popular.out","w",stdout); cin >> n >> m; for (int i = 1; i <= m; i++) { int a, b; cin >> a >> b; adj[a].push_back(b); // 建图(有向边 a→b) } // 对所有未访问的节点运行 Tarjan 算法 for (int i = 1; i <= n; i++) { if (!dfn[i]) { tarjan(i); } } // 按 SCC 的最小节点排序(可选) sort(all_sccs.begin(), all_sccs.end(), [](const vector<int> &a, const vector<int> &b) { return a.front() < b.front(); }); // 记录每个节点属于哪个 SCC for (int i = 0; i < all_sccs.size(); i++) { for (int u : all_sccs[i]) { component[u] = i; } } // 构建缩点后的图(DAG) for (int u = 1; u <= n; u++) { for (int v : adj[u]) { int cu = component[u], cv = component[v]; if (cu != cv) { // 如果 u 和 v 不在同一个 SCC new_adj[cu].insert(cv); // 添加边 cu→cv(自动去重) } } } for(int u = 1;u <= n;u++){ for(int v : new_adj[u]){ ru[v]++; } } // // 输出 SCC 数量 // cout << all_sccs.size() << '\n'; // // // 输出每个 SCC 的节点 // for (const auto &scc : all_sccs) { // for (int node : scc) { // cout << node << " "; // } // cout << endl; // } toposort(); int ans1 = 0; for(int i = 1;i <= n;i++){ // cout << ans[i]; if(ans[i] == n - 1)ans1++; } cout << ans1 << '\n'; return 0; }
最新发布
08-08
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值