為什么要遍歷兩次?——個人對於kosaraju算法的理解


  有人說kosaraju是最容易理解的求連通分量的算法。qwq我怎么不覺得......

  不理解為什么遍歷兩次的時候老師跟我說直接背就好了qwq怎么能這么不負責任....

以下代碼來自  http://blog.csdn.net/michealtx/article/details/8233814

    #define maxN 1024  
    int marked[maxN];//用於記錄某個點是否被訪問過,0為沒有被臨幸過,1為被臨幸過  
    int id[maxN];//記錄每個點所屬的連通分量  
    int count;//記錄連通分量總數目  
    void cc(graph *g){  
        int i;  
        memset(marked,0,sizeof(marked));  
        memset(id,0,sizeof(id));  
        count=0;  
        for(i=0;i<g->V;i++){//之所以這里用循環就是因為g指向的無向圖可能不是一個連通圖,而是由多個連同分量組成  
            if(!marked[i]){dfs(g,i); count++;}  
        }  
    }  
      
      
    void dfs(graph *g,int v){  
        graphNode *t;  
        marked[v]=1;  
        id[v]=count;  
        t=g->adjlist[v].next;//t指向v的鄰接點  
        while(t){  
            if(!marked[t->key]){dfs(g,t->key);}//這里是重點,就是你發現v到t->key有路徑就把它算到跟自己在一個連通分量里了,這里有一個隱性前提,就是你提前知道t->key一定可以到v,
所以你發現v可以到t->key的時候,你毫不猶豫把它算為跟自己一伙兒的了。Korasaju算法不同書上有不同的表述,區別是先遍歷圖g還是先遍歷圖g的逆向圖,這只是順序的區別。我把我看得版本完整說一下:
(1)先DFS遍歷圖g的逆向圖,記錄遍歷的逆后序。(什么叫逆后序?逆后序就是DFS時后序的逆序,注意逆后序不一定為DFS的前序。DFS前序為,訪問某個頂點前,把它push進隊列。DFS后序為訪問完某個頂點
后才把它push進隊列。而DFS逆后序為訪問完某個頂點后把它push進一個棧中。當DFS遍歷完整個圖后,后序隊列的輸出與逆后序棧的輸出正好相反。)(2)然后按着圖g逆向圖的DFS遍歷的逆后序序列遍歷圖g
求所有的強連通分量,這一步的過程跟無向圖求所有連通分量的算法一模一樣!按着這里說的遍歷順序重復無向圖求所有連通分量的步驟求出來的就是有向圖的所有強連通分量,為什么呢?因為我們完成第一步后
,按着第一步得到的逆后序要對有向圖g進行DFS遍歷的前一刻,前面這段過程就相當於我們完成了對這幅有向圖g一個加工,把它加工成了一個無向圖!也就是說,這個加工實現了我注釋開頭提到的那個隱性前提。
所以后面按着無向圖求所有連通分量的步驟求出來的就是有向圖g的所有強連通分量。舉個例子,比如有向圖3->5->4->3,它的逆向圖為3->4->5->3(你最好在紙上畫下,就是個三角循環圖),從逆向圖的頂點3
開始DFS,得到的逆后續為3,4,5 。按着這個順序對原圖進行DFS,DFS(3)時遇到5,則5肯定跟3在一個強連通分量中(為什么?因為我們逆向圖DFS(5)時肯定能到達3,這就是隱形前提。所以正向圖DFS(3)遇
到5時,我們毫不猶豫把它算到自己一個強連通分量中。)
t=t->next; } }

   這個是我唯一比較看得懂的一個思路......

  意思是說,我們進行對有向圖第二次深度遍歷時,其實是和尋找無向圖的連通分量步驟是一樣的。"就是你發現v到t->key有路徑就把它算到跟自己在一個連通分量里了,這里有一個隱性前提,就是你提前知道t->key一定可以到v,所以你發現v可以到t->key的時候,你毫不猶豫把它算為跟自己一伙兒的了"“前面這段過程就相當於我們完成了對這幅有向圖g一個加工,把它加工成了一個無向圖也就是說第一次的對逆圖的遍歷,是一個加邊的過程,確保r到s有路徑而s到r也有路徑。

 

 

  首先,后序就是先訪問節點,等到把這個節點深搜完了才把它加入棧中。

  第一個圖為原圖,第二個圖為逆圖。

  對於這個圖,得到的后序遍歷為1->2->5->6->4->3,得到的逆后序遍歷為 3->4->6->5->2->1(當然,強連通分量中各點順序無所謂)。

  如果直接用后序來進行第二次深搜,程序運行結果是這樣的

  前面已經說到了,第一次的逆圖遍歷是用來給有向圖加邊使之變成無向圖的。也就是假如r到s有邊,我們通過對逆圖的遍歷可以確定s到r是否有邊。對於后序來說,我們存儲到的先是子節點然后才是父節點。也就是說,排在后面的父節點能夠訪問到前面的節點,而排在前面的節點則不能訪問到前面的節點。所以我們把后序反過來,排在前面的就可以訪問到后面的節點。比如說逆后序中3排在1的前面,那么表示逆圖中3可以訪問到1,而1不能訪問到3。這樣便驗證了s到r是否有路徑。

  我們遍歷的這兩次,做了兩件事情:保證s到r有路,保證r到s有路。

  再來看剛才那個的錯誤,直接用按逆圖后序進行原圖深搜,1排在3的前面,所以我們的dfs同學就認為1可以訪問到3,所以把1加入到3所在的強連通分量中。

  咦,逆后序中3在2的前面,2怎么沒有加入3所在的強連通分量中呢?
  因為原圖遍歷時3不能訪問到2呀。是否屬於一個強連通分量是要靠逆圖和原圖的兩次判斷的,我一開始覺得加了邊就應該全在一個強連通分量里了qwq

 

