《啊哈算法》——割點、割邊、二分圖


  這篇文章我們簡單的介紹求解圖的割點、割邊和二分圖相關的概念。

 

  割點:

  對於含n個點、m條邊的連通無向圖G,如果去掉頂點vi(並同時去掉與之相連的邊),使得G不再連通,那么稱vi是一個割點。

  通過其定義,我們不難判斷某個點是否是割點,但是現在我們面臨的問題是,如何給出一個圖G,編碼讓計算機求解割點呢?

  首先我們考慮這樣一個問題,判定某個點的指標是什么。我們通過人腦來判斷其是否是割點,其實是利用非常模糊的視覺效應,即“通過去掉該點觀察圖是否連通”即可,而如果想要通過計算機來判斷,就需要非常量化的判斷條件。

  我們考慮從深度優先搜索的角度來找到這樣一個判斷條件,利用dfs遍歷圖,得到的生成子圖本質上會得到一個生成樹,我們拿出兩個相鄰的點vi、vj,vi是vj的父節點。我們回到深搜遍歷的過程中,假設當前遍歷到vj,如果我們從vj能夠找到一條回到已經訪問過的v1、v2...等節點,那么這表明去掉vi,將不會影響剩余圖的連通性。

  我們似乎發現了些什么,但是這種判定關系還是有些模糊。

  我們借用這樣一個概念——時間戳,即深度優先搜索的過程中,我們記錄訪問節點的順序,我們用num[i]來表示節點vi的時間戳,即在深搜遍歷過程中第幾個訪問vi節點。借用這個工具,我們考慮能不能將上述我們描述的關系用量化的表達式表示出來呢?好像還是有點捉襟見肘啊,我們不妨再設置一個數組low[i],用以表達vi不經過dfs的生成樹的父節點所能夠到達的時間戳最小的節點(好好理解,非常拗口),基於這個工具,我們能夠看到上述的判斷條件,可以用這樣一個表達式簡潔的概括:

                                                                          low[j] < num[i]

  那么現在我們首要的問題似乎變成了求解n個節點的low[]、num[]了。

  首先,對於num[],也就是時間戳的記錄,並不困難。而對於low[]數組的求解,就需要動一些腦筋了。我們模擬遍歷過程,當前遍歷到vi點,我們訪問所有與vi連通的點vj,會出現如下兩種情況。

  1.vj訪問過,被我們打上過時間戳,  那么我們此時需要更新low[i]了,即low[i] = min{num[j] | vj與vi連通}。

  2.vj沒有訪問過,那么我們繼續深搜遍歷點的過程。

  在遍歷完成之后,也完成了num[]、low[]的求解,我們再利用深搜的回溯過程,完成判斷即可。

  這里需要注意的一點是,對於某個圖的根節點,即dfs開始的那個點(記作v1)其實是不滿足上文給出的判斷式子的,需要我們特殊判斷,記child是根節點的子樹個數,則v1是個割點的必要條件是,child  = 2。

  簡單的參考代碼如下。

 

#include<cstdio>
#include<algorithm>
using namespace std;

int n , m , e[9][9] , root;
int num[9] , low[9] , flag[9],index;
int min(int a , int b)
{
     return a < b ? a : b;
}
void dfs(int cur , int father)
{
    int child = 0 , i , j;

      index++;
      num[cur] = index;
      low[cur] = index;
      for(i = 1;i <= n;i++)
      {
            if(e[cur][i] == 1)
            {
                   if(num[i] == 0)  //第一種情況
                   {
                         child++;
                         dfs(i,cur);
                         low[cur] = min(low[cur] , low[i]);//回溯過程:判斷割點

                         if(cur != root && low[i] >= num[cur])
                              flag[cur] = 1;
                         else if(cur == root && child == 2)
                              flag[cur] = 1;
                   }
                   
                    else if(i != father) //第二種情況
                    {
                        low[cur] = min(low[cur] , num[i]);
                    }

            }
      }
}

int main()
{
     int i , j, x , y;
     scanf("%d %d",&n,&m);
     for(i = 1;i <= n;i++)
          for(j = 1;j <= n;j++)
             e[i][j] = 0;
     for(i = 1;i <= m;i++)
     {
          scanf("%d %d",&x,&y);
          e[x][y] = 1;
          e[y][x] = 1;
     }

     root = 1;
     dfs(1,root);

     for(i = 1;i <= n;i++)
     {
           if(flag[i] == 1)
              printf("%d ",i);
     }

     return 0;
}

 

  割邊:

  有個割點的概念,割點非常好理解,即對於圖G,如果刪除邊ei,導致G的連通度發生變化,那么ei即是G的一個割邊。

  那么我們來繼續思考如何利用編程實現求G的割邊。

  基於上文我們對割點問題的思考,這里問題會顯得非常簡單,在判斷割點的時候,我們利用的核心判斷條件是low[j] >= num[i],其中vi是vj的父節點。那么拿到割邊上來,我們分兩部分看,如果low[j]>num[i],則表明去掉eij后,vj便不再和vj連通,這是符合割邊定義的。而如果low[j] = num[i],則表明去掉eij,vj依然能夠在不經過eij的情況下到達vi,連通度沒有發生改變。

  因此我們可以看到,對於互相連通的父子節點vi、vj,滿足 low[j]>num[i],可判定eij是一條割邊。

  基於dfs找割點的代碼,我們進行稍微的改動,有如下代碼。

 

#include<cstdio>
#include<algorithm>
using namespace std;

