二分圖


二分圖

\(\text{Thanks for wikipedia}\)ios

定義 及 性質

  • 二分圖 :

    二分圖的頂點可分紅 兩互斥的獨立集 \(U\)\(V\) , 使得全部邊都連結一個 \(U\) 中的點和一個 \(V\) 中的點 .
    頂點集 \(U, V\) 被稱爲是圖的兩個部分.
    等價的 , 二分圖 能夠被定義成 圖中全部的環都有偶數個頂點 .git

    二分圖

    \[\text{圖 1}\]算法

    如圖 \(1\) , 可將\(U\)\(V\) 當作一個着色 :
    \(U\) 中全部頂點爲藍色 , \(V\) 中全部頂點着綠色 , 每條邊的兩個端點的顏色不一樣, 符合圖着色問題的要求 .網絡

    非二分圖

    \[\text{圖 2}\]ui

    相反的 , 非二分圖沒法被二着色 ,
    如圖 \(2\) , 將其中一頂點着藍色 且另外一着綠色後 , 第三個頂點 與上述具備兩顏色的頂點相連 , 沒法再對它着藍色或綠色 .spa

  • 匹配 :

    給定一張圖 G , 在 G的一子圖 M 中 , M 的邊集中的任意兩條邊都沒有共同的端點 , 則稱 M是一個匹配.net

    對於圖 \(1\) 所示二分圖 , 圖 \(3\) 的選擇方案 , 即爲 圖 \(1\)的一匹配3d

    匹配

    \[\text{圖 3}\]code

  • 最大匹配:

    給定一張圖 \(G\) , 其中邊數最多的匹配 , 即該圖的最大匹配

    對於圖 \(1\) 所示二分圖 , 圖 \(4\) 的選擇方案 , 即爲 圖 \(1\)的一最大匹配

    最大匹配

    \[\text{圖 4}\]

    最大匹配可能不惟一 , 但最大匹配的邊數肯定 , 且不可能超過圖中頂點 數的一半 .
    根據匹配的性質 , 一匹配中 不一樣邊對應的兩對頂點 徹底不一樣 , 不然它們相鄰 , 不知足匹配的定義

  • 二分圖的最小頂點覆蓋 :

    最小頂點覆蓋要求用最少的點 , 讓每條邊都至少和其中一個點關聯 .
    \(\text{Knoig}\) 定理 : 二分圖的最小頂點覆蓋數等於二分圖的最大匹配數 .

  • 二分圖的最大獨立集 :

    \(n\) 個點的圖 \(G\) 中選出 \(m\) 個點 , 使這 \(m\) 個點兩兩之間沒有連邊 , 求\(m\) 最大值 .
    引理 : 二分圖的最大獨立集數 = 節點數(\(n\)) — 最大匹配數(\(m\))


二分圖判斷 :

模板題 : https://vjudge.net/problem/HihoCoder-1121

經過 二分圖可被二着色的性質可得 ,
一個無向圖是否爲 二分圖 , 可經過 \(dfs / bfs\) 染色 , 來進行判斷 .

對於圖中每一聯通塊 , 任選其中一點 , 將其染爲綠色
顯然 , 爲知足每一條邊兩端點顏色都不一樣 , 與選擇點相連的點 都應被染爲藍色

則可以使用 \(bfs\)進行模擬染色 :

  1. 對於一 與當前結點聯通, 未被染色的點 , 將其染爲 與當前結點相反色
  2. 對於一 與當前結點聯通, 已被染色的點 , 若其顏色與 當前結點相同 , 則不合法, 該圖不爲 二分圖

\(dfs\)\(bfs\) 的複雜度 都爲\(O(n)\) , 爲避免出現遞歸爆棧狀況 , 通常使用 \(bfs\)

\(dfs\) 實現 :

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
 
const int MAX_V = 205;
 
vector<int> G[MAX_V]; // 圖
int V;                  // 頂點數
int color[MAX_V];       // 頂點i的顏色 1 或 -1
 
// 把頂點染成1或-1
bool dfs(int v, int c)
{
    color[v] = c; // 把頂點v染成顏色c
    for (int i = 0; i < G[v].size(); ++i) {
        if (color[G[v][i]] == c) return false;
        if (color[G[v][i]] == 0 && !dfs(G[v][i], -c)) return false;
    }
    return true;
}
 
void solve()
{
    for (int i = 0; i < V; ++i) {
        if (color[i] == 0) {
            if (!dfs(i, 1)) {
                puts("Wrong\n");
                return ;
            }
        }
    }
    puts("Correct\n");
}
 
