有向图的强联通分量
总起
目前做到的有向图的强联通分量大多是模板题,按照基本套路可以完成。对于该类题目,基本解决方法如下:
tarjan
算法- 缩点、去重边、建图
- 在新建的拓扑图中找出问题答案
- 基础结论
知识讲解
基本概念
算法原理
基础结论
tarjan
缩点后的图为拓扑图- 按照
scc_cnt
编号,倒序为拓扑序 - 将一个有向图变为强连通图最少需要添加条边,为
din
为的强联通分量的个数,为dout
为的强连通分量的个数
例题
AcWing 1174. 受欢迎的牛
-
题目描述:给定一个有向图,问有多少个点可以被除自己外的其他所有点到达。
-
解题思路:
tarjan
算法缩点- 本题无需建图,只需计算所有强连通分量的出度
- 若出度为的强连通分量只有一个,输出该强连通分量的
Size
;若出度为的强连通分量个数,输出0
-
代码:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 1e4 + 10, M = 5e4 + 10; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, Size[N]; int dout[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[++ top] = u, in_stk[u] = true; for(int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if(!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if(in_stk[j]) low[u] = min(low[u], dfn[j]); } if(dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top --]; in_stk[y] = false; id[y] = scc_cnt; Size[scc_cnt] ++; } while(y != u); } } int main() { scanf("%d%d", &n, &m); memset(h, -1, sizeof h); while(m --){ int a, b; scanf("%d%d", &a, &b); add(a, b); } for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i); for(int i = 1; i <= n; i ++){ for(int j = h[i]; j != -1; j = ne[j]){ int k = e[j]; int a = id[i], b = id[k]; if(a != b) dout[a] ++; } } int zeros = 0, sum = 0; for(int i = 1; i <= scc_cnt; i ++){ if(!dout[i]){ zeros ++; sum += Size[i]; if(zeros > 1){ sum = 0; break; } } } printf("%d\n", sum); return 0; }
AcWing 367. 学校网络
-
题目描述:给定一个有向图,按照图中边的方向进行信息传递,问:
- 至少需要给多少个点发送信息才能使得所有点收到信息
- 至少需要添加多少条边使得该图变成强连通图
-
解题思路:
tarjan
算法缩点- 本题无需建图,只需计算所有强连通分量的出度与入度
- 问题一的答案是入度为的强连通分量中点的个数
- 问题二的答案是,为
din
为的强联通分量的个数,为dout
为的强连通分量的个数
-
代码:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 110, M = 1e4 + 10; int n, m; int h[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, Size[N]; int dout[N], din[N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[++ top] = u, in_stk[u] = true; for(int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if(!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if(in_stk[j]) low[u] = min(low[u], dfn[j]); } if(dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top --]; in_stk[y] = false; id[y] = scc_cnt; Size[scc_cnt] ++; } while(y != u); } } int main() { scanf("%d", &n); memset(h, -1, sizeof h); for(int i = 1; i <= n; i ++){ int t; while(scanf("%d", &t), t){ add(i, t); } } for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i); for(int i = 1; i <= n; i ++){ for(int j = h[i]; j != -1; j = ne[j]){ int k = e[j]; int a = id[i], b = id[k]; if(a != b) din[b] ++, dout[a] ++; } } int a = 0, b = 0; for(int i = 1; i <= scc_cnt; i ++){ if(!din[i]) a ++; if(!dout[i]) b ++; } printf("%d\n", a); if(scc_cnt == 1) puts("0"); else printf("%d\n", max(a, b)); return 0; }
AcWing 1175. 最大半连通子图
-
题目描述:给出半连通图定义,给定一个有向图,问:
- 的最大半连通子图拥有的节点数
- 不同的最大半连通子图的数目
-
解题思路:
tarjan
算法缩点- 去重边并建图
- 求最大半连通子图拥有的节点数等价于求最长链上的节点数,不同最大半连通子图的数目等价于求方案数
- 在缩点后的拓扑图上进行,记录答案及方案数
-
代码:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <unordered_set> using namespace std; typedef long long ll; const int N = 1e5 + 10, M = 2e6 + 10; //M要开二倍,因为新图最坏与原图边数相同 int n, m, mod; int h[N], hs[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, Size[N]; int f[N], g[N]; void add(int h[], int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[++ top] = u, in_stk[u] = true; for(int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if(!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if(in_stk[j]) low[u] = min(low[u], dfn[j]); } if(dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top --]; in_stk[y] = false; id[y] = scc_cnt; Size[scc_cnt] ++; } while(y != u); } } int main() { scanf("%d%d%d", &n, &m, &mod); memset(h, -1, sizeof h); memset(hs, -1, sizeof hs); while(m --){ int a, b; scanf("%d%d", &a, &b); add(h, a, b); } for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i); unordered_set<ll> S; for(int i = 1; i <= n; i ++){ for(int j = h[i]; j != -1; j = ne[j]){ int k = e[j]; int a = id[i], b = id[k]; ll hash = a * 1000000ll + b; if(a != b && !S.count(hash)){ add(hs, a, b); S.insert(hash); } } } for(int i = scc_cnt; i; i --){ //scc_cnt倒序即为拓扑序 if(!f[i]){ f[i] = Size[i]; g[i] = 1; } for(int j = hs[i]; j != -1; j = ne[j]){ int k = e[j]; if(f[k] < f[i] + Size[k]){ f[k] = f[i] + Size[k]; g[k] = g[i]; } else if(f[k] == f[i] + Size[k]) g[k] = (g[k] + g[i]) % mod; } } int maxf = 0, sum = 0; for(int i = 1; i <= scc_cnt; i ++){ if(f[i] > maxf){ maxf = f[i]; sum = g[i]; } else if(f[i] == maxf) sum = (sum + g[i]) % mod; } printf("%d\n%d\n", maxf, sum); return 0; }
ABC 245 | F - Endless Walk
- 题目描述:给定一个有向图,问有所少点可以不停地走下去。
- 解题思路:
- 建反图
tarjan
算法缩点- 去重边并建图
- 答案为的强连通分量中的点以及这些点可以走到的点
- 不可以将
din
为0
的点加入队列进行bfs,直至走到的强连通分量,然后取补集作为答案,有反例如下:
此时id = 1
的节点会被误算进答案。
- 代码:
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <queue> #include <unordered_set> using namespace std; typedef long long ll; const int N = 2e5 + 10, M = 4e5 + 10; int n, m; int h[N], hs[N], e[M], ne[M], idx; int dfn[N], low[N], timestamp; int stk[N], top; bool in_stk[N]; int id[N], scc_cnt, Size[N]; int din[N]; bool st[N]; void add(int h[], int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan(int u) { dfn[u] = low[u] = ++ timestamp; stk[++ top] = u, in_stk[u] = true; for(int i = h[u]; i != -1; i = ne[i]) { int j = e[i]; if(!dfn[j]) { tarjan(j); low[u] = min(low[u], low[j]); } else if(in_stk[j]) low[u] = min(low[u], dfn[j]); } if(dfn[u] == low[u]) { ++ scc_cnt; int y; do { y = stk[top --]; in_stk[y] = false; id[y] = scc_cnt; Size[scc_cnt] ++; } while(y != u); } } int main() { scanf("%d%d", &n, &m); memset(h, -1, sizeof h); memset(hs, -1, sizeof h); while(m --){ int a, b; scanf("%d%d", &a, &b); add(h, b, a); } for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i); unordered_set<ll> S; for(int i = 1; i <= n; i ++){ for(int j = h[i]; j != -1; j = ne[j]){ int k = e[j]; int a = id[i], b = id[k]; ll hash = a * 1000000ll + b; if(a != b && !S.count(hash)){ add(hs, a, b); din[b] ++; S.insert(hash); } } } int ans = 0; queue<int> que; for(int i = 1; i <= scc_cnt; i ++) if(Size[i] > 1) { que.push(i); st[i] = true; } while(que.size()) { int t = que.front(); que.pop(); ans += Size[t]; for(int i = hs[t]; ~ i; i = ne[i]){ int j = e[i]; if(!st[j]) { que.push(j); st[j] = true; } } } printf("%d\n",ans); return 0; }
- 题目链接:https://atcoder.jp/contests/abc245/tasks/abc245_f
分类:
ACM
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库