Tarjan——圖的連通性
前置:圖論
只需要一眼看透這玩意是個圖即可
作為一個新專題,它與前面的LCA,最短路和最小生成樹沒有非常大的聯絡
可能最大的聯絡就是LCA的離線演算法叫LCA的Tarjan演算法吧
不過由於Tarjan演算法是各種聯通分量的縮點手法
因此Tarjan可以和最短路演算法,拓撲排序,樹型DP(DAG上DP)混用
不得不說樹形DP真的是噩夢啊
概念:時間戳,搜尋樹,追溯值
1.時間戳
在無向圖/流圖的DFS遍歷中,按每個節點第一次被訪問的時間順序,依次給n個節點打上1~n的標記,該標記稱為時間戳
2.搜尋樹
在無向連通圖上任選一個節點(流圖的源點)進行DFS遍歷,每個節點僅訪問一次,所有發生遞迴的邊(即從節點x以(x,y)這條邊訪問到節點y是對y的第一次訪問)構成的樹叫無向聯通圖(流圖)的搜尋樹,不同無向聯通塊的搜尋樹構成整個圖的搜尋森林
3.追溯值:
設subtree(x)表示搜尋樹中以x為根的子樹
追溯值low(x)定義為以下節點的時間戳的最小值:
1.subtree(x)中的節點
2.通過一條不在搜尋樹上的邊可到達subtree(x)的節點
概念:割,連通
1.割點,割邊
對於圖G的節點x,若從圖中刪去x及所有與x關聯的邊後,G分裂為2個或兩個以上不相連的子圖,則x為G的割點
對於圖G的邊e,若從圖中刪去e後,G分為兩個不相連的子圖,則稱e為G的割邊
2.點雙,邊雙,強聯通
若一張無向聯通圖不存在割點,則稱它為點雙連通圖
若一張無向聯通圖不存在割邊,則稱它為邊雙連通圖
無向圖的極大點雙聯通子圖被稱為點雙連通分量,簡記為v-DCC
無向圖的極大邊雙連通子圖被稱為邊雙連通分量,簡記為e-DCC
給定一張有向圖,若對於圖中任意兩個節點x,y,滿足既存在x到達y的路徑,也存在y到達x的路徑,則稱該圖為強聯通圖
有向圖的極大強聯通子圖被稱為強聯通分量
演算法型別
求割點,割邊
割點
void tarjan(int x){
d[x]=l[x]=++sum;
int f=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
if(x!=r||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
}
割邊
void tarjan(int x,int r){ d[x]=l[x]=++num; for(int i=h[x];i;i=p[i].n){ int z=p[i].t; if(!d[z]){ tarjan(z,x); l[x]=min(l[x],l[z]); if(l[z]>d[x]){ b[i]=b[i^1]=1; } } else if(z!=r){ l[x]=min(l[x],d[z]); } } }
求聯通分量
點雙
void tarjan(int x,int f){
d[x]=l[x]=++dp;
v[x]=1;
s[top++]=x;
for(int i=h[x];i!=-1;i=p[i].n){
int y=p[i].t;
if(y==f){
continue;
}
if(!d[y]){
tarjan(y,x);
l[x]=min(l[x],l[y]);
if(l[y]>d[x]){
p[i].c=1;
p[i^1].c=1;
}
}
else if(v[y]){
l[x]=min(l[x],d[y]);
}
}
if(d[x]==l[x]){
sum++;
int y;
do{
y=s[--top];
v[y]=0;
c[y]=sum;
}while(y!=x);
}
}
邊雙
void tarjan(int u,int fa){
dfn[u]=low[u]=++inr;
stack[++top]=u;
vis[u]=1;
int x=0;
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa){
continue;
}
if(!dfn[v]){
++x;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cut[u]=1;
}
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(fa==-1&&x<=1){
cut[u]=0;
}
}
強聯通
void tarjan(int x){
dfn[x]=++dex;
low[x]=dex;
vis[x]=1;
stack[++top]=x;
for(int i=st[x];i!=0;i=e[i].n){
int y=e[i].t;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(vis[y]){
low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
vis[x]=0;
color[x]=++num;
while(stack[top]!=x){
color[stack[top]]=num;
vis[stack[top--]]=0;
}
top--;
}
}
縮點
就是重新建圖的事,新建鄰接表跑前向星即可
程式碼略了,前向星粘過來沒啥意思
例題
備用交換機
一眼看到底的割點裸題
Code
備用交換機
#include <bits/stdc++.h>
using namespace std;
const int o=5e5+10;
int h[o],d[o],l[o],s[o];
struct path{
int t;
int n;
}p[o];
int cnt,n,m,r,sum;
bool c[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x){
d[x]=l[x]=++sum;
int f=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
if(x!=r||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
}
void in(){
n=read();
int x,y;
while(scanf("%d%d",&x,&y)!=EOF){
if(x==y){
continue;
}
add(x,y);
add(y,x);
}
}
void work(){
for(int i=1;i<=n;i++){
if(!d[i]){
r=i;
tarjan(r);
}
}
}
void out(){
for(int i=1;i<=n;i++){
if(c[i]){
m++;
}
}
cout<<m<<endl;
for(int i=1;i<=n;i++){
if(c[i]){
cout<<i<<endl;
}
}
}
int main(){
in();
work();
out();
return 0;
}
旅遊航道
一眼割邊+計數,注意計數的位置
Code
旅遊航道
#include <bits/stdc++.h>
using namespace std;
const int o=1e5+10;
int h[o],d[o],l[o];
int n,m,cnt,num,ans;
struct node {
int t;
int n;
}p[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+(ch&15);
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x,int r){
d[x]=l[x]=++num;
for(int i=h[x];i;i=p[i].n){
int z=p[i].t;
if(!d[z]){
tarjan(z,x);
l[x]=min(l[x],l[z]);
if(l[z]>d[x]){
ans++;
}
}
else if(z!=r){
l[x]=min(l[x],d[z]);
}
}
}
void in(){
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
add(x,y);
add(y,x);
}
num=0;
}
void work(){
tarjan(1,1);
}
void out(){
cout<<ans<<endl;
}
void back(){
memset(p,0,sizeof(p));
memset(h,0,sizeof(h));
memset(d,0,sizeof(d));
memset(l,0,sizeof(l));
n=m=cnt=num=ans=0;
}
int main(){
while(scanf("%d%d",&n,&m)!=EOF){
if(!n&&!m){
return 0;
}
in();
work();
out();
back();
}
return 0;
}
BLO
由於是刪點,要考慮刪點對於圖的影響
題面中指示不聯通的點對,所以要考慮刪點對於連通性的影響
而連通性和點結合起來自然是一個割點問題
因此考慮是不是割點對於答案的影響
1.不是割點,刪完邊以後只剩下單點不與整個圖上其它點聯通
直接加法原理(n-1),考慮有序每個點乘2,加法原理就直接解出2*(n-1)
2.是割點
此時圖被劃分為若干聯通塊,並且每個聯通塊彼此不連通,個數設為k
運用加法原理和乘法原理即可
加法原理k選2
乘法原理從2個裡面任意挑點即可
Code
BLO
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int o=5e5+10;
struct node{
int t;
int n;
}p[o*2];
int h[o],d[o],l[o],s[o];
ll ans[o];
bool c[o];
int cnt,n,m,num;
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void add(int s,int t){
cnt++;
p[cnt].t=t;
p[cnt].n=h[s];
h[s]=cnt;
}
void tarjan(int x){
d[x]=l[x]=++num;
s[x]=1;
int f=0,sum=0;
for(int i=h[x];i;i=p[i].n){
int y=p[i].t;
if(!d[y]){
tarjan(y);
s[x]+=s[y];
l[x]=min(l[x],l[y]);
if(l[y]>=d[x]){
f++;
ans[x]+=(ll)s[y]*(n-s[y]);
sum+=s[y];
if(x!=1||f>1){
c[x]=1;
}
}
}
else{
l[x]=min(l[x],d[y]);
}
}
if(c[x]){
ans[x]+=(ll)(n-sum-1)*(sum+1)+(n-1);
}
else{
ans[x]=2*(n-1);
}
}
void in(){
n=read();
m=read();
cnt=1;
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
if(x==y){
continue;
}
add(x,y);
add(y,x);
}
}
void out(){
for(int i=1;i<=n;i++){
printf("%lld\n",ans[i]);
}
}
int main(){
in();
tarjan(1);
out();
return 0;
}
礦場搭建
其實這題還是一個割點
塌掉某個點實際上等價於刪點,然後找出聯通塊討論聯通塊中有幾個割點即可
注意只刪掉一個點
1.聯通塊中無割點:
如果一個安全出口正好塌了,就需要修另一個
所以這時修兩個
2.聯通塊有一個割點:
防塌割點修一個即可
3.聯通塊有兩個以上:
塌一個割點沒事可以跑路直接溜球
一個都不用
第二問就是個計數問題:
乘法原理即可
礦場搭建
#include <cctype>
#include <cstdio>
#include <cstring>
using namespace std;
#define il inline
#define rg register
#define ll long long
const int o=1e5+10;
int m,n,top,cnt,inr,opt,id;
int dfn[o],low[o],stack[o],cut[o],belong[o],siz[o],c[o];
bool vis[o];
ll ans;
struct node{
int to;
int next;
node(){}
node (int to,int next):to(to),next(next){};
}e[o];
int head[o],tot;
il void read(int &x){
int f=1;
rg char ch=getchar();
for(x=0;!isdigit(ch);ch=='-'&&(f=-1),ch=getchar());
for(;isdigit(ch);x=x*10+ch-48,ch=getchar());
x=x*f;
}
il void clear(){
tot=0;
top=0;
inr=0;
id=0;
cnt=0;
n=0;
ans=1;
memset(c,0,sizeof(c));
memset(dfn,0,sizeof(dfn));
memset(vis,0,sizeof(vis));
memset(low,0,sizeof(low));
memset(cut,0,sizeof(cut));
memset(siz,0,sizeof(siz));
memset(head,-1,sizeof(head));
memset(belong,0,sizeof(belong));
}
int min(int a,int b){
return a<b?a:b;
}
void add(int x,int y){
e[++tot]=node(y,head[x]);
head[x]=tot;
e[++tot]=node(x,head[y]);
head[y]=tot;
}
void tarjan(int u,int fa){
dfn[u]=low[u]=++inr;
stack[++top]=u;
vis[u]=1;
int x=0;
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(v==fa){
continue;
}
if(!dfn[v]){
++x;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(dfn[u]<=low[v]){
cut[u]=1;
}
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(fa==-1&&x<=1){
cut[u]=0;
}
}
void dfs(int u){
vis[u]=1;
++siz[id];
for(int i=head[u];i!=-1;i=e[i].next){
int v=e[i].to;
if(vis[v]){
continue;
}
if(!cut[v]){
dfs(v);
}
else if(belong[v]!=id){
belong[v]=id;
++c[id];
}
}
}
int main(){
while(1){
read(m);
if(!m){
break;
}
clear();
for(int x,y;m--;){
read(x);
read(y);
add(x,y);
if(x>n){
n=x;
}
if(y>n){
n=y;
}
}
for(int i=1;i<=n;++i){
if(!dfn[i]){
tarjan(i,-1);
}
}
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;++i){
if(!cut[i]&&!vis[i]){
++id;
dfs(i);
}
}
if(id==1){
cnt=2;
ans=(ll)n*(n-1)/2;
}
else{
for(int i=1;i<=id;++i){
if(c[i]==1){
++cnt;
ans=(ll)ans*siz[i];
}
}
}
printf("Case %d: %d %lld\n",++opt,cnt,ans);
}
return 0;
}
受歡迎的牛
如果說受歡迎可傳遞的話,那一個強連通分量中的所有節點都受到指向這之中任何一個節點的邊的起點的歡迎。
於是考慮縮點,縮點完成重新建邊
顯然如果出現“南北對峙”或者“三足鼎立”或者“五胡十六國”等局面是不可能出現公認受歡迎的牛的
因此只有出度為0的點只有一個的情況下才有可能出現
此時由於已經縮點,對應的個數就是強連通分量的節點個數
Code
受歡迎的牛
#include <bits/stdc++.h>
using namespace std;
const int o=1e6;
int d[o],l[o],v[o],s[o],c[o],t[o],f[o];
int n,m,top,sum,dp,tmp,ans;
vector <int> g[o];
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void tarjan(int x){
d[x]=l[x]=++dp;
v[x]=1;
s[++top]=x;
int a=g[x].size();
for(int i=0;i<a;i++){
int y=g[x][i];
if(!d[y]){
tarjan(y);
l[x]=min(l[x],l[y]);
}
else{
if(v[y]){
l[x]=min(l[x],l[y]);
}
}
}
if(d[x]==l[x]){
c[x]=++sum;
v[x]=0;
while(s[top]!=x){
c[s[top]]=sum;
v[s[top--]]=0;
}
top--;
}
}
void in(){
n=read();
m=read();
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
g[x].push_back(y);
}
}
void work(){
for(int i=1;i<=n;i++){
if(!d[i]){
tarjan(i);
}
}
for(int i=1;i<=n;i++){
int a=g[i].size();
for(int j=0;j<a;j++){
int b=g[i][j];
if(c[b]!=c[i]){
t[c[i]]++;
}
}
f[c[i]]++;
}
for(int i=1;i<=sum;i++){
if(t[i]==0){
tmp++;
ans=f[i];
}
}
}
void out(){
if(tmp==1){
printf("%d",ans);
}
else{
printf("0");
}
}
int main(){
in();
work();
out();
return 0;
}
分離的路徑
首先如果走點雙肯定是不會有重複路徑的
如果說走的邊一定重複那肯定是個割邊
但是本題並不是個求割邊的題,而是要說加邊
那麼對於不重要的環就直接縮掉就行了
縮完以後整張圖只剩下一棵樹了,所有的邊就肯定是割邊了
但是我們還要把割邊變成環
就相當於把每個葉子節點聯通就行了
(有個圖示就簡明易懂了)
加的邊數就是(度為0的節點數+1)/2
Code
分離的路徑
#include <bits/stdc++.h>
using namespace std;
const int o=1e5;
int d[o],l[o],v[o],s[o],c[o],h[o],f[o];
int n,m,top,sum,dp,ans,cnt;
struct node {
int t;
int n;
bool c;
}p[o*10];
void add(int s,int t){
p[cnt].t=t;
p[cnt].c=0;
p[cnt].n=h[s];
h[s]=cnt++;
}
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void tarjan(int x,int f){
d[x]=l[x]=++dp;
v[x]=1;
s[top++]=x;
for(int i=h[x];i!=-1;i=p[i].n){
int y=p[i].t;
if(y==f){
continue;
}
if(!d[y]){
tarjan(y,x);
l[x]=min(l[x],l[y]);
if(l[y]>d[x]){
p[i].c=1;
p[i^1].c=1;
}
}
else if(v[y]){
l[x]=min(l[x],d[y]);
}
}
if(d[x]==l[x]){
sum++;
int y;
do{
y=s[--top];
v[y]=0;
c[y]=sum;
}while(y!=x);
}
}
void in(){
memset(h,-1,sizeof(h));
n=read();
m=read();
int x,y;
for(int i=1;i<=m;i++){
x=read();
y=read();
add(x,y);
add(y,x);
}
}
void work(){
tarjan(1,1);
for(int x=1;x<=n;x++){
for(int i=h[x];i!=-1;i=p[i].n){
if(p[i].c){
f[c[x]]++;
}
}
}
ans=0;
for(int i=1;i<=sum;i++){
if(f[i]==1){
ans++;
}
}
}
void out(){
cout<<(ans+1)/2;
}
int main(){
in();
work();
out();
return 0;
}
ATM
問最多價值
樹形DP或者最長路
由於樹形DP並不是我的長處,就打了SPFA
由於每個點只會算一次,所以還要縮點
強連通分量裡每個點都可以到達強連通分量的其他節點,
因此如果有一個節點可以被當成終點那麼整個強連通分量都可以是終點
強連通分量的點權就直接是每個節點的加和
這樣以後再跑SPFA就行了
起點固定終止點固定在滿足終止點的最大值位置就行了
Code
ATM
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int o=5e5+10;
struct path{
int t;
int n;
}e[o],map[o];
int st[o],head[o],cnt;
int atm[o],money[o];
int d[o],q[o];
int stack[o],top;
int dfn[o],low[o],vis[o],color[o],num,dex;
int n,m,a,b,s,p,ans,z;
int read(){
int i=1,j=0;
char ch=getchar();
while(ch<'0'||ch>'9'){
if(ch=='-'){
i=-1;
}
ch=getchar();
}
while(ch>='0'&&ch<='9'){
j=(j<<1)+(j<<3)+ch-'0';
ch=getchar();
}
return i*j;
}
void build(int s,int t){
e[++cnt].t=t;
e[cnt].n=st[s];
st[s]=cnt;
}
void tarjan(int x){
dfn[x]=++dex;
low[x]=dex;
vis[x]=1;
stack[++top]=x;
for(int i=st[x];i!=0;i=e[i].n){
int y=e[i].t;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(vis[y]){
low[x]=min(low[x],dfn[y]);
}
}
if(dfn[x]==low[x]){
vis[x]=0;
color[x]=++num;
while(stack[top]!=x){
color[stack[top]]=num;
vis[stack[top--]]=0;
}
top--;
}
}
void add(){
cnt=0;
for(int i=1;i<=n;i++){
for(int j=st[i];j;j=e[j].n){
int y=e[j].t;
if(color[i]!=color[y]){
map[++cnt].t=color[y];
map[cnt].n=head[color[i]];
head[color[i]]=cnt;
}
}
}
}
void spfa(int x){
memset(vis,0,sizeof(vis));
int l=1,r=1;
q[l]=x;
vis[x]=1;
d[x]=money[x];
while(l<=r){
int u=q[l++];
for(int i=head[u];i!=0;i=map[i].n){
int v=map[i].t;
if(d[v]<d[u]+money[v]){
d[v]=d[u]+money[v];
if(vis[v]){
continue;
}
q[++r]=v;
vis[v]=1;
}
}
vis[u]=0;
}
}
void in(){
n=read();
m=read();
for(int i=1;i<=m;i++){
a=read();
b=read();
build(a,b);
}
}
void pre(){
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
add();
for(int i=1;i<=n;i++){
atm[i]=read();
money[color[i]]+=atm[i];
}
}
void work(){
s=read();
p=read();
spfa(color[s]);
for(int i=1;i<=p;i++){
z=read();
ans=max(ans,d[color[z]]);
}
}
void out(){
printf("%d",ans);
}
int main(){
in();
pre();
work();
out();
return 0;
}
Trick or Treat
差不多和ATM一個題吧。。。
只不過終點任意
Code
Trick or Treat
#include<map>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define MAXN 100010
using namespace std;
map<int,int>ma[MAXN];
int n,tot,tot1,tim,top,num,sumcol;
int to[MAXN],net[MAXN],head[MAXN];
int to1[MAXN],net1[MAXN],head1[MAXN];
int stack[MAXN],visstack[MAXN],ans[MAXN];
int low[MAXN],dfn[MAXN],vis[MAXN],col[MAXN],sum[MAXN];
void add(int u,int v){
to[++tot]=v;net[tot]=head[u];head[u]=tot;
}
void add1(int u,int v){
to1[++tot1]=v;net1[tot1]=head1[u];head1[u]=tot1;
}
void tarjin(int now){
low[now]=dfn[now]=++tim;
vis[now]=1;visstack[now]=1;
stack[++top]=now;
for(int i=head[now];i;i=net[i])
if(visstack[to[i]])
low[now]=min(low[now],dfn[to[i]]);
else if(!vis[to[i]]){
tarjin(to[i]);
low[now]=min(low[now],low[to[i]]);
}
if(low[now]==dfn[now]){
col[now]=++sumcol;
while(stack[top]!=now){
col[stack[top]]=sumcol;
visstack[stack[top]]=0;
top--;sum[sumcol]++;
}
visstack[now]=0;top--;sum[sumcol]++;
}
}
void dfs(int now){
if(ans[now]) return ;
ans[now]+=sum[now];
for(int i=head1[now];i;i=net1[i]){
dfs(to1[i]);
ans[now]+=ans[to1[i]];
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
add(i,x);
}
for(int i=1;i<=n;i++)
if(!vis[i]) tarjin(i);
for(int i=1;i<=n;i++)
for(int j=head[i];j;j=net[j])
if(col[i]!=col[to[j]])
if(ma[col[i]].find(col[to[j]])==ma[col[i]].end()){
ma[col[i]][col[to[j]]]=1;
add1(col[i],col[to[j]]);
}
for(int i=1;i<=n;i++) dfs(col[i]);
for(int i=1;i<=n;i++) cout<<ans[col[i]]<<endl;
}