int n , m , e[9][9] , root;
int num[9] , low[9] , flag[9],index;
int min(int a , int b)
{
     return a < b ? a : b;
}
void dfs(int cur , int father)
{
    int  i , j;

      index++;
      num[cur] = index;
      low[cur] = index;
      for(i = 1;i <= n;i++)
      {
            if(e[cur][i] == 1)
            {
                   if(num[i] == 0)  //第一種情況
                   {

                         dfs(i,cur);
                         low[cur] = min(low[cur] , low[i]);//回溯過程:判斷割點

                         if(low[i] > num[cur])
                              printf("%d-%d\n",cur,i);
                   }

                    else if(i != father) //第二種情況
                    {
                        low[cur] = min(low[cur] , num[i]);
                    }

            }
      }
}

int main()
{
     int i , j, x , y;
     scanf("%d %d",&n,&m);
     for(i = 1;i <= n;i++)
          for(j = 1;j <= n;j++)
             e[i][j] = 0;
     for(i = 1;i <= m;i++)
     {
          scanf("%d %d",&x,&y);
          e[x][y] = 1;
          e[y][x] = 1;
     }

     root = 1;
     dfs(1,root);


     return 0;
}

 

 

  二分圖的最大匹配:

  我們先給出這樣導入模型的實際問題:現有n個妹子和n個漢字相約去坐過山車,過山車的結構是兩兩一排,現在要求坐一排的必須是一男一女,並且要求兩人必須相互認識,那么請問我們最多能安排多少對男女上過山車?

  我們將問題抽象化,將每個個體視為點,而男女之間的是否認識視為點與點之間的邊,這里僅僅是強調了男生和女生的聯系,其余的關系我們不考慮,因此我們將男生放入A集合(該集合中的元素之間不存在邊),女生放入B集合,可以看到,這是典型的二分圖。

  而我們將一對一對的男女送到兩座一排的過山車的過程,抽象得來看,可以用圖論中的術語——匹配,來表示。即將A中每個元素與B中的元素形成一一對應的關系,需要強調的是,只可以使一一對應的關系,而這種一一對應的匹配的對數,便稱作匹配數量。即高度概括一下我們即將導入的模型——如何求解二分圖的最大匹配數量。

  我們注意到“最多”這個字眼,容易聯想到其與貪心算法有着密切的聯系,因此我們考慮從這個角度給出一個計算二分圖最大匹配數量的算法。

  考慮將全局問題給子問題化然后尋求局部最優解,這樣方能引導出全局最優解。假設含2n個頂點的二分圖的一個分圖A含有n個頂點,我們依次遍歷集合A中的點v1、v2...vn,我們從過程開始分析,假設當前遍歷到vi點,顯然,我們盡可能的將vi匹配到B集合中的某個和vi相連的點,是當前局部最優的策略,那么我們不妨再遍歷B集合中與vi相連的點vi'、vj'......容易看到,對於這些點(以vi'為例),都會滿足如下的兩個性質之中的一個:

  1.vi'在之前A集合(i-1)個點遍歷的過程中,並沒有與前i-1個點匹配。

  2.vi'在之前A集合(i-1)個點遍歷的過程中,和前i-1個點中的某個點進行了匹配。

  針對情況1,我們當然可以將vi與vi'匹配,則匹配數+1,這是當前的最優策略。

  針對情況2,就顯得有些麻煩,既然vi'在A中已經有了匹配點,那么我們就此放棄vi了么?顯然不是,我們需要經過深思熟慮才能決定是否放棄vi。我們注意到初始情況的二分圖G是存在一對多的情況,即A集合中的某個點可能與B集合中的多個點均有邊,這其實就像是在匹配的時候留下了“其他選項 ”,其實就十分像我們利用dfs實現的回溯尋找迷宮,而我們在面對這種情況的時候,則需要利用dfs來回溯回去來嘗試所有這些“其他選項”,判斷在A集合前i-1個點在最大匹配數的基礎上,能否將vi返回情況1.如果存在,那則匹配數+1,這是當前的最優策略;如果不存在,那么即可放棄vi的匹配(想一想,為什么這里就可以直接放棄了,這種做法其實和前面的鋪墊是自洽的)。

  理解了上文對求解二分圖最大匹配數算法的過程的描述,其正確性是不言自明的。概括起來,它本質上是一種貪心策略和基於dfs的窮舉策略。

  簡單的參考代碼如下。

 

#include<cstdio>
using namespace std;

int e[101][101];
int match[101];
int book[101];
int n , m;
int dfs(int u)
{
     int i;
     for(i = 1;i <= n;i++)
     {
          if(book[i] == 0 && e[u][i] == 1)
          {
                book[i] = 1;
                   if(match[i] == 0 || dfs(match[i]))
                   {
                        match[i] = u;
                        match[u] = i;
                        return 1;
                   }
          }
     }

     return 0;
}

int main()
{
      int i , j , t1 , t2 , sum = 0;
      scanf("%d %d",&n,&m);

      for(i = 1;i <= m;i++)
      {
            scanf("%d %d",&t1,&t2);
            e[t1][t2] = 1;
            e[t2][t1] = 1;
      }

        for(i = 1;i <= n;i++)   match[i] = 0;

        for(i = 1;i <= n;i++)
        {
              for(j = 1;j <= n;j++)  book[j] = 0;
                  if(dfs(i))  sum++;
        }

        printf("%d",sum);

        return 0;
}

 


免責聲明!

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



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