Tarjan 總結及各種題型拓展(縮點篇)

【Tarjan算法的做用】:html

  1. 求強連通份量;
  2. 縮點(將一個環縮成一個點);
  3. 割點(這裏不談)……

 

【Tarjan算法的過程】:node

  1. 初始化數組:dfn[u](時間戳:該節點是第幾個被首次訪問到的),low[u](low[u]表示u或u的子樹所能回溯到的棧中的最先的節點的dfn值)
  2. 堆棧:將u壓入棧頂
  3. 更新low[u]
  4. 對於邊(u,v),若是v不在棧中,即v是第一次被訪問,知足dfn[v]==0;則繼續向下找,而後low[u]=min(low[u],low[v]);若是v在棧中,即v已被訪問,知足dfn[v]!=0;若是v未被染色,表明v在棧中(dfn[v]!=0表示v進過棧,在棧中的點染色後被彈出,未被染色即未被彈出還在棧中),則low[u]=min(low[u],dfn[v])

        5.若是完成上述操做後 low[u]==dfn[u],則將u和在u以後入棧的全部節點彈出,被彈出的全部結點構成一個強連通份量c++

        6.繼續搜索(有向圖不必定連通),直到全部點都被遍歷算法

 

【圖解】:數組

 

 

 

 

 

 

 

 

 

【代碼實現】(部分):spa

struct node{
    int ver,next;
}r[];                                         //鄰接表
inline void tarjan(int u){
    dfn[u]=++num;                             //num計數
    low[u]=num;
    sta[++top]=u;                             //手寫棧,入棧
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);                        //向下找,dfs的思想
            low[u]=min(low[u],low[v]);
        }
        else 
        if(!c[v])                             //若是結點v還在棧中,則v不屬於任何強連通份量
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;                           //染色
        while(sta[top]!=u){
            c[sta[top]]=col;
            --top;
        }
        --top;                                //將u彈出(退棧)
    }
}

【時間複雜度】:O(n+m)3d

【基礎題型】:htm

     1.https://www.luogu.com.cn/problem/P2863blog

    【題目大意】:排序

      有一個 n 個點,m 條邊的有向圖,請求出這個圖點數大於 1 的強聯通份量個數。

    【題目分析】:

      裸題,跳過,直接上代碼

      注意:求點數大於 1 的強聯通份量個數

    【代碼】:

#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,tot,num,top,ans;
int dfn[10005],low[10005],sta[10005],take[10005],head[10005],color[10005];
struct node{
    int ver,next;
}r[200005];
inline void add(int x,int y){
    r[++cnt].ver=y;
    r[cnt].next=head[x];
    head[x]=cnt;
}
inline void tarjan(int x){
    dfn[x]=++tot;
    low[x]=tot;
    sta[++top]=x;
    for(int i=head[x];i;i=r[i].next){
        int y=r[i].ver;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(!color[y]) low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x]){
        color[x]=++num;
        while(sta[top]!=x){
            color[sta[top]]=num;
            --top;
        }
        --top;
    }
}
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);
    for(int i=1;i<=n;i++)
        take[color[i]]++;
    for(int i=1;i<=num;i++)
        if(take[i]>1)
            ans++;
    printf("%d",ans);
    return 0;
}

 

 


2.https://www.luogu.com.cn/problem/P2002
【題目大意】:

有n個城市,中間有單向道路鏈接,消息會沿着道路擴散,如今給出n個城市及其之間的道路,問至少須要在幾個城市發佈消息才能讓這全部n個城市都獲得消息。

【題目分析】:
當1->2,2->3,3->1時,三點構成一個環,這時不管在哪一個城市發佈消息,1,2,3三個城市都能獲得消息,此時該環等效於一個點,用Tarjan算法縮點,獲得有向無環圖(可能不止一個)
而後進行拓撲排序(更像一種思想,不會去看一下),在全部入度爲0的點(每個有向無環圖的起點)發佈消息,而後全部點均可以獲得消息

