zoukankan      html  css  js  c++  java
  • 为什么要遍历两次?——个人对于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;
    }
  • 相关阅读:
    Git上传文件、文件夹到github
    Git管理修改、撤销修改、删除文件
    Git使用教程之从远程库克隆项目(四)
    Git使用教程之在github上创建项目(三)
    Git使用教程之SSH连接方式配置(二)
    Git使用教程之新手也能看懂(一)
    JS字符串截取 “指定字符” 前面和后面的内容!
    vue.js 实现点击展开收起动画
    最简单的手机预览WEB移动端网页的方法
    vue-cli中浏览器图标的配置
  • 原文地址:https://www.cnblogs.com/xiongniLy/p/5708850.html
Copyright © 2011-2022 走看看