貼個代碼

#include <iostream>
#include <stdlib.h>
using namespace std;
struct Anode 
{
    int num;
    struct Anode *next;
};
struct node
{
    struct Anode *first;
};
node New[100],old[100];
int n,e,top1=-1,top2=-1;
int stack1[100],stack2[100],vis[100]={0};
void dfs1(int v)//用於逆圖的遍歷   有關1的數組均用於逆圖  
{                                //有關2的數組均用於原圖 
    struct Anode *temp; 
    vis[v]=1;
    temp=New[v].first;
    while(temp!=NULL)
    {
        if(vis[temp->num]==0)
            dfs1(temp->num);
        temp=temp->next;
    }
    top1++;
    stack1[top1]=v;
}
void dfs2(int v)//用於原圖的遍歷 
{
    struct Anode *temp;
    vis[v]=1;
    temp=old[v].first;
    while(temp!=NULL)
    {
        if(vis[temp->num]==0)
            dfs2(temp->num);
        temp=temp->next;
    }
    top2++;
    stack2[top2]=v;
}
int main()
{
    int i,a,b,t,cnt=0;
    struct Anode *temp;
    cin>>n>>e;
    for(i=1;i<=n;i++){old[i].first=NULL;New[i].first=NULL;}
    for(i=1;i<=e;i++)
    {
        cin>>a>>b;
        temp=(Anode *)malloc(sizeof(Anode));
        temp->next=old[a].first;
        temp->num=b;
        old[a].first=temp;
        
        temp=(Anode *)malloc(sizeof(Anode));
        temp->next=New[b].first;
        temp->num=a;
        New[b].first=temp;
    }
    
    for(i=1;i<=n;i++)
        if(vis[i]==0)dfs1(i);
    for(i=1;i<=n;i++)vis[i]=0;
    i=0;
    while(top1>=0)
    {
        t=stack1[top1];
        top1--;
        if(vis[t]==0)
        {
            dfs2(t);
            cnt++;
            cout<<"Group "<<cnt<<": ";
            while(top2>=0)
            {
                cout<<stack2[top2]<<" ";
                top2--;
            }
            cout<<endl;
        }
    }
    return 0;
}

 


免責聲明!

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



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