int main()
{
    int E;
    while (scanf("%d%d", &V, &E) == 2) {
        int a, b;
        for (int i = 0; i < V; ++i) G[i].clear();           
        memset(color, 0, sizeof color);
        for (int i = 0; i < E; ++i) {
            scanf("%d%d", &a, &b);
            G[a].push_back(b);
            G[b].push_back(a);
        }
        solve();
    }
    return 0;
}

\(bfs\) 實現 :

//By:Luckyblock
#include <cstdio>
#include <cstring>
#include <queue>
#include <ctype.h>
const int MARX = 1e4 + 10;
//=============================================================
struct edge
{
    int u, v, ne;
}e[MARX << 3];
int num, T, N, M, head[MARX], color[MARX];
//=============================================================
inline int read()
{
    int s = 1, w = 0; char ch = getchar();
    for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
    for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
    return s * w;
}
inline void add(int u, int v)
{
    e[++ num].v = v, e[num].u = u;
    e[num].ne = head[u], head[u] = num;
}
bool bfs()
{
    for(int i = 1; i <= N; i ++)
      if(head[i] && color[i] == 0)//找到未被染色的 聯通塊 
      {
        std :: queue <int> q;
        while (!q.empty()) q.pop();
        q.push(i); color[i] = 1;//對起點 進行染色 
        
        while(! q.empty())
        {
          int u = q.front(); q.pop();
          for(int j = head[u]; j; j = e[j].ne)
          {
            int v = e[j].v;
            if(color[v] == color[u]) return 0;//一邊兩端點 顏色相同 , 則不合法 
            if(! color[v]) color[v] = - color[u], q.push(v);// 染爲相反色 
          }
        }
      }
    return 1;
}
//=============================================================
signed main()
{
    T = read();
    while(T --)
    {
      num = 0;
      memset(head, 0, sizeof(head));
      memset(color, 0, sizeof(color));
      N = read(), M = read(); 
      bool flag = 1;
      
      for(int i = 1; i <= M; i ++)
      {
        int u = read(), v = read();
        add(u, v), add(v, u);
      }
      
      if(bfs()) printf("Correct\n");
      else printf("Wrong\n");
    }
}

