Bạn đang ở thành phố A, muốn đi đến thành phố B và quay về A. Nếu có đường đi cả hai chiều → A và B thuộc cùng một "vùng liên thông mạnh". Nếu chỉ đi được một chiều → không thuộc cùng SCC.
SCC (Strongly Connected Component) trong đồ thị có hướng là tập con lớn nhất các đỉnh sao cho từ bất kỳ đỉnh nào trong tập, ta đều đi được đến bất kỳ đỉnh nào còn lại.
graph LR
subgraph SCC1["SCC 1"]
A(("1")) --> B(("2"))
B --> C(("3"))
C --> A
end
subgraph SCC2["SCC 2"]
D(("4")) --> E(("5"))
E --> D
end
subgraph SCC3["SCC 3"]
F(("6"))
end
B --> D
C --> F
E --> F
Trong đồ thị trên:
- SCC 1: {1, 2, 3} — đi được từ bất kỳ đỉnh nào đến bất kỳ đỉnh nào trong tập
- SCC 2: {4, 5} — 4 → 5 → 4
- SCC 3: {6} — đỉnh cô lập (không có chu trình đi qua)
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN];intnum[MAXN],low[MAXN],cnt;stack<int>st;boolonStack[MAXN];vector<vector<int>>sccs;voiddfs(intu){num[u]=low[u]=++cnt;st.push(u);onStack[u]=true;for(intv:adj[u]){if(num[v]==0){// Cạnh cây (tree edge)dfs(v);low[u]=min(low[u],low[v]);}elseif(onStack[v]){// Cạnh ngược (back edge) — v đang trong stacklow[u]=min(low[u],num[v]);}}// u là gốc của SCCif(low[u]==num[u]){vector<int>scc;while(true){intv=st.top();st.pop();onStack[v]=false;scc.push_back(v);if(v==u)break;}sccs.push_back(scc);}}intmain(){ios::sync_with_stdio(false);cin.tie(0);intn,m;cin>>n>>m;for(inti=0;i<m;i++){intu,v;cin>>u>>v;adj[u].push_back(v);}for(inti=1;i<=n;i++){if(num[i]==0)dfs(i);}cout<<sccs.size()<<"\n";// In ra từng SCC (nếu cần đánh số từ 1)vector<int>sccId(n+1);for(inti=0;i<(int)sccs.size();i++){for(intv:sccs[i]){sccId[v]=i+1;}}for(inti=1;i<=n;i++){cout<<sccId[i]<<" \n"[i==n];}}
importsyssys.setrecursionlimit(300000)input=sys.stdin.readlinedeftarjan_scc(n,adj):num=[0]*(n+1)low=[0]*(n+1)on_stack=[False]*(n+1)stack=[]sccs=[]cnt=[0]defdfs(u):cnt[0]+=1num[u]=low[u]=cnt[0]stack.append(u)on_stack[u]=Trueforvinadj[u]:ifnum[v]==0:dfs(v)low[u]=min(low[u],low[v])elifon_stack[v]:low[u]=min(low[u],num[v])# u là gốc của SCCiflow[u]==num[u]:scc=[]whileTrue:v=stack.pop()on_stack[v]=Falsescc.append(v)ifv==u:breaksccs.append(scc)foriinrange(1,n+1):ifnum[i]==0:dfs(i)returnsccsn,m=map(int,input().split())adj=[[]for_inrange(n+1)]for_inrange(m):u,v=map(int,input().split())adj[u].append(v)sccs=tarjan_scc(n,adj)print(len(sccs))scc_id=[0]*(n+1)fori,sccinenumerate(sccs):forvinscc:scc_id[v]=i+1print(*scc_id[1:])
Đỉnh hoàn thành muộn nhất trong DFS lượt 1 thuộc SCC "gốc" (không bị SCC khác "hút"). Khi đảo cạnh, DFS từ đỉnh này chỉ thăm được đúng các đỉnh trong cùng SCC.
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN],rev[MAXN];boolvisited[MAXN];vector<int>order;vector<vector<int>>sccs;voiddfs1(intu){visited[u]=true;for(intv:adj[u])if(!visited[v])dfs1(v);order.push_back(u);}voiddfs2(intu,vector<int>&scc){visited[u]=true;scc.push_back(u);for(intv:rev[u])if(!visited[v])dfs2(v,scc);}intmain(){ios::sync_with_stdio(false);cin.tie(0);intn,m;cin>>n>>m;for(inti=0;i<m;i++){intu,v;cin>>u>>v;adj[u].push_back(v);rev[v].push_back(u);}// Lượt 1: DFS trên đồ thị gốcfor(inti=1;i<=n;i++)if(!visited[i])dfs1(i);// Lượt 2: DFS trên đồ thị đảo theo thứ tự hoàn thànhmemset(visited,false,sizeof(visited));reverse(order.begin(),order.end());for(intu:order){if(!visited[u]){vector<int>scc;dfs2(u,scc);sccs.push_back(scc);}}cout<<sccs.size()<<"\n";vector<int>sccId(n+1);for(inti=0;i<(int)sccs.size();i++)for(intv:sccs[i])sccId[v]=i+1;for(inti=1;i<=n;i++)cout<<sccId[i]<<" \n"[i==n];}
Nén mỗi SCC thành 1 đỉnh "siêu". Kết quả là DAG (đồ thị có hướng không chu trình).
graph LR
subgraph SCC1["SCC {1,2,3}"]
direction LR
A(("1")) --> B(("2"))
B --> C(("3"))
C --> A
end
subgraph SCC2["SCC {4,5}"]
direction LR
D(("4")) --> E(("5"))
E --> D
end
F(("6"))
B --> D
C --> F
E --> F
// Sau khi có sccId[] (mỗi đỉnh thuộc SCC nào)vector<int>condAdj[MAXN];// Đồ thị coset<pair<int,int>>edges;// Trùng cạnhfor(intu=1;u<=n;u++){for(intv:adj[u]){intsu=sccId[u],sv=sccId[v];if(su!=sv&&edges.count({su,sv})==0){condAdj[su].push_back(sv);edges.insert({su,sv});}}}
# Sau khi có scc_id[] (mỗi đỉnh thuộc SCC nào)defbuild_condensation(n,adj,scc_id):num_scc=max(scc_id[1:])cond_adj=[[]for_inrange(num_scc+1)]seen=set()foruinrange(1,n+1):forvinadj[u]:su,sv=scc_id[u],scc_id[v]ifsu!=svand(su,sv)notinseen:cond_adj[su].append(sv)seen.add((su,sv))returncond_adj
Cầu (Bridge) là cạnh (u, v) mà sau khi xóa, đỉnh u và v không còn liên thông với nhau.
graph LR
A(("1")) --- B(("2"))
B --- C(("3"))
C --- A
B --- D(("4"))
D --- E(("5"))
E --- F(("6"))
F --- D
Trong đồ thị trên:
- Cạnh (B, D) = cầu — xóa nó thì {1,2,3} tách rời {4,5,6}
- Các cạnh trong {1,2,3} và {4,5,6} không phải cầu vì vẫn có đường đi khác
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN];intnum[MAXN],low[MAXN],cnt;boolisArt[MAXN];voiddfs(intu,intparent){num[u]=low[u]=++cnt;intchildCount=0;for(intv:adj[u]){if(v==parent)continue;if(num[v]==0){childCount++;dfs(v,u);low[u]=min(low[u],low[v]);// Trường hợp 2: u không phải gốcif(parent!=-1&&low[v]>=num[u]){isArt[u]=true;}}else{low[u]=min(low[u],num[v]);}}// Trường hợp 1: u là gốc và có ≥ 2 conif(parent==-1&&childCount>=2){isArt[u]=true;}}intmain(){ios::sync_with_stdio(false);cin.tie(0);intn,m;cin>>n>>m;for(inti=0;i<m;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}for(inti=1;i<=n;i++){if(num[i]==0){dfs(i,-1);}}vector<int>result;for(inti=1;i<=n;i++)if(isArt[i])result.push_back(i);cout<<result.size()<<"\n";for(intv:result)cout<<v<<" ";}
importsyssys.setrecursionlimit(300000)input=sys.stdin.readlinedeffind_articulation_points(n,adj):num=[0]*(n+1)low=[0]*(n+1)is_art=[False]*(n+1)cnt=[0]defdfs(u,parent):cnt[0]+=1num[u]=low[u]=cnt[0]child_count=0forvinadj[u]:ifv==parent:continueifnum[v]==0:child_count+=1dfs(v,u)low[u]=min(low[u],low[v])# Trường hợp 2: u không phải gốcifparent!=-1andlow[v]>=num[u]:is_art[u]=Trueelse:low[u]=min(low[u],num[v])# Trường hợp 1: u là gốc và có ≥ 2 conifparent==-1andchild_count>=2:is_art[u]=Trueforiinrange(1,n+1):ifnum[i]==0:dfs(i,-1)return[iforiinrange(1,n+1)ifis_art[i]]n,m=map(int,input().split())adj=[[]for_inrange(n+1)]for_inrange(m):u,v=map(int,input().split())adj[u].append(v)adj[v].append(u)arts=find_articulation_points(n,adj)print(len(arts))print(*arts)
// DSU gộp các đỉnh không bị tách bởi cầu// Sau khi tìm cầu, duyệt lại DFS bỏ qua cầuintcomp[MAXN];// comp[u] = id của 2ECC chứa uvoiddfs2ecc(intu,intid,intparent){comp[u]=id;for(intv:adj[u]){if(v==parent||comp[v]!=0)continue;// Kiểm tra (u,v) có phải cầu khôngif(isBridge(u,v))continue;dfs2ecc(v,id,u);}}
// SAI: quên gán low[u] = num[u]dfs(u){num[u]=low[u]=++cnt;// ← PHẢI CÓ cả low[u]}// Nếu chỉ gán num[u] mà không gán low[u]:// low[u] = 0 → thuật toán sai hoàn toàn
Đồ thị có hướng: SCC là tập đỉnh đi được lẫn nhau (dùng Tarjan/Kosaraju)
Đồ thị vô hướng: Khái niệm SCC không áp dụng (mọi đỉnh trong thành phần liên thông đều đi được lẫn nhau khi xem như đồ thị có hướng). Dùng connected component thay vì Tarjan!
// SAI: chỉ bỏ qua parentif(v==parent)continue;// Nếu có cạnh trùng (u,v) xuất hiện 2 lần → cần xử lý đúng// Cách an toàn: dùng visited edge hoặc đếm số lần gặp parent