有向图的强连通分量 scc
基本概念:
-
树枝边(x,y):x是y的父亲
-
前向边(x,y):x是y的祖先结点
-
后向边(x,y):y是x的祖先结点
-
横叉边(x,y):在对有向图进行dfs遍历时,x是已经搜过的图的分支(不是前向边),现在在搜的点是y,y到x的有向边是横叉边
连通分量作用
- 通过缩点,将图变成有向无环图
缩点步骤:
for i=1;i<=n;i++ for i的所有邻点j if i和j不在同一个scc中: 加一条新边id[i]→id[j]
tarjin算法求强连通分量
引入时间戳的概念:在dfs遍历的过程中,按照每个结点第一次被访问的时间顺序,依次给予图中N个结点1~N的整数标记,该标记被称为时间戳,记为dfn[x].
对每个点定义两个时间戳:
dfn[u]
表示遍历到u的时间戳;low[u]
表示从u开始走,所能遍历到的最小时间戳是什么。
我们在求强连通分量的时候,求的是每个强连通分量最上面的那个点,也就是最高点。
且若u是所在的强连通分量的最高点 等价于 dfn[u]== low[u],
因为low[u]表示的是从u开始能够遍历到的最小的时间戳,若正好等于自己的时间戳,则就是说明u是最高点。
tarjan模板
背模板的思路
- 加时间戳;
- 放入栈中,做好标记;
- 遍历邻点
1)如果没遍历过,tarjan一遍,用low[j]更新最小值low
2) 如果在栈中,用dfn[j]更新最小值low
4.找到最高点
1)scc个数++
2)do-while循环:
从栈中取出每个元素;标志为出栈;
对元素做好属于哪个scc;该scc中点的数量++
具体模板代码
// tarjan 算法求强连通分量 // 时间复杂度O(n+ m) void tarjan(int u){ // 初始化自己的时间戳 dfn[u] = low[u] = ++ timestamp; //将该点放入栈中 stk[++ top] = u, in_stk[u] = true; // 遍历和u连通的点 for(int i = h[u]; ~i; i = ne[i]){ int j = e[i]; if(!dfn[j]){ tarjan(j); // 更新u所能遍历到的时间戳的最小值 low[u] = min(low[u], low[j]); } // 如果当前点在栈中 //注意栈中存的可能是树中几个不同分支的点,因为有横叉边存在 // 栈中存的所有点,是还没搜完的点,同时都不是强连通分量的最高点 // 这里表示当前强连通分量还没有遍历完,即栈中有值 else if(in_stk[j]) //更新一下u点所能到的最小的时间戳 //此时j要么是u的祖先,要么是横叉边的点,时间戳小于u low[u] = min(low[u], dfn[j]); } // 找到该连通分量的最高点 if(dfn[u] == low[u]){ int y; ++ scc_cnt; // 强连通分量的个数++ do{// 取出来该连通分量的所有点 y = stk[top --]; in_stk[y] = false; id[y] = scc_cnt; // 标记点属于哪个连通分量 size_scc[scc_cnt] ++; } while(y != u); } }
题目
1174. 受欢迎的牛
思路:
先找到强连通分量,缩点。
- 假如存在两及以上个出度=0的牛(强连通分量) 则必然有一头牛(强连通分量)不被所有牛欢迎。
因为出度都为零的牛(强连通分量)之间一点不会喜欢。 - 当只有一个牛(强连通分量)的出度为0,则该强连通分量中的所有点都被其他强连通分量的牛欢迎
#include <bits/stdc++.h> #define endl '\n' #define int long long using namespace std; typedef pair<int, int> pii; const int N = 5e5 + 10; int n, m; vector<int> g[N]; int dfn[N], low[N], timestemp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, scc_size[N]; int dout[N]; // 记录新图中每个点(也就是原图每个连通分量)的出度 void tarjan(int u) { dfn[u] = low[u] = ++timestemp; stk[++top] = u, in_stk[u] = true; for (int i = 0; i < g[u].size(); i++) { int k = g[u][i]; // cout<<k<<endl; if (!dfn[k]) { tarjan(k); low[u] = min(low[u], low[k]); } else if (in_stk[k]) { low[u] = min(low[u], dfn[k]); } } if (low[u] == dfn[u]) { int k; scc_cnt++; do { k = stk[top--]; scc_size[scc_cnt]++; id[k] = scc_cnt; } while (k != u); } } void slove() { cin >> n >> m; for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; g[a].push_back(b); } for (int i = 1; i <= n; i++) { if (!dfn[i]) { tarjan(i); } } for (int i = 1; i <= n; i++) { for (int j : g[i]) { if (id[i] != id[j]) { dout[id[i]]++; } } } // 和本题有关的部分: // zeros是统计在新图中,出度为0的点的个数 // sum表示满足条件的点(最受欢迎的奶牛)的个数 int zeros = 0, sum = 0; for (int i = 1; i <= scc_cnt; i++) { if (!dout[i]) { zeros++; sum += scc_size[i]; if (zeros > 1) { sum = 0; break; } } } cout << sum << endl; } signed main() { slove(); return 0; }
练习
J - Watch Where You Step
原文
题意
给定有向图的邻接矩阵,现在需要给该图增加边,使得如果两点可达必直接可达,求需要加边的最大数量。
思路:
通过强连通分量缩点。
- 对每个联通块内部
将其中的点全部连接起来需要n*(n-1)条边(建完全图) - 对每个联通块之间
将其全部链接起来需要条边(v[i]为每个块中的点数),为避免重复建边,需要对每个联通块排序。
因此对整个图进行两次dfs
- 第一次 对原图跑dfs 用栈记录连通分量缩后的点访问顺序
- 第二次 对反向图根据出栈顺序跑dfs 获得每个联通块中的点的个数;
最后对每个块按序处理即可。
#include <bits/stdc++.h> #define endl '\n' #define int long long using namespace std; typedef pair<int, int> pii; const int N = 1e6 + 10; const int INF = 0x3f3f3f3f3f; int n; int a[N]; int cnt[N],st[N]; vector<int> g[N]; vector<int> gg[N]; int vis[N]; int vis2[N]; stack<int> stk; vector<int> vec; int sum_bian; void dfs(int u){ vis[u]=1; for(int v: g[u]){ if(!vis[v]) dfs(v); } stk.push(u); } int dfs2(int u){ vis2[u]=1; int res=1; for(int v:gg[u]){ if(!vis2[v]) res+=dfs2(v); } return res; } void slove() { int n;cin>>n; for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ int t;cin>>t; if(t) sum_bian++, g[i].push_back(j),gg[j].push_back(i); } } for(int i=0;i<n;i++){ if(!vis[i]) dfs(i); } while(stk.size()){ int k=stk.top(); stk.pop(); if(!vis2[k]) { int cnt=dfs2(k); vec.push_back(cnt); } } int ans=0; for(int i = 0; i < vec.size(); ++i) { ans += vec[i] * (vec[i] - 1); for(int j = i + 1; j < vec.size(); ++j) { ans += vec[i] * vec[j]; } } cout<<ans-sum_bian<<endl; } signed main() { slove(); return 0; }
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/16465873.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步