【圖解】:
顯而易見,只要在全部入度爲0的點(有向無環圖的起點)(1,7兩點)發佈消息,全部點就均可以收到消息


【代碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,cr,dsc,col,top,ans;
int c[100005],h[100005],dfn[100005],low[100005],sta[100005],rd[100005];      //rd[i]記錄i點的入度
struct node{
    int ver,next;
}r[500005];
inline void add(int x,int y){
    r[++cr].ver=y;
    r[cr].next=h[x];
    h[x]=cr;
}
inline void tarjan(int u){
    dfn[u]=++dsc;
    low[u]=dsc;
    sta[++top]=u;
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else 
        if(!c[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;
        while(sta[top]!=u){
            c[sta[top]]=col;
            top--;
        }
        top--;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        for(int j=h[i];j;j=r[j].next){
            int l=r[j].ver;
            if(c[i]!=c[l]) rd[c[l]]++;
        }
    for(int i=1;i<=col;i++)
        if(rd[i]==0)
            ans++;
    printf("%d",ans);
    return 0;
}

3.https://www.luogu.com.cn/problem/P3387
【題目大意】:

          給定一個 n 個點 m 條邊有向圖,每一個點有一個權值,求一條路徑,使路徑通過的點權值之和最大。容許屢次通過一條邊或者一個點,可是,重複通過的點,權值只計算一次。求權值和。

  【題目分析+圖解】:
用一個數組w記錄每一個點的點權,縮點,再用另外一個數組W記錄縮點後的每一個點的點權(爲構成該縮點的全部點的點權之和),獲得有向無環圖

        可知:有三條路徑:1. 1->2->5

                                         2. 1->3->5

                                         3. 1->4->5

        易得:三條路徑只需比較後半部分,若要使所選路徑的點權值和最大,則2,3,4三點中應選擇點權值最大的點

        用sum[i]數組進行DP操做,表示從入度爲0的點(起點)到i點的路徑的最大權值和

        注意:須要初始化sum[i]=W[i](只須要初始化入度爲0的點,但全部點都初始化也不要緊),表示從i點走到i點通過的點的最大權值和(點權)

        狀態轉移方程:sum[l]=max(sum[l],sum[i]+W[l])

 
【代碼】

#include<bits/stdc++.h>
using namespace std;
queue<int> q;
int n,m,cr,cR,col,top,arr,ans;
int w[10005],W[10005],c[10005],h[10005],H[10005],sta[10005],dfn[10005],low[10005],rd[10005],sum[10005];     //小寫表示縮點前,大寫表示縮點後,c表示染色
struct node{
    int ver,next;
}r[100005],R[100005];
inline void add(int x,int y){
    r[++cr].ver=y;
    r[cr].next=h[x];
    h[x]=cr;
}
inline void Add(int x,int y){
    R[++cR].ver=y;
    R[cR].next=H[x];
    H[x]=cR;
}
inline void tarjan(int u){
    dfn[u]=++arr;
    low[u]=arr;
    sta[++top]=u;
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else
        if(!c[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;
        W[col]+=w[u];                              //計算縮點後的點的權值
        while(sta[top]!=u){
            W[col]+=w[sta[top]];
            c[sta[top]]=col;
            --top;
        }
        --top;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        for(int j=h[i];j;j=r[j].next){
            int l=r[j].ver;
            if(c[i]!=c[l]){
                Add(c[i],c[l]);
                rd[c[l]]++;                        //統計入度
            }
        }
    for(int i=1;i<=col;i++)                        //初始化
        sum[i]=W[i];
    for(int i=1;i<=col;i++)
        if(rd[i]==0)
            q.push(i);                             //入隊,進行拓撲排序(bfs),隊列裏存放入度爲0的點
    while(q.size()){
        int i=q.front();
        q.pop();
        if(rd[i]==0)
            for(int j=H[i];j;j=R[j].next){
                int l=R[j].ver;
                rd[l]--;
                if(rd[l]==0)
                    q.push(l);                     //若是入度爲0,入隊,入隊後不會再次入隊,無需判斷
                sum[l]=max(sum[l],sum[i]+W[l]);
            }
    }
    for(int i=1;i<=col;i++)
        ans=max(ans,sum[i]);                       //拓撲排序(搜索)完後再更新ans,不然答案可能會出錯
    printf("%d",ans);
    return 0;
}

  【拓展題型】:
   4.https://www.luogu.com.cn/problem/P2341
【題目分析】:

           易得:存在於同一個強聯通份量裏的全部牛必定互相受歡迎

           那麼,找出入度爲0的縮點後的點(反向建邊),這樣能夠保證全部的奶牛都喜歡它,可是它不喜歡任何人,因此說不存在其餘奶牛明星

           特殊狀況:若是有兩個入度爲0的縮點,則不存在奶牛明星,由於這樣沒法知足全部的牛喜歡他

   【代碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt,tot,dsc,col,top,ans;
int c[10005],h[10005],dfn[10005],low[10005],rd[10005],sta[10005],num[10005];
struct node{
    int ver,next;
}r[200005];
inline void add(int x,int y){
    r[++tot].ver=y;
    r[tot].next=h[x];
    h[x]=tot;
}
inline void tarjan(int u){
    dfn[u]=++dsc;
    low[u]=dsc;
    sta[++top]=u;
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else
            if(!c[v])
                low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;
        num[col]++;
        while(sta[top]!=u){
            num[col]++;
            c[sta[top]]=col;
            top--;
        }
        top--;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(y,x);                                //反向建邊
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        for(int j=h[i];j;j=r[j].next){
            int l=r[j].ver;
            if(c[i]!=c[l])
                rd[c[l]]++;
        }
    dsc=0;
    for(int i=1;i<=col;i++)
        if(rd[i]==0){
            ans=num[i];
            dsc++;                               //統計入度爲0的點的個數
        }
    if(dsc>1) ans=0;                             //若是存在兩個及兩個以上的入度爲0的點,則不存在明星奶牛
    printf("%d",ans);
    return 0;
}


5.https://www.luogu.com.cn/problem/P2746
   【題目分析+圖解】:
先給你一條鏈,如何使點上任意一點均可以到達其餘全部點

            分析一下就很容易想到,只須要加一條邊,使該鏈構成一個環

 

            接下來類比,將樹轉化爲幾條鏈,鏈數爲出度爲0的結點(在下面的狀況下可理解爲樹的葉子節點)的個數

 

 

             但存在另外一種狀況

 

 

              此時鏈數爲入度爲0的點的數量

              因此須要添加的邊的數量爲 max(入度爲0的點的數量,出度爲0的點的數量)

              特殊狀況見代碼

   【代碼】:
 
#include<bits/stdc++.h>
using namespace std;
int n,cr,cR,dsc,col,top,lck,ans;
bool V[1000];
int c[1000],h[1000],H[1000],dfn[1000],low[1000],sta[1000],rd[1000],cd[1000];     //cd[]表示出度
struct node{
    int ver,next;
}r[100000],R[100000];
inline void add(int x,int y){
    r[++cr].ver=y;
    r[cr].next=h[x];
    h[x]=cr;
}
inline void Add(int x,int y){
    R[++cR].ver=y;
    R[cR].next=H[x];
    H[x]=cR;
}
inline void tarjan(int u){
    dfn[u]=++dsc;
    low[u]=dsc;
    sta[++top]=u;
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else 
        if(!c[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;
        while(sta[top]!=u){
            c[sta[top]]=col;
            top--;
        }
        top--;
    }
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int x;
        scanf("%d",&x);
        while(x!=0){
            add(i,x);
            scanf("%d",&x);
        }
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++){
        memset(V,0,sizeof(V));
        for(int j=h[i];j;j=r[j].next){
            int l=r[j].ver;
            if(V[c[l]]) continue;
            if(c[i]!=c[l]){
                Add(c[i],c[l]);
                V[c[l]]=1;
                rd[c[l]]++;
                cd[c[i]]++;                    //同時記錄出度和入度
            }
        }
    }
    for(int i=1;i<=col;i++){
        if(rd[i]==0)
            lck++;
        if(cd[i]==0)
            ans++;
    }
    if(col==1)                                 //特殊狀況:若是整個圖縮爲一個點,則不須要加邊
        printf("%d\n0",lck);
    else printf("%d\n%d",lck,max(lck,ans));
    return 0;
}


6.https://www.luogu.com.cn/problem/P3627
【題目分析】:
    本題跟【基礎題型】3 相似,但若是採用一樣的解題方法會超時,那麼咱們須要一些特殊操做
首先咱們須要將點權轉化爲邊權
一條邊的權值爲該邊通向的縮點後的點的點權
而後取負,用SPFA算法搜最短路,而後求出的最小值取負,獲得最長路的結果,即爲答案

【代碼】:
#include<bits/stdc++.h>
using namespace std;
int n,m,s,p,cr,cR,col,dsc,top,ans;
bool V[500005],jb[500005],JB[500005];            //jb[i]表示縮點前i點是否爲酒吧,JB[i]表示縮點後i點是否爲酒吧
int c[500005],w[500005],W[500005],h[500005],H[500005],dfn[500005],low[500005],sta[500005],dis[500005];
struct node{
    int ver,edge,next;
}r[500005],R[500005];
queue<int> q;
inline void add(int x,int y){
    r[++cr].ver=y;
    r[cr].next=h[x];
    h[x]=cr;
}
inline void Add(int x,int y,int z){
    R[++cR].ver=y;
    R[cR].edge=z;
    R[cR].next=H[x];
    H[x]=cR;
}
inline void tarjan(int u){
    dfn[u]=++dsc;
    low[u]=dsc;
    sta[++top]=u;
    for(int i=h[u];i;i=r[i].next){
        int v=r[i].ver;
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else 
        if(!c[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u]){
        c[u]=++col;
        W[col]+=w[u];
        if(jb[u]) JB[col]=1;                     //若是該強連通份量中有一點爲酒吧,則縮點後能夠在該點(結束)統計答案
        while(sta[top]!=u){
            W[col]+=w[sta[top]];
            c[sta[top]]=col;
            if(jb[sta[top]]) JB[col]=1;
            --top;
        }
        --top;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    for(int i=1;i<=n;i++)
        scanf("%d",&w[i]);
    scanf("%d%d",&s,&p);
    for(int i=1;i<=p;i++){
        int x;
        scanf("%d",&x);
        jb[x]=1;
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    memset(dis,0x7fffffff,sizeof(dis));             //初始化
    dis[c[s]]=-W[c[s]];                             //初始化,縮點c[s]沒有入度,因此dis[c[s]]權值爲點c[s]的點權的相反數
    for(int i=1;i<=n;i++){
        for(int j=h[i];j;j=r[j].next){
            int l=r[j].ver;
            if(c[i]!=c[l])
                Add(c[i],c[l],-W[c[l]]);            //取負
        }
    }
    q.push(c[s]);
    while(q.size()){
        int x=q.front();
        q.pop();
        V[x]=0;
        for(int i=H[x];i;i=R[i].next){
            int j=R[i].ver;
            int l=R[i].edge;
            if(dis[j]>dis[x]+l){
                dis[j]=dis[x]+l;
                if(!V[j]) q.push(j);
                V[j]=1;
            }
        }
    }
    for(int i=1;i<=col;i++)
        if(JB[i])                                   //判斷是否能夠在該點結束(更新答案)
            ans=max(ans,-dis[i]);
    printf("%d",ans);
    return 0;
}2020-07-25
相關文章
相關標籤/搜索