Tarjan算法


Tarjan算法

Tarjan算法是用於求圖上的強連通分量(環)的算法。

應用:

  • 有向圖求強連通分量/縮點
  • 無向圖求割點
  • 無向圖找環

求強連通分量/縮點

強連通是有向圖才有的概念。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱為強連通分量。求有向圖的強連通分量是Tarjan最基本的應用。

算法原理

Tarjan算法的復雜度是O(v+e)的,因為只需要進行一次dfs就能處理出一個塊中所有的強連通分量。首先建立dfn[]數組和low[]數組,前者記錄節點的dfs序(第幾個被dfs到),確定后就不會改變,后者記錄該節點屬於的強連通分量(能遍歷到的點)中dfs序最小的節點(的dfs序),是會不斷更新的。申明一個棧,用於存儲可能成為強連通分量中的點。

然后,依次對每個節點進行dfs(因為從一個節點不一定能遍歷到其他所有的節點),初始化low[]為dfs序(low[u]=dfn[u]=dfsid),將每次dfs的點u加入棧中,遍歷這個點能到達的相鄰的點v,如果這個點在棧中,說明構成了一個環,也就是強連通分量,就用v點的dfs序更新(取min)low[u],當遞歸回溯時,所有強連通分量中的點low[]值都會被更新為dfn[v]。當一個點u的low[]值與它的dfs序相同時,說明這個點是一個強連通分量的根(也可能這個分量只有這一個點),將棧中的點逐個彈出,直至u(也彈出)為止,將這些點染成同個顏色。

縮點:對於有些有向圖上的問題,可以將一個強連通分量用一個點來等效替代,這個時候染成的一種顏色就是新圖上的一個點。

int vis[maxn];//記錄棧中的元素
int cnt[maxn];//記錄一個強連通分量中的結點個數
int color[maxn];//染色,將同一個強連通分量中的點染成同個顏色
int dfn[maxn],low[maxn],dfsid=0,id=0;
stack<int>st;
void tarjan(int u){
    low[u]=dfn[u]=++dfsid;
    vis[u]=1;
    st.push(u);
    for(int i=head[u];i>0;i=E[i].next){
        int v=E[i].to;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){//u是一個強連通分量的根
        id++;
        int k;
        do{
            k=st.top();st.pop();
            vis[k]=0;
            color[k]=id;
            cnt[id]++;
        }while(k!=u);
    }
}
int main(){
    for(int i=1;i<=n;i++)
		if(!dfn[i])tarjan(i);
}

無向圖求割點

割點是無向圖才有的概念。如果有一個頂點集合,刪除這個頂點集合以及這個集合中所有頂點相關聯的邊以后,圖的連通分量增多,就稱這個點集為割集。若一個割集只有一個點,則稱這個點為割點。

算法原理

設u為dfs中的非根結點,如果點u的子結點v(后遍歷到的點)能遍歷到的最小 dfs序>=dfn[u],那么刪除u后v將和u之前的點斷開,說明u是割點 。雖然是無向圖,但這里的遍歷是有方向的,一個點不能沿着已經訪問過的點繼續往前(根)遍歷,但是low[]值能被所有相鄰點更新。最后得到的low[]並不是正確的(能訪問到的最小dfs序),因為后續結點被限制在只能遍歷到已訪問過的點就停止了,因此判斷的時候用low[v]==dfn[u]其實也是可以的。

若u是dfs中的根結點,那么如果u有2個及以上的兒子(除去u之外的每個連通塊都是一個兒子),u是割點。

#include <bits/stdc++.h>
using namespace std;
const int maxm=1e6+5;
const int maxn=1e5+5;
struct edge{
    int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
    E[++tot].v=v;
    E[tot].next=head[u];
    head[u]=tot;
}
set<int>res;//一定要用set,因為兩種判割點方式可能會重復記錄一個點
int low[maxn],dfn[maxn],dfsid;
void tarjan(int u,int rt){
    low[u]=dfn[u]=++dfsid;
    int child=0;
    for(int i=head[u];i>0;i=E[i].next){
        int v=E[i].v;
        if(!dfn[v]){
            child++;
            tarjan(v,rt);
            low[u]=min(low[u],low[v]);
            if(u!=rt&&low[v]>=dfn[u]){
                res.insert(u);
            }
        }
        low[u]=min(low[u],dfn[v]);
    }
    if(u==rt&&child>1)
        res.insert(u);
}
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        addedge(u,v);
        addedge(v,u);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i,i);
    printf("%d\n",res.size());
    for(auto v:res){
        printf("%d ",v);
    }
    printf("\n");
}

