连通性相关
强连通
强连通:有向图 \(G\) 中每个点中可互相到达。
强连通分量:极大的强连通。(最大可能的)
求强连通分量
先跑出图的 DFS 搜索树(黑边)。
一个结论:一个强连通分量 一定在该强连通分量中的第一个被访问的点 的子树内。
设根为 \(u\),考虑若存在一个点 \(v\) 不在 \(u\) 子树中则一定存在一条边离开 \(u\) 子树,只可能是返祖/横叉边,显然指向的点已经被标记过了。
Tarjan
一般都用这个。
记 \(dfn(u),low(u)\),后者表示不经过其父亲能到达的最小的 \(dfn\)。
记一个栈 \(stk\) 表示当前搜索到的节点,对于一个节点 \(v\):
- \(v\) 未被访问:先算 \(v\) 的答案,再用 \(low(v)\) 更新 \(low(u)\);
- \(v\) 访问过,且在栈中:用 \(dfn(v)\) 更新 \(low(u)\);
- \(v\) 访问过,不在栈中:\(v\) 所在连通分量已被处理完。不做操作。
最后只有满足 \(dfn(u)=low(u)\) 的 \(u\) 才能作为结论中的点,据此确定一个强连通分量 \(G=\braket{stk,\cdot}\)。
时间复杂度 \(O(n+m)\)。非常优秀。
int stk[maxn],top;
int dfn[maxn],dfncnt;
int low[maxn];
bool ins[maxn];
int scc[maxn],scccnt;
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u; ins[u]=1;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(ins[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
scccnt++;
while(stk[top]!=u){
scc[stk[top]]=scccnt;
ins[stk[top]]=0;
top--;
}
scc[stk[top]]=scccnt;
ins[stk[top]]=0;
top--;
}
}
Kosaraju
黑科技吧。实现非常简单:DFS,以后序遍历(即回溯时编号,相当于反 \(dfn\) 满足 \(rdfn(u)>rdfn(v)(v\in subtree(u))\))标号,在反图上从编号最大的点 DFS,所有遍历到的点即为一个强连通分量。???有点玄幻但的确是对的,马良极小。同样时间复杂度 \(O(n+m)\)。
int scc[maxn],scccnt;
int rdfn[maxn],dfncnt;
bool vis[maxn];
void dfs1(int u){
vis[u]=1;
for(int v:e[u])
if(!vis[v]) dfs1(v);
rdfn[++dfncnt]=u;
}
void dfs2(int u,int col){
scc[u]=col;
for(int v:re[u])
if(!scc[v]) dfs2(v);
}
void Kosaraju(){
for(int i=1;i<=n;i++)
if(!vis[i]) dfs1(i);
for(int i=n;i>=1;i--)
if(!scc[i]) dfs2(rdfn[i],++scccnt);
}
例题:[USACO03FALL / HAOI2006] 受欢迎的牛 G
每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。
先缩点,然后找 DAG 上出边为 0 的点,如果不唯一就无解,否则即为那个点包含的连通分量大小。
code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+3;
int n,m;
vector<int>e[maxn];
int stk[maxn],top;
int dfn[maxn],dfncnt,low[maxn];
int scc[maxn],scccnt;
bool ins[maxn];
int out[maxn],siz[maxn];
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u; ins[u]=1;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(ins[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
scccnt++;
while(stk[top]!=u){
scc[stk[top]]=scccnt;
ins[stk[top]]=0;
top--;
siz[scccnt]++;
}
scc[stk[top]]=scccnt;
ins[stk[top]]=0;
top--;
siz[scccnt]++;
}
}
signed main(){
cin>>n>>m;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
e[u].emplace_back(v);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++){
for(int j:e[i]){
if(scc[i]!=scc[j]){
out[scc[i]]++;
}
}
}
int oucnt=0,id=0;
for(int i=1;i<=scccnt;i++){
if(out[i]==0) oucnt++,id=i;
}
if(oucnt>1){
cout<<0;
}else{
cout<<siz[id];
}
return 0;
}
割
割点:如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点。
割边:如果把一条边删除后这个图的极大连通分量数增加了,那么这条边就是这个图的割边。(也叫桥)
求割点
Tarjan
还是它。
如果 \(\exists v\in son(u)\),\(low(v)\ge dfn(u)\),即点 \(v\) 不能通过除 \(u\) 以外的边回到祖先,则说明 \(u\) 是一个割点。
而注意到起始点的 \(low(v)\) 一定大于等于 \(dfn(u)\),却不能确定 \(u\) 是割点,如图点 1 并不是割点。
所以我们对于这种点需要特殊判断。在搜索树上,若它有大于一个儿子,则说明它是一个割点。
int rcnt;
void tarjan(int u,int rt){
dfn[u]=low[u]=++dfncnt;
for(int v:e[u]){
if(!dfn[v]){
if(u==rt) rcnt++;
tarjan(v,rt);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) ge[u]=1;
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
rcnt=0;
tarjan(i,0,i);
ge[i]=rcnt>1;
}
}
求割边
Tarjan
依然是它。
如果 \(\exists v\in son(u)\),\(low(v){\color{red}{\text{ > }}} dfn(u)\),即点 \(v\) 不能通过除 \(u\) 以外的边回到祖先,则说明 \((u,fa(u))\) 是一条割边。
为什么是大于呢,见图。
显然 \(low(v)=dfn(u)\) 并不能说明什么。图上 \(low(2)>dfn(1)\),即 2 只能通过走边 \((2,1)\) 才能到 1,即 \((2,1)\) 是割边。
并没有对应板子喵。
int fa[maxn],br[maxn];
void tarjan(int u,int Fa){
fa[u]=Fa;
dfn[u]=low[u]=++dfncnt;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) br[v]=1; // 割边为边 (v,fa[v])
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
双连通
点双连通:删除任意一个非 \(u,v\) 的点 \(u,v\) 仍然连通。称 \(u,v\) 点双连通。
边双连通:删除任意一条边 \(u,v\) 仍然连通。称 \(u,v\) 边双连通。
点双连通:极大的点双连通。
边双连通:极大的边双连通。
求边双连通分量
Tarjan
仍然可以用 tarjan,无向图上的边双等价于有向图上的强连通,由于是无向图注意不要走反边(可以直接判 fa,但是如果碰到大小为 2 的边双就只判第一次不走 fa 即可,也可以去重)。时间复杂度 \(O(n+m)\)。
void tarjan(int u,int fa){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u; ins[u]=1;
int pppp=0;
for(int v:e[u]){
if(v==fa&&!pppp){pppp=1; continue;}
if(!dfn[v]){
tarjan(v,u);
low[u]=min(low[u],low[v]);
}else if(ins[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
scccnt++;
while(stk[top]!=u){
scc[scccnt].emplace_back(stk[top]);
ins[stk[top]]=0;
top--;
}
scc[scccnt].emplace_back(stk[top]);
top--;
}
}
求点双连通分量
Tarjan
是它。
点双有两个性质:
- 两个有交的点双的交集大小为 1,即割点;
- 在一个点双中,dfn 最小的点一定是割点或者根。
对于性质 2 讨论:
- 若 \(u\) 是割点,则 \(u\) 为当前点双的根;
- 若 \(u\) 是根,则:
- 若 \(u\) 有大于 1 个子树,则 \(u\) 是割点;
- 若 \(u\) 有 1 个子树,则它是当前点双的根;
- 若 \(u\) 没有子树,则将其看做一个大小为 1 的点双。
void tarjan(int u,int rt){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u;
if(u==rt&&e[u].empty()){
scccnt++;
scc[scccnt].emplace_back(stk[top]);
return;
}
for(int v:e[u]){
if(!dfn[v]){
tarjan(v,rt);
if(u==rt) rcnt++;
if(low[v]>=dfn[u]){
if(u!=rt||rcnt>1) cut[u]=1; // 是割点
scccnt++;
while(stk[top]!=v){
scc[scccnt].emplace_back(stk[top]);
top--;
}
scc[scccnt].emplace_back(v);
top--;
scc[scccnt].emplace_back(u);
}
low[u]=min(low[u],low[v]);
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
圆方树
基于点双,将图上问题转化为树上(森林)问题。
对于每个点双,新建一个方点,将每个点双中的点连向方点,构建成一个树(森林)。
构建圆方树
Tarjan
还™是 tarjan。具体而言,每找到一个点双就新建一个方点,再每个圆点向方点连边即可。注意开双倍空间。
对于一个点的图,视具体情况处理。
int nodecnt; // 节点数
vector<int>g[maxn<<1]; // 圆方树
void tarjan(int u,int rt){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u;
if(u==rt&&e[u].empty()){
scccnt++;
scc[scccnt].emplace_back(stk[top]);
return;
}
for(int v:e[u]){
if(!dfn[v]){
tarjan(v,rt);
if(u==rt) rcnt++;
if(low[v]==dfn[u]){
if(u!=rt||rcnt>1) cut[u]=1; // 是割点
scccnt++;
while(stk[top]!=v){
scc[scccnt].emplace_back(stk[top]);
top--;
}
scc[scccnt].emplace_back(v);
top--;
scc[scccnt].emplace_back(u);
}
low[u]=min(low[u],low[v]);
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
void build_sct(){
nodecnt=n;
for(int i=1;i<=scccnt;i++){
if(scc[i].size()==1){
......
}else{
nodecnt++;
for(int j:scc[i]){
g[nodecnt].emplace_back(j);
g[j].emplace_back(nodecnt);
}
}
}
}
给你一个不一定连通的无向图,求三元组 \((s,c,f)\) 满足存在从 \(s\) 经过 \(c\) 到 \(f\) 的简单路径的数量。
\(n\le 10^5,m\le 2\times 10^5\)
建出圆方树,然后跑树形 DP 即可。
code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+3;
int n,m;
vector<int>e[maxn];
int dfn[maxn],dfncnt,low[maxn];
int trsiz;
int stk[maxn],top;
int nodecnt; // 节点数
vector<int>g[maxn<<1]; // 圆方树
int siz[maxn<<1],w[maxn<<1];
void tarjan(int u){
dfn[u]=low[u]=++dfncnt;
stk[++top]=u;
trsiz++;
for(int v:e[u]){
if(!dfn[v]){
tarjan(v);
if(low[v]==dfn[u]){
nodecnt++;
w[nodecnt]=0;
while(stk[top]!=v){
w[nodecnt]++;
g[nodecnt].emplace_back(stk[top]);
g[stk[top]].emplace_back(nodecnt);
top--;
}
w[nodecnt]++;
g[nodecnt].emplace_back(stk[top]);
g[stk[top]].emplace_back(nodecnt);
top--;
w[nodecnt]++;
g[nodecnt].emplace_back(u);
g[u].emplace_back(nodecnt);
}
low[u]=min(low[u],low[v]);
}else{
low[u]=min(low[u],dfn[v]);
}
}
}
int ans=0;
void dfs1(int u,int fa){
siz[u]=u<=n;
for(int v:g[u]){
if(v!=fa){
dfs1(v,u);
ans=ans+2*w[u]*siz[v]*siz[u];
siz[u]+=siz[v];
}
}
ans=ans+2*w[u]*siz[u]*(trsiz-siz[u]);
}
signed main(){
cin>>n>>m; nodecnt=n;
for(int i=1,u,v;i<=m;i++){
cin>>u>>v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
for(int i=1;i<=n;i++) w[i]=-1;
for(int i=1;i<=n;i++){
if(!dfn[i]){
trsiz=0;
top=0;
tarjan(i);
dfs1(i,0);
}
}
cout<<ans;
return 0;
}