二分圖 最大匹配

  • 匈牙利算法 :

    • 算法核心

      對於一匹配 \(M\) ,
      增廣路徑是指從 \(M\) 中未使用的頂點開始 , 並從 \(M\) 中未使用的頂點結束的交替路徑 .
      能夠證實 , 一個匹配是最大匹配 , 當且僅當它沒有任何增廣路經

      匈牙利算法的核心 即尋找增廣路徑 , 它是一種用增廣路徑 求 二分圖最大匹配的算法

    • 算法流程 :

      先將給定二分圖分爲 \(U\) , \(V\) 兩獨立集 , 枚舉 \(U\) 中結點

      對當前枚舉\(U\) 中結點 \(u\), 枚舉其相鄰 的\(V\) 中結點 \(v\)
      • \(v\) 沒有與任一 \(U\) 中其餘結點配對 , 說明 \(E(u,v)\) 爲一增廣路 , 則可直接加入 最大匹配中
      • 不然 , 枚舉 \(v\) 相鄰的 \(U\)中結點 , 遞歸檢查 \(v\) 點的匹配點 是否可與其餘 \(V\)中結點 配對 .

        若能夠 , 則 \(u\) 可與 \(v\) 點進行匹配 , 加入最大匹配中
        不然 , \(u\) 不可與 \(v\) 匹配 , \(u\) 點無匹配點 , 不可加入最大匹配中

    • 手動模擬 :

      寫的太垃圾看不懂 ? 手玩一組數據試試 :

      從徹底圖中 摳出的二分圖

      \[\text{圖 5}\]

    1. 對 Yugari 進行匹配 :
      其直接鏈接點 Reimu 未被匹配 , 則將 Yugari 與 Reimu 進行匹配

    2. 對 Marisa 進行匹配 :
      其直接鏈接點 Patchouli 未被匹配 , 則將 Marisa 與 Patchouli 進行匹配

    3. 對 Suika 進行匹配 :
      其直接鏈接點 Reimu 被匹配 , 檢查 Reimu 的匹配點 Yugari 可否尋找到其餘匹配點
      • Yugari 可與 Yuyuko 進行匹配 , 則將 Yugari 與 Yuyuko 進行匹配
      因爲Yugari 匹配對象改變 , Reimu 未被匹配 , 則將 Suika與 Reimu 進行匹配
    4. 對 Aya 進行匹配 :

      其直接鏈接點 Reimu 被匹配 , 檢查 Reimu 的匹配點 Suika 可否尋找到其餘匹配點
      • Suika 無其餘匹配點 , 不可將 Suika 與其餘結點進行匹配

      因爲Suika 匹配對象不可改變 , Reimu 被匹配 , 則 Aya 無匹配點

    則此二分圖的一種最大匹配爲 :

    從徹底圖中 摳出的二分圖

    \[\text{圖 6}\]

    • 例題 :

      1. P3386 【模板】二分圖匹配

        模板題

      //By:Luckyblock
      #include <cstdio>
      #include <cstring>
      #include <ctype.h>
      #define ll long long
      const int MARX = 1e3 + 10;
      const int MARX1 = 1e7 + 10; 
      //=============================================================
      struct edge
      {
          int u, v, ne;
      }e[MARX1];
      int N, M, E, num, ans, head[MARX], match[MARX];
      bool vis[MARX];
      //=============================================================
      inline int read()
      {
          int s = 1, w = 0; char ch = getchar();
          for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
          for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
          return s * w;
      }
      void add(int u, int v)
      {
          e[++ num].u = u, e[num].v = v;
          e[num].ne = head[u], head[u] = num;
      }
      bool dfs(int u)//匈牙利算法配對 
      {
          for(int i = head[u]; i; i = e[i].ne)//枚舉能到達的點 
            if(! vis[e[i].v])//在此輪配對中未被訪問 
            {
              vis[e[i].v] = 1;
              if(! match[e[i].v] || dfs(match[e[i].v]))//若可配對 
              {
                match[e[i].v] = u;//更新 
                return 1; 
              }
            }
          return 0;
      }
      //=============================================================
      signed main()
      {
          N = read(), M = read(), E = read();
          for(int i = 1; i <= E; i ++)
          {
            int u = read(), v = read();
            if(u > N || v > M) continue;
            add(u, v);
          }
      
          for(int i = 1; i <= N; i ++)//枚舉一組中的N個點
          {
            memset(vis, 0, sizeof(vis));//初始化 
            if(dfs(i)) ans ++;//能夠將i點 加入匹配中 
          }
          printf("%d", ans);
      }
      1. P2756 飛行員配對方案問題

        二分圖匹配 模板 , 使用匈牙利算法
        題目要求輸出方案
        顯然 , 沒有匹配對象的點 , 其匹配對象爲 0
        將匹配完成後 各點的非0匹配對象輸出便可

      //By:Luckyblock
      #include <cstdio>
      #include <cstring>
      #include <ctype.h>
      #define ll long long
      const int MARX = 1e3 + 10;
      const int MARX1 = 1e7 + 10; 
      //=============================================================
      struct edge
      {
          int u, v, ne;
      }e[MARX1];
      int N, M, E, num, ans, head[MARX], match[MARX];
      bool vis[MARX];
      //=============================================================
      inline int read()
      {
          int s = 1, w = 0; char ch = getchar();
          for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
          for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
          return s * w;
      }
      void add(int u, int v)
      {
          e[++ num].u = u, e[num].v = v;
          e[num].ne = head[u], head[u] = num;
      }
      bool dfs(int u)//匈牙利算法配對 
      {
          for(int i = head[u]; i; i = e[i].ne)//枚舉能到達的點 
            if(! vis[e[i].v])//在此輪配對中未被訪問 
            {
              vis[e[i].v] = 1;
              if(! match[e[i].v] || dfs(match[e[i].v]))//若可配對 
              {
                match[e[i].v] = u; //更新
                return 1;
              }
            }
          return 0;
      }
      //=============================================================
      signed main()
      {
          N = read(), M = read();
          while(1) 
          {
            int u = read(), v = read();
            if(u == -1 && v == -1) break;
            add(u, v);
          }
      
          for(int i = 1; i <= N; i ++)//枚舉一組中的N個點
          {
            memset(vis, 0, sizeof(vis));//初始化 
            if(dfs(i)) ans ++;//能夠將i點 加入匹配中 
          }
          printf("%d\n", ans);
          for(int i = N + 1; i <= M; i ++)//輸出方案 
            if(match[i]) printf("%d %d\n", match[i], i);
      }
      1. P4304 [TJOI2013]攻擊裝置 ( P3355 騎士共存問題 )

        • 題目要求 :
          給定一 \(01\) 矩陣 , 可在 \(0\) 的位置 放置攻擊裝置.
          攻擊裝置 \((x,y)\) 可按照 "日" 字攻擊其周圍的 \(8\) 個位置
          \((x-1,y-2), (x-2,y-1), (x+1,y-2), (x+2,y-1), (x-1,y+2), (x-2,y+1), (x+1,y+2), (x+2,y+1)\)
          求在裝置互不攻擊的狀況下 , 最多能夠放置多少個裝置 .

        • 分析題意 :
          若將矩陣上每格看作一結點 , 向其可攻擊位置連邊
          建圖後, 則題目轉化爲 : 求給定圖的 最大獨立集

          因爲題目給定了一張棋盤圖 ,
          可將棋盤黑白染色, 使相鄰兩格子不一樣色 .
          觀察圖形發現 , 對於每個格子, 其可以攻擊到的位置 顏色必定與其不一樣

          棋盤圖

          將格點轉化爲結點後 , 按照染色的不一樣, 可將各點分爲兩點集
          能夠發現 , 對於每個點集之間沒有連邊 , 即每個點集都爲一獨立集
          則原棋盤圖轉化爲 一二分圖

          對於一二分圖, 其最大獨立集 = 節點數 \((n)\) \(—\) 最大匹配數 \((m)\)
          可以使用 求得最大匹配數 , 即得答案

      #include <cstdio>
      #include <queue>
      #include <cstring>
      #include <vector>
      #include <ctype.h>
      #define ll long long
      int ex[10] = {2, -2, 2, -2, -1, 1, -1, 1};//座標變化量
      int ey[10] = {1, 1, -1, -1, 2, 2, -2, -2};
      const int MARX = 4e4 + 10;
      const int MARX1 = 3e7 + 10; 
      //=============================================================
      struct edge
      {
          int u, v, ne;
      }e[MARX1];
      int N, num, node, ans, head[MARX], color[210][210], match[MARX];
      bool map[210][210], vis[MARX];
      std :: vector <int> U;
      //=============================================================
      inline int read()
      {
          int s = 1, w = 0; char ch = getchar();
          for(; !isdigit(ch); ch = getchar()) if(ch == '-') s = -1;
          for(; isdigit(ch); ch = getchar()) w = (w << 1) + (w << 3) + (ch ^ '0');
          return s * w;
      }
      void add(int u, int v)
      {
          e[++ num].u = u, e[num].v = v;
          e[num].ne = head[u], head[u] = num;
      }
      bool dfs(int u)//匈牙利算法 
      {
          for(int i = head[u]; i; i = e[i].ne)//枚舉能到達的點  
            if(! vis[e[i].v])//在此輪配對中未被訪問 
            {
              vis[e[i].v] = 1;//標記 
              if(! match[e[i].v] || dfs(match[e[i].v]))//若可配對 
              {
                match[e[i].v] = u;//更新 答案 
                return 1;
              }
            }
          return 0;
      }
      //=============================================================
      signed main()
      {
          N = read();
          for(int i = 1; i <= N; i ++) 
          {
            char tmp[210]; scanf("%s", tmp + 1);
            for(int j = 1; j <= N; j ++) 
            {
              map[i][j] = (tmp[j] == '0');// 判斷一格點是否可放置裝置 
              node += map[i][j];  //更新總可以使用格點數 
              color[i][j] = (i + j) % 2 ? 1 : -1;//對棋盤按照奇偶性 黑白染色 
            }
          }
      
          for(int i = 1; i <= N; i ++)//枚舉座標 
            for(int j = 1; j <= N; j ++)
              if(map[i][j])
                for(int k = 0; k < 8; k ++)//枚舉 可攻擊的點 
                {
                  if(i + ex[k] < 1 || i + ex[k] > N || j + ey[k] < 1 || j + ey[k] > N) continue;
                  if(! map[i + ex[k]][j + ey[k]]) continue;
                  add((i - 1) * N + j, (i + ex[k] - 1) * N + j + ey[k]);//連邊 
                }
      
          for(int i = 1; i <= N; i ++)//將染爲 1的點加入點集 
            for(int j = 1; j <= N; j ++)
              if(map[i][j] && color[i][j] == 1) 
                U.push_back((i - 1) * N + j);
      
          for(int i = 0, size = U.size(); i < size; i ++)//枚舉一獨立集中的點 
          {
            memset(vis, 0, sizeof(vis)); 
            if(dfs(U[i])) ans ++;//進行配對 
          }
          printf("%d", node - ans);//求得 最大獨立集 
      }
  • 網絡流 :

    咕了咕了 要是沒退役就再來更吧 = =


寫在後面 :

關於匈牙利算法的樣例解釋 :
你知道在一張徹底圖裏摳出一張二分圖有多難嗎

相關文章
相關標籤/搜索