仙人掌圖判斷&判環

定義(有向圖和無向圖略有不同)

有向圖:

  1. 是一個強連通圖
  2. 每條有向邊都屬於且僅屬於一個環(強連通圖所有邊都必須在環上)

無向圖:

  1. 是一個連通圖
  2. 每條無向邊至多屬於一個環(即有些無向邊可以不屬於任何一個環)

無向圖仙人掌的判定和環計數:

首先判斷一個圖是否是連通的,dfs一次判斷點的個數即可,之后每找到一個環,就將環上除了根(最先遍歷到的點)之外的點度數都+1,如果一個點度數為2,說明這個點的前向邊被兩個環所共有,即該圖不是仙人掌圖。

//洛谷P4129,ans需要改成大數
#include <bits/stdc++.h>
using namespace std;
const int maxm=1e6+5;
const int maxn=1e5+5;
struct edge{
    int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
    E[++tot].v=v;
    E[tot].next=head[u];
    head[u]=tot;
}
int dfn[maxn],dep[maxn],fa[maxn],dfsid=0;
int cnt[maxn];//記錄一條邊屬於環的個數
long long ans=1;
bool cal(int u,int rt){
    ans=ans*(dep[u]-dep[rt]+2);//dep[u]-dep[rt]+1為環的大小
    for(u;u!=rt;u=fa[u]){//這里用點來代替dfs樹邊
        if(++cnt[u]==2)return 0;//如果一個邊被兩個環共用,那么不是仙人掌圖
    }
    return 1;
}
int siz=0;
int ok=1;
void tarjan(int u){
    siz++;
    dfn[u]=++dfsid;
    for(int i=head[u];i;i=E[i].next){
        int v=E[i].v;
        if(v==fa[u])continue;
        if(!dfn[v]){
            dep[v]=dep[u]+1;
            fa[v]=u;
            tarjan(v);
        }
    }
    for(int i=head[u];i;i=E[i].next){
        int v=E[i].v;
        if(fa[v]!=u&&dfn[u]<dfn[v]){
            if(!cal(v,u))
                ok=0;
        }
    }
}
int a[maxn];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int ki;
        scanf("%d",&ki);
        for(int i=1;i<=ki;i++){
            scanf("%d",&a[i]);
        }
        for(int i=1;i<=ki-1;i++){
            addedge(a[i],a[i+1]);
            addedge(a[i+1],a[i]);
        }
    }
    tarjan(1);
    if(ok==0||siz!=n){//邊被多個環共用||非連通圖
        printf("0\n");
    }
    else{
        cout<<ans<<endl;//所有環大小+1的乘積
    }
}

有向圖仙人掌的判定:

先用tarjan判斷這個圖是否強連通,之后同無向圖一樣,找是否有度數為2的點。

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
const int maxm=1e6+5;
struct edge{
    int v,next;
}E[maxm];
int head[maxn],tot=0;
void addedge(int u,int v){
    E[++tot].v=v;
    E[tot].next=head[u];
    head[u]=tot;
}
int vis[maxn];//記錄棧中的元素
int dfn[maxn],low[maxn],fa[maxn],dfsid=0,id=0;
stack<int>st;
int siz=0;
void tarjan(int u){
    siz++;
    low[u]=dfn[u]=++dfsid;
    vis[u]=1;
    st.push(u);
    for(int i=head[u];i>0;i=E[i].next){
        int v=E[i].v;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){//u是一個強連通分量的根
        id++;
        int k;
        do{
            k=st.top();st.pop();
            vis[k]=0;
        }while(k!=u);
    }
}
int du[maxn];
bool cal(int u,int rt){
    for(u;u!=rt;u=fa[u]){
        if(++du[u]==2){
            return 0;
        }
    }
    return 1;
}
int ok=1;
void dfs(int u){
    dfn[u]=++id;
    for(int i=head[u];i;i=E[i].next){
        int v=E[i].v;
        if(!dfn[v]){
            fa[v]=u;
            dfs(v);
        }
        else{
            if(!cal(u,v)){
                ok=0;
            }
        }
    }
}
void init(int n){
    dfsid=0;id=0;
    fill(dfn,dfn+1+n,0);
    fill(low,low+1+n,0);
    fill(fa,fa+1+n,0);
    fill(du,du+1+n,0);
    fill(vis,vis+1+n,0);
    siz=0;ok=1;
}
int main(){
    int T;
    cin>>T;
    while(T--){
        int n;
        scanf("%d",&n);
        init(n);
        fill(head,head+1+n,0);
        tot=0;
        int u,v;
        while(scanf("%d%d",&u,&v)){
            if(u==0&&v==0) break;
            u++;v++;
            addedge(u,v);
        }
        tarjan(1);
        if(id>1||siz!=n){
            printf("NO\n");
            continue;
        }
        init(n);
        dfs(1);
        if(!ok){
            printf("NO\n");
        }
        else{
            printf("YES\n");
        }
    }
}

