图论-无向图连通性
连通性相关问题是图论中比较重要的一部分,其具有较大的现实性意义(所以比较好拿来出题)。本文主要介绍无向图中相关问题。
基础概念
将后文所有需要用的概念都放这里了。(感性理解,要严格定义去看 oi-wiki)
联通:两点之间相互可达。
联通图:某个图中,任意两点之间都联通。
联通分量:极大的联通子图(再加任意一条边/一个点进来都不是联通子图了)。
DFS 树(森林):采用 DFS 的方式遍历一个图,所经过的边和点构成的一棵树称之为原图的 DFS 树(森林)。原图是个联通图就是树,否则就是森林。
树边/非树边:在 DFS 树上的边就是树边,反之就是非树边。
时间戳(dfn):按照 DFS 遍历原图,每个点被访问到的时间顺序。
追溯值(low):不经过其父亲节点能到达的时间戳最小的点的时间戳(怎么这么拗口)。
割点(割顶):对于一个无向图,如果把某个点以及连接这个点的边删掉后这个图的联通分量数增加了,那么这个点就是这个图的割点(割顶)。
割边(桥):对于一个无向图,如果把某条边删掉后这个图的联通分量数增加了,那么这条边就是这个图的割边(桥)。
边双联通图:一个没有桥的联通图。
边双联通分量:极大的边双联通图。
点双联通图:一个没有割点的联通图。
点双联通分量:极大的点双联通图。
无向图的 Tarjan 算法
其实就是求解 dfn 数组和 low 数组的过程。
算法流程:DFS 遍历原图,在 DFS 的过程中维护出 dfn 的值。对于某个点
大致代码如下:
void tarjan(int u , int father){
dfn[u] = low[u] = ++clk;
for(int v : g[u]){
if(!dfn[v]){
tarjan(v , u);
low[u] = min(low[u] , low[v]);
}
else if(v != father) low[u] = min(low[u] , dfn[v]);
}
}
割点与桥、双联通分量、缩点
割点的判定:对于一个点
题目连接:洛谷-P3388
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 100;
int dfn[N] , low[N] , clk;
int id[N];
int n , m;
bool cut[N];
vector <int> g[N];
int ans;
void tarjan(int u , int fa){
dfn[u] = low[u] = ++clk;
int ch = 0;
for(int v : g[u]){
if(!dfn[v]){
tarjan(v , u);
low[u] = min(low[u] , low[v]);
if(fa == 0 ? ++ch > 1 : low[v] >= dfn[u]) cut[u] = 1;
}
else if(v != fa) low[u] = min(low[u] , dfn[v]);
}
if(cut[u]) ans++;
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int u , v;scanf("%d%d",&u,&v);
g[u].push_back(v);g[v].push_back(u);
}
for(int i = 1;i <= n;i++) if(!dfn[i]) tarjan(i , 0);
printf("%d\n",ans);
for(int i = 1;i <= n;i++) if(cut[i]) printf("%d ",i);
return 0;
}
桥的判定:首先,非树边肯定不是桥,(毕竟有你没你一个样)。对于树边,跟割点类似,对于一个点
由于不用特殊判断根节点,甚至比割点好写,嘻嘻~
题目链接:UVA-796
代码:
#include <bits/stdc++.h>
#define Pair pair<int , int>
#define pbp(x , y) push_back(make_pair(min(x,y) , max(x,y)))
#define pb push_back
#define fi first
#define se second
using namespace std;
const int N = 1e5 + 100;
int dfn[N] , low[N] , clk;
vector <Pair> ans;
vector <int> g[N];
int cnt;
int n;
void tarjan(int u , int fa){
dfn[u] = low[u] = ++clk;
for(int v : g[u]){
if(!dfn[v]){
tarjan(v , u);
low[u] = min(low[u] , low[v]);
if(low[v] > dfn[u]) ans.pbp(u , v);
}
else if(v != fa) low[u] = min(low[u] , dfn[v]);
}
}
void init(){
memset(dfn , 0 , sizeof(dfn));
memset(low , 0 , sizeof(low));
clk = 0;
for(int i = 0;i < n;i++) g[i].clear();
ans.clear();
}
int main(){
while(scanf("%d",&n) != EOF){
init();
int u , v , k;
for(int i = 0;i < n;i++){
scanf("%d (%d)",&u,&k);
while(k--){
scanf("%d",&v);
g[u].pb(v);
}
}
for(int i = 0;i < n;i++) if(!dfn[i]) tarjan(i , -1);
sort(ans.begin(),ans.end());
printf("%d critical links\n",(int)ans.size());
for(Pair x : ans) printf("%d - %d\n",x.fi,x.se);
puts("");
}
return 0;
}
边双联通分量的求法:根据定义,边双联通分量没有桥。于是将把原图中的桥去掉,新图中每个联通块都会对应一个边双联通分量了。
题目链接:洛谷-P8436
代码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 5e5 + 100;
const int M = 4e6 + 100;
int n , m;
int dfn[N] , low[N] , clk;
int h[N] , nxt[M] , to[M] , idx = 1;
int b[M];
int dcc[N];
void add(int u , int v){
if(u == v) return ;
nxt[++idx] = h[u];
to[idx] = v;
h[u] = idx;
nxt[++idx] = h[v];
to[idx] = u;
h[v] = idx;
}
void tarjan(int u , int in){
dfn[u] = low[u] = ++clk;
for(int i = h[u];i;i = nxt[i]){
int v = to[i];
if(!dfn[v]){
tarjan(v , i);
low[u] = min(low[u] , low[v]);
if(low[v] > dfn[u]) b[i] = b[i ^ 1] = 1;
}
else if(i != (in ^ 1)) low[u] = min(low[u] , dfn[v]);
}
}
int cnt;
vector<int> ans[M];
void dfs(int u){
dcc[u] = cnt;
ans[cnt].push_back(u);
for(int i = h[u];i;i = nxt[i]){
int v = to[i];
if(!dcc[v] && !b[i]) dfs(v);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int u , v;
scanf("%d%d",&u,&v);
add(u , v);
}
for(int i = 1;i <= n;i++) if(!dfn[i]) tarjan(i , 0);
for(int i = 1;i <= n;i++) if(!dcc[i]){
++cnt;
dfs(i);
}
printf("%d\n",cnt);
for(int i = 1;i <= cnt;i++){
printf("%d ",(int)ans[i].size());
for(int nod : ans[i]) printf("%d ",nod);
puts("");
}
return 0;
}
点双联通分量的求法:在 Tarjan 算法的过程中,维护一个栈。当一个点第一次被访问时,将其入栈。当某个点
题目链接:洛谷-P8435
代码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 5e5 + 100;
int dfn[N] , low[N] , clk;
int stk[N] , tp;
int id[N];
int n , m;
bool cut[N];
vector <int> g[N];
int rt;
vector<int> vdcc[N];
int cnt;
void tarjan(int u , int fa){
dfn[u] = low[u] = ++clk;
stk[++tp] = u;
int ch = 0;
for(int v : g[u]){
if(!dfn[v]){
tarjan(v , u);
low[u] = min(low[u] , low[v]);
if(low[v] >= dfn[u]){
if(u != rt || ++ch > 1) cut[u] = 1;
vdcc[++cnt].pb(u);
int now;
do{
now = stk[tp--];
vdcc[cnt].pb(now);
}while(now != v);
}
}
else if(v != fa) low[u] = min(low[u] , dfn[v]);
}
if(!fa && !ch) vdcc[++cnt].pb(u);
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int u , v;scanf("%d%d",&u,&v);
g[u].pb(v);g[v].pb(u);
}
for(int i = 1;i <= n;i++) if(!dfn[i]){
rt = i , tp = 0;
tarjan(i , 0);
}
printf("%d\n",cnt);
for(int i = 1;i <= cnt;i++){
printf("%d ",(int)vdcc[i].size());
for(int v : vdcc[i]) printf("%d ",v);
puts("");
}
return 0;
}
例题及扩展应用
……感觉这些题出的都好模板。
洛谷-P2860
一句话题意:往图中加一些边,使得原图是个边双连通图。问最少需要加多少条边。
显然先求边双联通分量,然后缩点。题目保证原图联通,则缩点后一定是一棵树。否则如果有环,这些环上的点可以构成边双,继续缩点。
先给一个结论:假设这棵树中度数为
我们一步步来证明它:首先如果原图中所有点度数都大于
分两种情况讨论:若图中还存在两个度数为
综上,我们证明了只需要
代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5050 , M = 20050;
int dfn[N] , low[N] , clk , b[M];
int h[N] , nxt[M] , ver[M] , idx = 1;
int dcc[N] , cnt;
vector<int> g[N];
void add(int u , int v){
nxt[++idx] = h[u];ver[idx] = v;h[u] = idx;
nxt[++idx] = h[v];ver[idx] = u;h[v] = idx;
}
void tarjan(int u , int in){
dfn[u] = low[u] = ++clk;
for(int i = h[u];i;i = nxt[i]){
int v = ver[i];
if(!dfn[v]){
tarjan(v , i);
low[u] = min(low[u] , low[v]);
if(low[v] > dfn[u]) b[i] = b[i ^ 1] = 1;
}
else if(i != (in ^ 1)) low[u] = min(low[u] , dfn[v]);
}
}
void dfs(int u){
dcc[u] = cnt;
for(int i = h[u];i;i = nxt[i]){
int v = ver[i];
if(!dcc[v] && !b[i]) dfs(v);
}
}
int n , m , u , v;
int in[N];
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
scanf("%d%d",&u,&v);
add(u , v);
}
tarjan(1 , 0);
for(int i = 1;i <= n;i++) if(!dcc[i]) cnt++,dfs(i);
for(u = 1;u <= n;u++){
for(int i = h[u];i;i = nxt[i]){
v = ver[i];
if(dcc[u] != dcc[v]) in[dcc[v]]++;
}
}
int ans = 0;
for(int i = 1;i <= cnt;i++) if(in[i] == 1) ans++;
printf("%d\n",(ans + 1) / 2);
return 0;
}
HDU-4738
好像又是模板。注意细节,本身不联通就不需要炸桥,输出
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
const int inf = 1e9;
const int M = 2e6 + 100;
int n , m , ans;
int dfn[N] , low[N] , clk;
int h[N] , nxt[M] , to[M] , idx = 1 , w[M];
void add(int u , int v , int val){
nxt[++idx] = h[u];
to[idx] = v;
w[idx] = val;
h[u] = idx;
nxt[++idx] = h[v];
to[idx] = u;
w[idx] = val;
h[v] = idx;
return ;
}
void tarjan(int u , int in){
dfn[u] = low[u] = ++clk;
for(int i = h[u];i;i = nxt[i]){
int v = to[i];
if(!dfn[v]){
tarjan(v , i);
low[u] = min(low[u] , low[v]);
if(low[v] > dfn[u]) ans = min(ans , w[i]);
}
else if(i != (in ^ 1)) low[u] = min(low[u] , dfn[v]);
}
}
void init(){
for(int i = 1;i <= n;i++) dfn[i] = low[i] = 0;
clk = 0 , idx = 1 , ans = inf;
memset(h , 0 , sizeof(h));
}
int main(){
while(scanf("%d%d",&n,&m) != EOF){
if(!n && !m) break;
init();
for(int i = 1;i <= m;i++){
int x , y , z;
scanf("%d%d%d",&x,&y,&z);
add(x , y , z);
}
int flag = 0;
for(int i = 1;i <= n;i++) if(!dfn[i]) tarjan(i , 0) , flag++;
if(flag > 1) puts("0");
else printf("%d\n",(ans == inf) ? -1 : ((ans == 0) ? 1 : ans));
}
return 0;
}