強連通分量與縮點(Tarjan演算法)(洛谷P3387)

forezxl發表於2017-10-05

名詞解釋:

強連通分量:

有向圖強連通分量:在有向圖G中,如果兩個頂點vi,vj間(vi>vj)有一條從vi到vj的有向路徑,同時還有一條從vj到vi的有向路徑,則稱兩個頂點強連通(strongly connected)。如果有向圖G的每兩個頂點都強連通,稱G是一個強連通圖。有向圖的極大強連通子圖,稱為強連通分量(strongly connected components)。

簡單點說,即在有向圖中對於某個子圖S,如果S中的點兩兩都直接或間接的相連,則稱這個子圖為強連通分量。(要求最大的滿足條件的子圖)

縮點:

把一個強連通分量用一個點代替的操作稱為縮點。

演算法實現

對於縮點,我們採用Tarjan演算法來實現它。
Tarjan其實就是一個DFS的過程。

所需記錄的量

開一個棧來記錄dfs的軌跡。

對於節點i,記錄dfn[i]表示i是第幾個被訪問的,low[i]表示以i為父親的節點能訪問到的在棧中最前面的節點。(即i指向的節點的最小非零dfn值,因為會有環存在,所以上述情況是有可能發生的)。

遍歷過程

程式碼解釋

void tarjan(int x){//當前遍歷到x
    dfn[x]=low[x]=++p;//初始化dfn與low
    stack[++top]=x;//入棧
    f[x]=true;//標記入棧
    for (int i=h1[x];i;i=ed[i].next)//訪問其兒子
        if (!dfn[ed[i].to]){//如果還沒有做過
            tarjan(ed[i].to);//繼續遍歷
            low[x]=min(low[x],low[ed[i].to]);//遞迴完成後更新low值
        }
        else //如果已經做過
            if (f[ed[i].to])//如果在棧內 
                low[x]=min(low[x],dfn[ed[i].to]);//更新low值
    if (low[x]==dfn[x]){//如果到底
        f[x]=false;//判其出棧
        k++;
        while (stack[top+1]!=x){//棧要彈到當前節點位置
        //在這裡可以更新一些其他的東西
            num[stack[top]]=k;//給新的強連通分量標號
            f[stack[top--]]=false;//出棧
        }
        num[x]=k;//當前節點也屬於新的強連通分量
    }
}

模板

縮點配合最短(長)路演算法可以用於求有向圖中路徑上的最大點權和。

洛谷P3387為例:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define MAXN 10000
using namespace std;
struct edge{
    int next;
    int to;
};
int n,m,k,p,top,ans;
int dfn[MAXN+5],low[MAXN+5],stack[MAXN+5],num[MAXN+5];
int dis[MAXN+5],que[MAXN*10+5],h1[MAXN+5],h2[MAXN+5],w[MAXN+5];
edge ed[MAXN*20+5];
bool f[MAXN+5];
inline char readc(){//讀優
    static char buf[100000],*l=buf,*r=buf;
    if (l==r) r=(l=buf)+fread(buf,1,100000,stdin);
    if (l==r) return EOF; return *l++;
}
inline int _read(){
    int num=0; char ch=readc();
    while (ch<'0'||ch>'9') ch=readc();
    while (ch>='0'&&ch<='9') { num=num*10+ch-48; ch=readc(); }
    return num;
}
void addedge(int x,int y,int *h){
    ed[++k].next=h[x];
    ed[k].to=y;
    h[x]=k;
}
void tarjan(int x){//縮點
    dfn[x]=low[x]=++p;
    stack[++top]=x;
    f[x]=true;
    for (int i=h1[x];i;i=ed[i].next)
        if (!dfn[ed[i].to]){
            tarjan(ed[i].to);
            low[x]=min(low[x],low[ed[i].to]);
        }
        else 
            if (f[ed[i].to]) low[x]=min(low[x],dfn[ed[i].to]);
    if (low[x]==dfn[x]){
        f[x]=false;
        while (stack[top+1]!=x){
            num[stack[top]]=x;//這裡直接把所有點都縮到祖先上
            f[stack[top--]]=false;
        }
        num[x]=x;
    }
}
void spfa(int now){//求最大點權和
    int l=0,r=1; que[1]=now; dis[now]=w[now]; ans=max(ans,dis[now]);
    while (l<r){
        int x=que[++l];
        f[x]=false;
        for (int i=h2[x];i;i=ed[i].next)
            if (dis[ed[i].to]<dis[x]+w[ed[i].to]){
                dis[ed[i].to]=dis[x]+w[ed[i].to];
                ans=max(ans,dis[ed[i].to]);
                if (!f[ed[i].to]){
                    f[ed[i].to]=true; que[++r]=ed[i].to;
                }
            }
    }
}
int main(){
    n=_read(); m=_read();
    for (int i=1;i<=n;i++)
        w[i]=_read();
    for (int i=1;i<=m;i++){
        int u=_read(),v=_read();
        addedge(u,v,h1);
    }
    for (int i=1;i<=n;i++)//要保證每個點都被縮過
        if (!dfn[i])
            tarjan(i);
    for (int i=1;i<=n;i++)
        if (num[i]!=i)
            w[num[i]]+=w[i];
    for (int i=1;i<=n;i++)
        for (int j=h1[i];j;j=ed[j].next)
            if (num[i]!=num[ed[j].to])//如果他們不屬於一個強連通分量
                addedge(num[i],num[ed[j].to],h2);//重新建邊
    for (int i=1;i<=n;i++)
        if (num[i]==i&&!dis[i])
            spfa(i);
    printf("%d\n",ans);
    return 0;
}

相關文章