再入有向图的强连通分量
tarjan
- 连通分量
对于分量中任意两点 u , v u,v u,v,必然可以从 u 走 到 v u走到v u走到v,且从 v 走 到 u v走到u v走到u
- 强连通分量 S C C SCC SCC
极大连通分量(加上其它任意一个点,都不是连通分量)
- 应用
将任意一个 有向图 ⇒ 缩 点 \Rightarrow^{缩点} ⇒缩点 有向无环图(DAG)拓扑图
- 求最短路/长路,递推
- 定义:
- 树枝边(dfs时的树边)
- 前向边
- 后向边
- 横叉边(只会往左边横叉,往右边其实是树枝边了)
时间戳:对每个点定义两个时间戳
dfn:遍历到u的时间戳
low:从u开始走,所能遍历到的最小时间戳
重要性质:无向图 D F S DFS DFS后的 d f s 树 dfs树 dfs树,所有的非树边都是从下往上的。
- 算法(判断 x x x是否是在一个强连通分量里)
- 它可以回到当前搜索树边的祖先节点。(后向边)
- 走横叉边,横插边可以走到祖先节点。
时间复杂度 O ( n + m ) O(n+m) O(n+m)
后续
缩点
for(int i = 1; i <= n; i ++ )
for(int i = h[x]; ~i; i = ne[i]){
int j = e[i]
if(id[i] != id[j]){
add(id[i], id[j]);
}
}
- 缩点后的DAG图,加最少的边使得图变为强连通分量。
a d d = m a x ( P , Q ) p ∈ 起 点 , Q ∈ 终 点 add = max(P,Q) {p\in 起点, Q\in 终点} add=max(P,Q)p∈起点,Q∈终点
(题目不保证图连通的话,入度和出度要分开算)
证明,不相交起点和终点路径之间连边
拓扑序
连通分量编号 递减的顺序就是拓扑序了。
证明:算导论的dfs拓扑排序做法。
板子
void tarjan(int x){
dfn[x] = low[x] = ++dfs_clock;
in_stk[x] = true; stk[++top] = x;
for(int i = h[x]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[x], low[j]);
}else if(in_stk[j]) low[x] = min(low[x], dfn[j]);
}
if(low[x] == dfn[x]){
int y;
scc_cnt++;
do{
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
siz[scc_cnt]++;
}while(y != x);
}
}
作者:HenriHGS
链接:https://www.acwing.com/activity/content/code/content/1737726/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Kosaraju算法
逆图学习!!!
一、算法简介
在计算科学中, K o s a r a j u Kosaraju Kosaraju的算法(又称为– S h a r i r K o s a r a j u Sharir Kosaraju SharirKosaraju算法)是一个线性时间( l i n e a r t i m e linear time lineartime)算法找到的有向图的强连通分量。它利用了一个事实,逆图(与各边方向相同的图形反转, t r a n s p o s e g r a p h transpose graph transposegraph)有相同的强连通分量的原始图。
有关强连通分量的介绍在之前Tarjan 算法中:Tarjan Algorithm
逆图( T r a n p o s e G r a p h Tranpose Graph TranposeGraph)
我们对逆图定义如下:
G
T
=
(
V
,
E
T
)
,
E
T
=
{
(
u
,
v
)
:
(
v
,
u
)
∈
E
}
G^T=(V,E^T),E^T=\{(u,v):(v,u)\in E\}
GT=(V,ET),ET={(u,v):(v,u)∈E}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gLY4tvxp-1633622499654)(D:\aaaa文件夹\工作\文档\模板\图论,似神仙\图床\QQ截图20210906192232.png)]
上图是有向图 G G G , 和图 G G G的逆图 G T G^T GT
K o s a r a j u Kosaraju Kosaraju 算法就是分别对原图 G G G 和它的逆图 G T G^T GT 进行两遍 D F S DFS DFS,即:
1).对原图 G G G进行深度优先搜索,找出每个节点的完成时间(时间戳)
2).选择完成时间较大的节点开始,对逆图 G T G^T GT 搜索,能够到达的点构成一个强连通分量
3).如果所有节点未被遍历,重复2). ,否则算法结束;
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define x first
#define y second
#define For(i,x,y) for(int i = (x); i <= (y); i ++ )
#define fori(i,x,y) for(int i = (x); i < (y); i ++ )
using namespace std;
const int N = 1e4+10, M = 1e5+10;
int e1[M], e2[M], ne1[M], ne2[M], h1[N], h2[N], idx1, idx2;
void add1(int a, int b){
e1[idx1] = b, ne1[idx1] = h1[a], h1[a] = idx1++;
}
void add2(int a, int b){
e2[idx2] = b, ne2[idx2] = h2[a], h2[a] = idx2++;
}
int dfn[N], finish[N], dfs_clock;
//typedef pair<int,int>PII;
//PII p[N];
int ord[N<<1];
void dfs1(int x){
if(dfn[x]) return;
dfn[x] = ++dfs_clock;
for(int i = h1[x]; ~i; i = ne1[i]){
int j = e1[i];
dfs1(j);
}
finish[x] = ++dfs_clock;//点的完成时间要不同
ord[finish[x]] = x;
}
int scc_cnt = 0;
bool st[N];
int id[N], tot;
void dfs2(int x){
if(st[x]) return ;
tot++;
st[x] = true;
id[x] = scc_cnt;
for(int i = h2[x]; ~i; i = ne2[i]){
int j = e2[i];
dfs2(j);
}
}
int dout[N], siz[N];
int main(){
memset(h1,-1,sizeof h2);
memset(h2,-1,sizeof h2);
idx1 = idx2 = 0;
int n, m;
scanf("%d %d", &n, &m);
For(i,1,m){
int a,b;
scanf("%d %d", &a, &b);
add1(a,b); add2(b,a);
}
For(i,1,n)dfs1(i);
// For(i,1,n)p[i] = {finish[i],i};
//sort(p+1,p+1+n);
memset(st,0,sizeof st);
for(int i = n*2; i ; i -- ){
if(ord[i] && !st[ord[i]]) tot = 0,scc_cnt++,dfs2(ord[i]), siz[id[ord[i]]] = tot;
}
//题目部分
For(x,1,n){
for(int i = h1[x]; ~i; i = ne1[i]){
int j = e1[i];
if(id[x] == id[j]) continue;
dout[id[x]]++;
}
}
// printf("id:");
// For(i,1,n) printf("%d ", id[i]);
// puts("");
int sum = 0, zero = 0;
For(i,1,scc_cnt) {
if(dout[i]) continue;
zero++;
sum += siz[i];
if(zero > 1) sum = 0;
}
printf("%d\n", sum);
return 0;
}
再入无向图的双连通分量(tarjan神犇%%%%%%%%)
一、分类
- 双连通分量
- 重连通分量
二、双连通分量(tarjan算法)
- 点连通分量和边连通分量之间没有必然联系
- 割点和桥之间没有必然联系
1. 边双连通分量 e-DCC
-
桥:一条边,删掉这条边后,图变的不连通。 C r i t i c a l l i n k Critical \ link Critical link
-
定义:极大的不包含桥的连通块,称为边双连通分量。
-
性质:
性质1:在一个边双连通分量里,不管删掉哪条边,都是连通的。
性质2:存在两条没有公共边的路径。
- 对于一棵树 它的所有边双连通分量都是结点。,被桥所分割。
2. 点双连通分量 v-DCC
- 割点:在连通的无向图中,如果把这个点删除,图变的不连通。 C r i t i c a l n o d e Critical \ node Critical node
每个割点至少属于两个双连通分量。
如果任意两点至少存在两条“点不重复”的路径,则是点双连通的。
等价于
任意两条边都在一个简单环中,即内部无割顶。
- 定义:极大的不包含割点的连通块,称为点双连通分量。(
块
)
不难发现,每条边恰好属于一个点双连通分量,但不同点双连通分量可能会会有公共点,可以证明不同双连通分量最多只有一个公共点,且它一定是割顶。 另一方面,任意割顶至少是两个不同 点双连通分量的公共点
它的所有点双连通分量都是边。
一个点双连通分量(这是特殊情况)
三、边双连通分量 e-DCC(类比有向图)
-
时间戳: d f n ( x ) dfn(x) dfn(x),即dfs序,
-
l o w ( x ) low(x) low(x): x的子树,往下走的话,最早能到达的点(即dfs序较小的)。
-
问题1:dfs时如何判断是否是桥?
看y是否会指回,祖先结点。
等价于: d f n ( x ) < l o w ( y ) dfn(x) < low(y) dfn(x)<low(y)
(图6)
- 问题2:如何找所有的边双连通分量?
法1:把所有桥都删掉。
法2:stack,(利用 d f n ( x ) = = l o w ( x ) dfn(x) == low(x) dfn(x)==low(x), 那么 x x x 连向父亲的边是桥)
acwing eg1: 395. 冗余路径
思路:
先求边双,之后缩点。
a
n
s
=
⌈
c
n
t
2
⌉
=
c
n
t
+
1
2
ans = \lceil \frac{cnt}{2} \rceil =\frac{cnt+1}{2}
ans=⌈2cnt⌉=2cnt+1
图片(7)
# include <iostream>
# include <cstring>
# include <cstdio>
# define mst(x,a) memset(x,a,sizeof(x))
using namespace std;
const int N = 5e3+10, M = 1e4+10;
int n, m;
int dfn[N], low[N], dfs_clock, id[N], du[N];
int stk[N], top;
int e[M], ne[M], h[M], idx;
bool is_bridge[M];
int dcc_cnt;
void init(){
mst(h,-1); mst(dfn,0);
mst(id,0); mst(du,0);
top = dcc_cnt = dfs_clock = idx = 0;
}
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int x, int from){
dfn[x] = low[x] = ++dfs_clock;
stk[++top] = x;
for(int i = h[x]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
dfs(j,i);
low[x] = min(low[x],low[j]);
if(dfn[x] < low[j])
is_bridge[i] = is_bridge[i^1] = true;
}else if(i != (from^1)) low[x] = min(low[x],dfn[j]);
//^的优先级较低,所以要加括号
}
if(dfn[x] == low[x]){
++dcc_cnt;
int y;
do{
y = stk[top--];
id[y] = dcc_cnt;//缩点
}while(y != x);
}
}
void sol(){
init();
scanf("%d%d", &n, &m);
int a, b;
while(m--){
scanf("%d%d", &a, &b);
add(a,b);
add(b,a);
}
///这道题保证连通,所以不用每个点都求。
/*
for(int i = 1; i <= n; i ++ ){
if(!dfn[i]) dfs(i,-1);
}
*/
dfs(1,-1);
for(int i = 0; i < idx; i ++){
if(is_bridge[i]) du[id[e[i]]]++;
}
int cnt = 0;
for(int i = 1; i <= dcc_cnt; i ++ )if(du[i] == 1) cnt++;
cnt = (cnt+1)/2;
printf("%d\n", cnt);
}
int main(){
sol();
return 0;
}
tips:
类比有向图。
a
n
s
=
max
(
p
,
q
)
ans = \max(p,q)
ans=max(p,q)
(p:出度为0的点,q:入度为0的点)
四、点双连通分量 v-DCC
- 问题1:如何求割点?
l o w ( y ) ≥ d f n ( x ) low(y) \geq dfn(x) low(y)≥dfn(x)
- 如果x不是根结点(root),x是割点
- x是根结点,至少有两个子结点。(看child)
- 问题2:如何求点双连通分量。
i f : d f n ( x ) ≤ l o w ( y ) if : dfn(x) \leq low(y) if:dfn(x)≤low(y)
c n t + + ; cnt++; cnt++;
i f : x ! = r o o t ∣ ∣ c n t > 1 if: x != root || cnt > 1 if:x!=root∣∣cnt>1
- 将栈中元素弹出,直至弹出y为止。
- 且x也属于该点双连通分量。(=情况,看下图8。因为前面的<情况已经被弹出了)
- 孤立点,也是双连通分量。
- acwing 1183. 电力
题意: 删除一个点后,连通块有几个。
思路:类似求割点
# include <iostream>
# include <cstdio>
# include <cstring>
# include <algorithm>
# define For(i,x,y) for(int i = (x); i <= (y); i ++ )
# define fori(i,x,y) for(int i = (x); i < (y); i ++ )
# define mst(x,a) memset(x,a,sizeof(x))
using namespace std;
const int N = 1e4+10, M = 3e4+10;
int ne[M], h[N], e[M], idx;
int low[N], dfn[N], dfs_clock;
int n, m;
int ans, root;
void dfs(int u){
dfn[u] = low[u] = ++dfs_clock;
int cnt = 0;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
dfs(j);
low[u] = min(low[u], low[j]);
if(low[j] >= dfn[u]){
cnt++;
}
}else low[u] = min(low[u],dfn[j]);
}
if(root != u && cnt) cnt++;
ans = max(ans,cnt);
}
void init(){
mst(h, -1 ); mst(dfn,0);
idx = dfs_clock = ans = 0;
}
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void sol(){
init();
while(m--){
int a, b;
scanf("%d%d", &a,&b);
add(a,b);
add(b,a);
}
int cnt = 0;
fori(i,0,n){
if(!dfn[i]){
root = i;
dfs(i);
cnt++;
}
}
printf("%d\n", ans + cnt - 1);
}
int main(){
while(scanf("%d%d", &n, &m) && (n||m))sol();
return 0;
}
-
矿场搭建:
题意:给定一个无向图,问最少在几个点设置出口,可以使得不管哪个点坍塌,其余所有点都可以与这个出口连通。思路:
- 为了保证出口本身不坍塌,所以出口数量 >= 2.
- 分别看每个连通块。
- 无割点(度数为0)
C c n t 2 C_{cnt}^{2} Ccnt2
2)有割点(缩点,注意这里的缩点和前面几种连通分量都不同,2倍原来的点)
1>每个割点单独作为一个点。
2>从每个v-DCC向其他包含的割点连边(如图9)
3>V-DCC,度数为1,需要在该分量内部放出口,并且非割点,放一个即可。
c n t − 1 cnt - 1 cnt−1
(图10)
4> V-DCC, 度数大于1,无需设置出口
# include <iostream>
# include <cstring>
# include <cstdio>
# include <stack>
# include <vector>
# define For(i,x,y) for(int i = (x); i <= (y); i ++ )
# define mst(x,a) memset(x,a,sizeof(x))
# define pb push_back
using namespace std;
typedef unsigned long long ULL;
const int N = 1010;
const int M = 1010;
int e[M], ne[M], idx, h[N];
vector<int> dcc[N<<1];
int dcc_cnt, stk[N], top, root;
int dfn[N], low[N], dfs_clock;
bool is_cut[N];
int n, m;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void init(){
mst(h,-1); mst(is_cut, 0); mst(dfn,0);
For(i,1,dcc_cnt) dcc[i].clear();
n = dcc_cnt = idx = top = dfs_clock = 0;
while(m--){
int a, b;
scanf("%d%d", &a, &b);
add(a,b);
add(b,a);
n = max(a,n);
n = max(n,b);
}
}
void dfs(int u){
dfn[u] = low[u] = ++ dfs_clock;
stk[++top] = u;
if(root == u && h[u] == -1){
dcc_cnt ++ ;
dcc[dcc_cnt].pb(stk[top--]);
return ;
}
int child = 0;
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
dfs(j);
low[u] = min(low[u], low[j]);
if(dfn[u] <= low[j]){
child ++;
if(root != u || child > 1) is_cut[u] = true;
int y;
dcc_cnt++;
do{
y = stk[top--];
dcc[dcc_cnt].pb(y);
}while(y != j);
dcc[dcc_cnt].pb(u);
}
}else low[u] = min(low[u],dfn[j]);
}
}
void sol(){
init();
for(root = 1; root <= n; root ++ )
if(!dfn[root])
dfs(root);
ULL ans2 = 1;
int ans1 = 0;
For(i,1,dcc_cnt){
vector<int>& v = dcc[i];
int cnt = 0 ;
for(int j = 0; j < v.size(); j ++ )
if(is_cut[v[j]]) cnt++;
if(cnt == 0){
if(v.size() > 1) ans1 += 2, ans2 *= v.size()*(v.size()-1)/2;
else ans1++;
}else if(cnt == 1) ans1++, ans2 *= v.size()-1;
}
printf("%d %llu\n", ans1, ans2);
}
int main(){
int cas = 0;
while(scanf("%d", &m) && m){
printf("Case %d: ", ++cas);
sol();
}
return 0;
}