目录
1 强联通分量缩点(有向图)
在有向图中,可以利用 T a r j a n Tarjan Tarjan对强联通分量缩点,使其变成一个有向无环图(此时处于同一缩点的点具有相同的性质)。
算法中 d f n [ i ] dfn[i] dfn[i]表示第 i i i个节点的时间戳, l o w [ i ] low[i] low[i]表示第 i i i个点能到达的点的最小时间戳, b e l o n g [ i ] belong[i] belong[i]表示第 i i i个点属于哪个缩点, v i s i t [ i ] visit[i] visit[i]表示第 i i i个点是否在栈里。
算法流程:
- 如果某个点 u u u没有时间戳(相当于未被访问),则从这个点开始 d f s dfs dfs。
- 如果点 u u u的后继节点 v v v没有时间戳,那么就访问 v v v,并且令 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])。
- 否则,如果点 u u u的后继节点 v v v在栈内,那么令 l o w [ u ] = m i n ( l o w [ u ] , d f n [ v ] ) low[u]=min(low[u],dfn[v]) low[u]=min(low[u],dfn[v])。
- 最后如果 l o w [ u ] = = d f n [ u ] low[u]==dfn[u] low[u]==dfn[u],那么说明从栈顶到 u u u组成一个连通分量。
1.1 Proving Equivalences
1.1.1 题意
求一个图至少添加多少条边就能变成一个强联通图。
链接:link。
1.1.2 思路
命题:令有向无环图出度为
0
0
0的点有
o
u
t
s
outs
outs个,入度为
0
0
0的点有
i
n
s
ins
ins个,那么至少需要添加
m
a
x
(
o
u
t
s
,
i
n
s
)
max(outs,ins)
max(outs,ins)条边,使得图强联通。
证明:略。
1.1.2.1 时间复杂度分析
每个节点只需要访问一次,所以节点复杂度为 O ( n ) \mathcal{O}(n) O(n)。
1.1.2.2 实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<stack>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m,dfn[N],low[N],belong[N],in[N],out[N],deep,reduce;
bool visit[N];
vector<int> component[N],g[N];
stack<int> stk;
void init(){
deep=reduce=0;
while(!stk.empty()) stk.pop();
for(int i=1;i<=n;i++) component[i].clear(),g[i].clear();
memset(visit,false, sizeof(visit));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
}
void DFS(int u){
stk.push(u);
visit[u]=true;
dfn[u]=low[u]=++deep;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(!dfn[v]) DFS(v),low[u]=min(low[u],low[v]);
else if(visit[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
int tmp;reduce++;
do{
tmp=stk.top();stk.pop();
visit[tmp]=false;
component[reduce].push_back(tmp);
belong[tmp]=reduce;
}while(tmp!=u);
}
}
void Tarjan(){
for(int i=1;i<=n;++i){
if(!dfn[i]){
DFS(i);
}
}
}
int Find(){
if(reduce==1) return 0;
for(int i=1;i<=n;i++){
for(int j=0;j<g[i].size();j++){
int v=g[i][j];
if(belong[i]!=belong[v]){
out[belong[i]]++;
in[belong[v]]++;
}
}
}
int ins=0,outs=0;
for(int i=1;i<=reduce;++i){
if(in[i]==0) ins++;
if(out[i]==0) outs++;
}
return max(ins,outs);
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d %d",&n,&m);
init();
for(int i=1;i<=m;++i){
int x,y;scanf("%d %d",&x,&y);
g[x].push_back(y);
}
Tarjan();
printf("%d\n",Find());
}
return 0;
}
1.2 Popular Cows
1.2.1 题意
一个有向图中,有哪些点能被其他所有点可达。
链接:link。
1.2.2 思路
命题:在有向无环图中,如果出度为
0
0
0的点只有一个,那么这个点能被其他点可达。
证明:略。
1.2.2.1 时间复杂度分析
每个节点只需要访问一次,所以节点复杂度为 O ( n ) \mathcal{O}(n) O(n)。
1.2.2.2 实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<stack>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m,dfn[N],low[N],belong[N],in[N],out[N],deep,reduce;
bool visit[N];
vector<int> component[N],g[N];
stack<int> stk;
void init(){
deep=reduce=0;
while(!stk.empty()) stk.pop();
for(int i=1;i<=n;i++) component[i].clear(),g[i].clear();
memset(visit,false, sizeof(visit));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
}
void DFS(int u){
stk.push(u);
visit[u]=true;
dfn[u]=low[u]=++deep;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(!dfn[v]) DFS(v),low[u]=min(low[u],low[v]);
else if(visit[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
int tmp;reduce++;
do{
tmp=stk.top();stk.pop();
visit[tmp]=false;
component[reduce].push_back(tmp);
belong[tmp]=reduce;
}while(tmp!=u);
}
}
void Tarjan(){
for(int i=1;i<=n;++i){
if(!dfn[i]){
DFS(i);
}
}
}
int Find(){
for(int i=1;i<=n;i++){
for(int j=0;j<g[i].size();j++){
int v=g[i][j];
if(belong[i]!=belong[v]){
out[belong[i]]++;
in[belong[v]]++;
}
}
}
int sum=0,ind;
for(int i=1;i<=reduce;++i){
if(out[i]==0) sum++,ind=i;
}
if(sum==1) return component[ind].size();
else return 0;
}
int main(){
scanf("%d %d",&n,&m);
init();
for(int i=1;i<=m;++i){
int x,y;scanf("%d %d",&x,&y);
g[x].push_back(y);
}
Tarjan();
printf("%d\n",Find());
return 0;
}
2 边双联通分量缩点(无向图)
在无向图中,可以利用 T a r j a n Tarjan Tarjan对边双联通分量缩点,使其变成一颗树(此时处于同一缩点的点具有相同的性质)。其中,边双联通是指,任意两个点之间至少有两条边不重复的路径。相对地,有点双联通图,指任意两个点之间至少有两条点不重复的路径。
算法流程:
- 如果某个点 u u u没有时间戳(相当于未被访问),则从这个点开始 d f s dfs dfs。
- 对于每个节点 u u u不处理其父节点(紧前访问的节点)。
- 如果点 u u u的后继节点 v v v没有时间戳,那么就访问 v v v,并且令 l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u]=min(low[u],low[v]) low[u]=min(low[u],low[v])。
- 否则,如果点 u u u的后继节点 v v v在栈内,那么令 l o w [ u ] = m i n ( l o w [ u ] , d f n [ v ] ) low[u]=min(low[u],dfn[v]) low[u]=min(low[u],dfn[v])。
- 最后如果 l o w [ u ] = = d f n [ u ] low[u]==dfn[u] low[u]==dfn[u],那么说明从栈顶到 u u u组成一个连通分量。
其实边双连通分量的算法与强联通分量比起来,只是多了第 2 2 2步而已。
2.1 Road Construction
2.1.1 题意
给定一个无向图,求最少添加的边数,使得即使删除一条边,依然连通。
链接:link。
2.1.2 思路
命题:对于一颗数,入度为
1
1
1的点有
i
n
s
ins
ins个,则至少添加
⌈
i
n
s
2
⌉
\lceil \frac{ins}{2} \rceil
⌈2ins⌉条边使得图边双联通。
证明:略。
2.1.2.1 时间复杂度分析
每个节点只需要访问一次,所以节点复杂度为 O ( n ) \mathcal{O}(n) O(n)。
2.1.2.2 实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<stack>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m,dfn[N],low[N],belong[N],in[N],out[N],deep,reduce;
bool visit[N];
vector<int> component[N],g[N];
stack<int> stk;
void init(){
deep=reduce=0;
while(!stk.empty()) stk.pop();
for(int i=1;i<=n;i++) component[i].clear(),g[i].clear();
memset(visit,false, sizeof(visit));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
}
void DFS(int u,int fa){
stk.push(u);
visit[u]=true;
dfn[u]=low[u]=++deep;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa) continue;
if(!dfn[v]) DFS(v,u),low[u]=min(low[u],low[v]);
else if(visit[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
int tmp;reduce++;
do{
tmp=stk.top();
visit[tmp]=false;
component[reduce].push_back(tmp);
belong[tmp]=reduce;
stk.pop();
}while(tmp!=u);
}
}
void Tarjan(){
for(int i=1;i<=n;++i){
if(!dfn[i]){
DFS(i,0);
}
}
}
int Find(){
for(int i=1;i<=n;i++){
for(int j=0;j<g[i].size();j++){
int v=g[i][j];
if(belong[i]!=belong[v]){
out[belong[i]]++;
in[belong[v]]++;
}
}
}
int res=0;
for(int i=1;i<=reduce;++i){
if(in[i]==1) res++;
}
return (res+1)/2;
}
int main(){
while(~scanf("%d %d",&n,&m)){
init();
for(int i=1;i<=m;++i){
int x,y;scanf("%d %d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
Tarjan();
printf("%d\n",Find());
}
return 0;
}
2.2 割点
2.1 题意
找出一个无向图的割点。
链接:link。
2.2 思路
命题:如果点
u
u
u有前驱节点,那么只要存在
d
f
n
[
u
]
<
=
l
o
w
[
v
]
dfn[u]<=low[v]
dfn[u]<=low[v](
v
v
v为
u
u
u的后继节点),则
u
u
u为割点;如果点
u
u
u没有有前驱节点,那么只要存在
d
f
n
[
u
]
<
=
l
o
w
[
v
]
dfn[u]<=low[v]
dfn[u]<=low[v],并且
u
u
u的后继节点的个数大于1,则
u
u
u为割点。
证明:略。
2.2.1 时间复杂度分析
每个节点只需要访问一次,所以节点复杂度为 O ( n ) \mathcal{O}(n) O(n)。
2.2.2 实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<stack>
#include<algorithm>
using namespace std;
const int N=1e5+5;
int n,m,dfn[N],low[N],belong[N],in[N],out[N],iscut[N],deep,reduce;
bool visit[N];
vector<int> component[N],g[N];
stack<int> stk;
void init(){
deep=reduce=0;
while(!stk.empty()) stk.pop();
for(int i=1;i<=n;i++) component[i].clear(),g[i].clear(),iscut[i]=0;
memset(visit,false, sizeof(visit));
memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
}
int DFS(int u,int fa){
int child=0;
stk.push(u);
visit[u]=true;
dfn[u]=low[u]=++deep;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa) continue;
if(!dfn[v]){
DFS(v,u);
child++;
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]) iscut[u]=1;
}
else if(visit[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
int tmp;reduce++;
do{
tmp=stk.top();
visit[tmp]=false;
component[reduce].push_back(tmp);
belong[tmp]=reduce;
stk.pop();
}while(tmp!=u);
}
return child;
}
void Tarjan(){
for(int i=1;i<=n;++i){
if(!dfn[i]&&DFS(i,0)==1){
iscut[i]=0;
}
}
}
int main(){
scanf("%d %d",&n,&m);
init();
for(int i=1;i<=m;++i){
int x,y;scanf("%d %d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
Tarjan();
int cnt=0;
for(int i=1;i<=n;++i){
if(iscut[i]) cnt++;
}
printf("%d\n",cnt++);
for(int i=1;i<=n;++i){
if(iscut[i]) printf("%d ",i);
}
printf("\n");
return 0;
}