「笔记」二分图


写在前面 :

\(\text{Thanks for wikipedia}\)
\(\text{Thanks for OI-Wiki}\)
\(\text{Thanks for Luogu}\)

定义 及 性质

  • 二分图 :

    二分图的顶点可分成 两互斥的独立集 \(U\)\(V\) , 使得所有边都连结一个 \(U\) 中的点和一个 \(V\) 中的点 .
    顶点集 \(U, V\) 被称为是图的两个部分.
    等价的 , 二分图 可以被定义成 图中所有的环都有偶数个顶点 .

    二分图

    \[\text{图 1} \]

    如图 \(1\) , 可将\(U\)\(V\) 当做一个着色 :
    \(U\) 中所有顶点为蓝色 , \(V\) 中所有顶点着绿色 , 每条边的两个端点的颜色不同, 符合图着色问题的要求 .

    非二分图

    \[\text{图 2} \]

    相反的 , 非二分图无法被二着色 ,
    如图 \(2\) , 将其中一顶点着蓝色 且另一着绿色后 , 第三个顶点 与上述具有两颜色的顶点相连 , 无法再对它着蓝色或绿色 .

  • 匹配 :

    给定一张图 G , 在 G的一子图 M 中 , M 的边集中的任意两条边都没有共同的端点 , 则称 M是一个匹配

    对于图 \(1\) 所示二分图 , 图 \(3\) 的选择方案 , 即为 图 \(1\)的一匹配

    匹配

    \[\text{图 3} \]

  • 最大匹配:

    给定一张图 \(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);//求得 最大独立集 
      }
      
  • 网络流 :

    • 算法核心:
      在一个匹配中, 任意两条边都没有共同的端点.
      每一个端点 最多只有 1 的贡献.

      在网络流中, 最多只有 1 的贡献, 可转化为 边的容量为 1.
      由此可对原二分图进行转化:

      1. 新建源点 与 汇点.
      2. 源点向每个左侧点 各连一条容量为 1 的有向边.
      3. 原图中的边改为 自左向右的有向边, 容量为 1.
      4. 每个右侧点向汇点 各连一条容量为 1 的有向边.
    网络流建图

    \[\text{图 7} \]

    强制使每一条边,每一个点 贡献至多为1.
    使用网络最大流算法, 求得源点至汇点的最大流, 即为答案.

    P3386 【模板】二分图匹配 Dinic:

    //知识点:网络流
    /*
    By:Luckyblock
    使用了当前弧优化.
    
    边从0开始编号的原因: 0 ^ 1 = 1, 1 ^ 1 = 0, 1 ^ 1 != 2 
    每一次有流量汇入 == 成功匹配
    */
    #include <cstdio>
    #include <cctype>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    #define min std::min
    #define ll long long
    const int MARX = 1e7 + 10;
    const int INF = 2e9 + 10;
    //===========================================================
    struct Edge
    {
        int u, v, w, ne;
    } e[MARX << 1];
    int N, M, E, S, T, edgenum = - 1, head[MARX];
    int Dep[MARX], cur[MARX];
    //===========================================================
    inline int read()
    {
        int f = 1, w = 0; char ch = getchar();
        for(; ! isdigit(ch); ch = getchar()) if(ch == '-') f = - 1;
        for(; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
        return f * w;
    }
    void AddEdge(int u, int v, int w)
    {
        e[++ edgenum].u = u, e[edgenum].v = v, e[edgenum].w = w;
        e[edgenum].ne = head[u], head[u] = edgenum; 
    }
    bool Bfs()
    {
    	std :: queue <int> Q;
        memset(Dep, 0 ,sizeof(Dep));
        Dep[S] = 1; Q.push(S);
    
        do 
        {
          int u = Q.front(); Q.pop();
            for(int i = head[u]; i != - 1; i = e[i].ne)
              if(e[i].w > 0 && Dep[e[i].v] == 0)
                Dep[e[i].v] = Dep[u] + 1,
                Q.push(e[i].v);
        } 
        while(! Q.empty());
        return Dep[T];
    }
    int Dfs(int u, int dis)
    {
        if(u == T) return dis;
        for(int& i = cur[u]; i != - 1; i = e[i].ne)
          if(Dep[e[i].v] == Dep[e[i].u] + 1 && e[i].w) 
          {
      	    int ret = Dfs(e[i].v, min(dis, e[i].w));
      	    if(ret > 0) 
    	    {
    	      e[i].w -= ret, e[i ^ 1].w += ret;
    	      return ret;
    	    }
        }
        return 0;
    }
    int Dinic() 
    {
        int ans = 0;
        while(Bfs()) 
        {
          for(int i = 0; i <= N + M + 1; i ++) cur[i] = head[i];
            while(int ret = Dfs(S, INF)) ans += ret;
        }
        return ans;
    }
    //===========================================================
    int main()
    {
        N = read(), M = read(), E = read(), S = 0, T = N + M + 1;
        memset(head, - 1, sizeof(head)); 
        for(int i = 1; i <= N; i ++) AddEdge(0, i, 1), AddEdge(i, 0, 0);
        for(int i = N + 1; i <= N + M; i ++) AddEdge(i, T, 1), AddEdge(T, i, 0);
        for(int i = 1; i <= E; i ++)
        {
          int u = read(), v = read();
          if(u > N || v > M) continue;
          AddEdge(u, v + N, 1), AddEdge(v + N, u, 0);
        }
        printf("%d", Dinic());
        return 0;
    }
    

写在后面 :

关于二分图最大匹配的样例解释 :
你知道在一张完全图里抠出一张二分图有多难吗

2020.6.13: 好像Yukari写成yugari了= =希望不要被隙ejwopidmwadklawhdiowh

posted @ 2019-11-07 17:58  Luckyblock  阅读(343)  评论(2编辑  收藏  举报