例題

Bomb(縮點/拓撲)

題意:

一個平面上有n個炸彈,每個炸彈i有一個圓形的爆炸范圍ri和二維坐標xi,yi,以及手動引爆這個炸彈的費用ci。如果引爆了一個炸彈,其爆炸范圍內(包括邊界)的所有其他炸彈都會被引爆(會發生連鎖反應),問如何以最小的費用引爆所有的炸彈?

思路:

顯然一個炸彈能引爆其他炸彈,但是其他炸彈不一定能引爆這個炸彈,這是一個有向圖。但是有的炸彈可能會循環引爆構成一個環,用tarjan將圖上的環縮掉就可以變成一個森林圖,顯然只要把圖中所有入度為0節點引爆,所有點都會被引爆。

縮點后,計算入度前要判斷該邊連接的兩個點u,v是否為同一顏色,同色就不計度數,不同顏色就將v所在顏色的入度+1。同樣地,計算一個大點的權值也是用小點對該顏色的權值取min。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e3+5;
const int maxm=2e6+5;
struct edge{
    int v,next;
}E[maxm];
int tot=0,head[maxn];
void addedge(int u,int v){
    E[++tot].v=v;
    E[tot].next=head[u];
    head[u]=tot;
}
int vis[maxn];//記錄棧中的元素
int cnt[maxn];//記錄一個強連通分量中的結點個數
int color[maxn];//染色,將同一個強連通分量中的點染成同個顏色
int dfn[maxn],low[maxn],dfsid=0,id=0;
int n;
stack<int>st;
void tarjan(int u){
    low[u]=dfn[u]=++dfsid;
    vis[u]=1;
    st.push(u);
    for(int i=head[u];i>0;i=E[i].next){
        int v=E[i].v;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){//u是一個強連通分量的根
        id++;
        int k;
        do{
            k=st.top();st.pop();
            vis[k]=0;
            color[k]=id;
            cnt[id]++;
        }while(k!=u);
    }
}
int x[maxn],y[maxn],r[maxn],c[maxn];
bool f(ll x1,ll y1,ll r1,ll x2,ll y2){
    if((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)<=r1*r1)
        return 1;
    else return 0;
}
int du[maxn];//入度
int cost[maxn];
void init(){
    memset(cost,0,sizeof(cost));
    memset(du,0,sizeof(du));
    memset(head,0,sizeof(head));
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(vis,0,sizeof(vis));
    memset(cnt,0,sizeof(cnt));
    dfsid=0,id=0;
    tot=0;
    while(st.size())st.pop();
}
int main(){
    int t;
    cin>>t;
    for(int kase=1;kase<=t;kase++){
        scanf("%d",&n);
        init();
        for(int i=1;i<=n;i++){
            scanf("%d%d%d%d",&x[i],&y[i],&r[i],&c[i]);
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                if(j==i)continue;
                if(f(x[i],y[i],r[i],x[j],y[j])){
                    addedge(i,j);
                }
            }
        }
        for(int i=1;i<=n;i++)
            if(!dfn[i])tarjan(i);
        for(int u=1;u<=n;u++){
            for(int i=head[u];i;i=E[i].next){
                int v=E[i].v;
                if(color[v]!=color[u]){
                    du[color[v]]++;//記錄縮成的大點的入度
                }
            }
        }
        for(int i=1;i<=n;i++){
            if(cost[color[i]]==0){
                cost[color[i]]=c[i];
            }
            else
                cost[color[i]]=min(cost[color[i]],c[i]);
        }
        int ans=0;
        for(int i=1;i<=id;i++){
            if(du[i]==0)
                ans+=cost[i];
        }
        printf("Case #%d: %d\n",kase,ans);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM