二分图最大匹配 前置知识:二分图 、图匹配 
引入 本文讨论二分图 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 
生活中一个典型的二分图匹配的例子是男女配对。设有若干男生(𝑋 X 𝑌 Y 𝐸 E 
提示  本文假设已知二分图顶点集 𝑉 V 𝑉   = 𝑋   ∪ 𝑌 V = X ∪ Y 𝑉 V 二分图的染色算法  在 𝑂 ( | 𝑉 |   + | 𝐸 | ) O ( | V | + | E | ) 
Kuhn 算法 Kuhn 算法是 Berge 引理  的直接应用。它也是 匈牙利算法  的一部分。
过程 为了求出最大匹配,算法依次枚举所有顶点,求出从它出发的一条增广路,并进行增广。因为增广路的长度总是奇数,所以在二分图中,它的端点必然分别位于左右两个部分。这说明,只需要考虑从左部出发的增广路即可。
为了寻找增广路,可以依据当前的匹配 𝑀 M DFS  或 BFS  在 𝑂 ( | 𝐸 | ) O ( | E | ) 
(图中,深色点为匹配点,浅色点为未匹配点,红边为匹配边,黑边为非匹配边,箭头表示当前匹配对应的定向。由图可知,路径 1   → 8   → 3   → 1 1   → 6   → 1 2 1 → 8 → 3 → 11 → 6 → 12 
算法开始时,所有边都指向右部点。每次寻找到增广路后,都需要沿着增广路将经过的所有边都反向,以表示它们的匹配状态已经反转。算法结束时,所有指向左部点的边就是匹配边。
因为至多只需要枚举 𝑂 ( | 𝑉 | ) O ( | V | ) 各一次 ,所以,算法总的时间复杂度为 𝑂 ( | 𝑉 | | 𝐸 | ) O ( | V | | E | ) 
优化 有一些简单的技巧,可以优化 Kuhn 算法的常数:
Kuhn 算法基于 Berge 引理,而后者并不要求事先给出二分图的左右部分。因此,即使在左右两部分未明确划分的情况下,Kuhn 算法也能正确运行,只要图本身是二分图。但是,先将二分图染色,确定好它的左部和右部,往往效率更高。 因为上文描述的 Kuhn 算法的时间复杂度实际上是 𝑂 ( | 𝑋 | | 𝐸 | ) O ( | X | | E | ) 𝑋 X  在寻找增广路时,用于避免重复查找的标记无需每次 DFS 都清空。可以在清空标记前,尝试为所有未匹配的左部点都寻找增广路。在一轮这样的查找中,所有边至多访问一次,复杂度仍然是 𝑂 ( | 𝐸 | ) O ( | E | ) 𝑘 k | 𝑀 |   + 1 | M | + 1 𝑀 M 𝑂 ( 𝑘 | 𝐸 | ) O ( k | E | )  寻找增广路时,优先考虑未匹配的右部点,因为这意味着更短的增广路。 因为 Berge 引理并不要求初始匹配为空,所以,Kuhn 算法开始时,可以随机地选取一些互不相交的边作为初始匹配,以减少后续搜索的次数。如果已经应用优化 3,本优化可以忽略。 虽然最差复杂度仍然是 𝑂 ( | 𝑉 | | 𝐸 | ) O ( | V | | E | ) 
参考实现 实现时,不需要真正维护定向,只需要为每个顶点都维护与它相匹配的顶点即可。
模板题 Library Checker - Matching on Bipartite Graph    1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 #include   <algorithm> 
#include   <ctime> 
#include   <iostream> 
#include   <random> 
#include   <tuple> 
#include   <vector> 
std :: mt19937_64   rng ( 
     static_cast < std :: mt19937_64 :: result_type > ( std :: time ( nullptr ))); 
struct   BipartiteGraph   { 
   int   n1 ,   n2 ;                         // number of vertices in X and Y, resp. 
   std :: vector < std :: vector < int >>   g ;    // edges from X to Y 
   std :: vector < int >   ma ,   mb ;    // matches from X to Y and from Y to X, resp. 
   std :: vector < bool >   vis ;      // visiting marks for DFS. 
   BipartiteGraph ( int   n1 ,   int   n2 ) 
       :   n1 ( n1 ),   n2 ( n2 ),   g ( n1 ),   ma ( n1 ,   -1 ),   mb ( n2 ,   -1 )   {} 
   // Add an edge from u in X to v in Y. 
   void   add_edge ( int   u ,   int   v )   {   g [ u ]. emplace_back ( v );   } 
   // Find an augmenting path starting at u. 
   bool   dfs ( int   u )   { 
     vis [ u ]   =   true ; 
     // Heuristic: find unsaturated vertices whenever possible. 
     for   ( int   v   :   g [ u ])   { 
       if   ( mb [ v ]   ==   -1 )   { 
         ma [ u ]   =   v ; 
         mb [ v ]   =   u ; 
         return   true ; 
       } 
     } 
     for   ( int   v   :   g [ u ])   { 
       if   ( ! vis [ mb [ v ]]   &&   dfs ( mb [ v ]))   { 
         ma [ u ]   =   v ; 
         mb [ v ]   =   u ; 
         return   true ; 
       } 
     } 
     return   false ; 
   } 
   // Kuhn's maximum matching algorithm. 
   std :: vector < std :: pair < int ,   int >>   kuhn_maximum_matching ()   { 
     // Randomly shuffle the edges. 
     for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
       std :: shuffle ( g [ u ]. begin (),   g [ u ]. end (),   rng ); 
     } 
     // Find a maximal set of vertex-disjoint augmenting paths in each round. 
     while   ( true )   { 
       bool   succ   =   false ; 
       vis . assign ( n1 ,   false ); 
       for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
         succ   |=   ma [ u ]   ==   -1   &&   dfs ( u ); 
       } 
       if   ( ! succ )   break ; 
     } 
     // Collect the matched pairs. 
     std :: vector < std :: pair < int ,   int >>   matches ; 
     matches . reserve ( n1 ); 
     for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
       if   ( ma [ u ]   !=   -1 )   { 
         matches . emplace_back ( u ,   ma [ u ]); 
       } 
     } 
     return   matches ; 
   } 
}; 
int   main ()   { 
   std :: ios :: sync_with_stdio ( false ),   std :: cin . tie ( nullptr ); 
   int   n1 ,   n2 ,   m ; 
   std :: cin   >>   n1   >>   n2   >>   m ; 
   BipartiteGraph   gr ( n1 ,   n2 ); 
   for   ( int   i   =   0 ;   i   <   m ;   ++ i )   { 
     int   u ,   v ; 
     std :: cin   >>   u   >>   v ; 
     gr . add_edge ( u ,   v ); 
   } 
   auto   res   =   gr . kuhn_maximum_matching (); 
   std :: cout   <<   res . size ()   <<   '\n' ; 
   for   ( int   i   =   0 ;   i   <   res . size ();   ++ i )   { 
     std :: cout   <<   res [ i ]. first   <<   ' '   <<   res [ i ]. second   <<   '\n' ; 
   } 
   return   0 ; 
} 
Hopcroft–Karp 算法 Hopcroft–Karp 算法进一步优化了 Kuhn 算法查找增广路的过程,将总的轮数降低到了 𝑂 ( | 𝑉 | 1 / 2 ) O ( | V | 1 / 2 ) 𝑂 ( | 𝑉 | 1 / 2 | 𝐸 | ) O ( | V | 1 / 2 | E | ) Dinic 算法  的一种特殊情形。
过程 算法依然是在寻找增广路,但是为了在更少的轮次内完成匹配,算法在每一轮中都采取了如下策略:
将匹配边定向为指向左部点,非匹配边定向为指向右部点。 从所有未匹配的左部点出发,在有向图上进行 BFS,记录每个访问到的顶点所在的层数 𝑑 ( 𝑣 ) d ( v )  依次从每个未匹配的左部点出发进行 DFS,寻找增广路并进行增广。DFS 时沿着满足层数连续且严格递增(即 𝑑 ( 𝑣 ′ )   = 𝑑 ( 𝑣 )   + 1 d ( v ′ ) = d ( v ) + 1  用网络流的术语说,步骤 2 是构造了一个层次图,而步骤 3 则是找到了层次图上的阻塞流。所谓层次图,是指它的每条边都必然从这一层指向下一层;而所谓阻塞流,在当前语境下,就是指一组极大的、两两之间没有公共顶点的增广路。步骤 3 得到的这一组增广路必然是极大的:假设不然,存在一条新的增广路,那么在当初枚举到它的起点时,就应当已经找到这样一条路。
比起前文的 Kuhn 算法,Hopcroft–Karp 算法最关键的改变就是在求阻塞流之前,添加了求层次图这一步骤。基于层次图进行 DFS,相当于限制了算法总是沿着最短路径到达各个顶点。这样做的好处是,在算法的不同轮次间,算法找到的增广路的长度是严格递增的。而且,可以证明的是,直到求出最大匹配为止,增广路的长度至多增加 3 | 𝑀 | 1 / 2 3 | M | 1 / 2 | 𝑀 | | M | 𝑂 ( | 𝑀 | 1 / 2 ) O ( | M | 1 / 2 ) 𝑂 ( | 𝑀 | 1 / 2 | 𝐸 | ) O ( | M | 1 / 2 | E | ) 2 | 𝑀 |   ≤ | 𝑉 | 2 | M | ≤ | V | 𝑂 ( | 𝑉 | 1 / 2 | 𝐸 | ) O ( | V | 1 / 2 | E | ) 
证明  首先说明,在算法的不同轮次之间,算法找到的增广路的长度是严格递增的。
 假设当前轮次的 BFS 中,顶点向前延伸了 ℓ ℓ ℓ ℓ { 𝑃 𝑖 } { P i } ℓ ℓ 
 实际上,如果 𝑃 P 𝑀 M 𝑃 ′ P ′ 𝑀   ⊕ 𝑃 M ⊕ P | 𝑃 ′ |   ≥ | 𝑃 |   + 2 | 𝑃   ∩ 𝑃 ′ | | P ′ | ≥ | P | + 2 | P ∩ P ′ | 𝑁   = ( 𝑀   ⊕ 𝑃 )   ⊕ 𝑃 ′ N = ( M ⊕ P ) ⊕ P ′ 𝑀 M Berge 引理的证明 ,可以说明对称差 𝑀   ⊕ 𝑁   = 𝑃   ⊕ 𝑃 ′ M ⊕ N = P ⊕ P ′ 𝑀 M 𝑃 1 P 1 𝑃 2 P 2 𝑃 P 
 2 | 𝑃 | ≤ | 𝑃 1 | + | 𝑃 2 | ≤ | 𝑃 ⊕ 𝑃 ′ | = | 𝑃 | + | 𝑃 ′ | − 2 | 𝑃 ∩ 𝑃 ′ | . 2 | P | ≤ | P 1 | + | P 2 | ≤ | P ⊕ P ′ | = | P | + | P ′ | − 2 | P ∩ P ′ | . 这就说明 | 𝑃 ′ |   ≥ | 𝑃 |   + 2 | 𝑃   ∩ 𝑃 ′ | | P ′ | ≥ | P | + 2 | P ∩ P ′ | { 𝑃 𝑖 } { P i } 𝑃 ′ P ′ { 𝑃 𝑖 } { P i } 
 最后,需要说明,增广路的长度至多增加 3 | 𝑀 | 1 / 2 3 | M | 1 / 2 
 记 𝑝   = ⌊ | 𝑀 | 1 / 2 ⌋ p = ⌊ | M | 1 / 2 ⌋ 𝑝 p | 𝑀 | 1 / 2 | M | 1 / 2 𝑀 𝑝 M p ( 𝑉 , 𝑀   ⊕ 𝑀 𝑝 ) ( V , M ⊕ M p ) | 𝑀 |   − | 𝑀 𝑝 | | M | − | M p | 𝑀 𝑝 M p | 𝑀 | 1 / 2 / 2 | M | 1 / 2 / 2 𝑀 M 2 | 𝑀 | 1 / 2 2 | M | 1 / 2 | 𝑀 |   − | 𝑀 𝑝 |   ≤ 2 | 𝑀 | 1 / 2 | M | − | M p | ≤ 2 | M | 1 / 2 𝑀 𝑝 M p 2 | 𝑀 | 1 / 2 2 | M | 1 / 2 2 | 𝑀 | 1 / 2 2 | M | 1 / 2 3 | 𝑀 | 1 / 2 3 | M | 1 / 2 
这仅仅是对 Hopcroft–Karp 算法的最差复杂度的估计。实际上,在随机图中,Hopcroft–Karp 算法的时间复杂度有很大概率是 𝑂 ( | 𝐸 | l o g  | 𝑉 | ) O ( | E | log  | V | ) 
优化 在建立层次图时,和一般的 Dinic 算法一样,Hopcroft–Karp 算法在到达未匹配的右部点时就终止。但是,仅仅就二分图匹配问题来说,这样做是没有必要的。而且,因为 BFS 过早终止,限制了后续 DFS 的范围,会导致每轮找到的增广路数目有限,从而拖慢整体匹配效率。在有些图上,它的效率甚至不如经过优化的 Kuhn 算法。所以,一个简单的改进是,不提前终止 BFS,而是为所有可以到达的顶点建立层次图。
正确性证明  在优化后的算法中,阻塞流中增广路的长度将不再相同,因此,前文中关于复杂度的证明也不再成立。但是,可以说明的是,通过为算法的每个轮次建立辅助图,同样可以建立最短增广路长度严格递增的结论,进而保证最差复杂度依然是正确的。
 设二分图为 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 𝑀 M 𝑊   ⊆ 𝑌 W ⊆ Y 𝑦   ∈ 𝑊 y ∈ W 𝑑 ( 𝑦 ) d ( y ) 𝑑 m a x   = m a x 𝑦 ∈ 𝑊 𝑑 ( 𝑦 ) d max = max y ∈ W d ( y ) 𝑦   ∈ 𝑊 y ∈ W 𝑦 y 𝑑 m a x   − 𝑑 ( 𝑦 ) d max − d ( y ) 𝐺 ′   = ( 𝑋 ′ , 𝑌 ′ , 𝐸 ′ ) G ′ = ( X ′ , Y ′ , E ′ ) 𝑀 ′ M ′ 𝑑 m a x d max 𝐺 G 𝑀 M 𝐺 ′ G ′ 𝑀 ′ M ′ 𝐺 G 𝐺 ′ G ′ 𝐺 ′ G ′ 𝑑 m a x d max 𝐺 G 𝑑 m i n   = m i n 𝑦 ∈ 𝑊 𝑑 ( 𝑦 ) d min = min y ∈ W d ( y ) 𝐺 ′ G ′ 𝑑 m a x d max 𝑂 ( | 𝑀 | 1 / 2 | 𝐸 | ) O ( | M | 1 / 2 | E | ) 
参考实现 模板题 Library Checker - Matching on Bipartite Graph    1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
92 
93 
94 
95 
96 
97 #include   <algorithm> 
#include   <iostream> 
#include   <queue> 
#include   <tuple> 
#include   <vector> 
struct   BipartiteGraph   { 
   int   n1 ,   n2 ;                         // number of vertices in X and Y, resp. 
   std :: vector < std :: vector < int >>   g ;    // edges from X to Y 
   std :: vector < int >   ma ,   mb ;    // matches from X to Y and from Y to X, resp. 
   std :: vector < int >   dist ;      // distance from unsaturated vertices in X. 
   BipartiteGraph ( int   n1 ,   int   n2 ) 
       :   n1 ( n1 ),   n2 ( n2 ),   g ( n1 ),   ma ( n1 ,   -1 ),   mb ( n2 ,   -1 )   {} 
   // Add an edge from u in X to v in Y. 
   void   add_edge ( int   u ,   int   v )   {   g [ u ]. emplace_back ( v );   } 
   // Build the level graph. 
   bool   bfs ()   { 
     dist . assign ( n1 ,   -1 ); 
     std :: queue < int >   q ; 
     for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
       if   ( ma [ u ]   ==   -1 )   { 
         dist [ u ]   =   0 ; 
         q . emplace ( u ); 
       } 
     } 
     // Build the level graph for all reachable vertices. 
     bool   succ   =   false ; 
     while   ( ! q . empty ())   { 
       int   u   =   q . front (); 
       q . pop (); 
       for   ( int   v   :   g [ u ])   { 
         if   ( mb [ v ]   ==   -1 )   { 
           succ   =   true ; 
         }   else   if   ( dist [ mb [ v ]]   ==   -1 )   { 
           dist [ mb [ v ]]   =   dist [ u ]   +   1 ; 
           q . emplace ( mb [ v ]); 
         } 
       } 
     } 
     return   succ ; 
   } 
   // Find an augmenting path starting at u. 
   bool   dfs ( int   u )   { 
     for   ( int   v   :   g [ u ])   { 
       if   ( mb [ v ]   ==   -1   ||   ( dist [ mb [ v ]]   ==   dist [ u ]   +   1   &&   dfs ( mb [ v ])))   { 
         ma [ u ]   =   v ; 
         mb [ v ]   =   u ; 
         return   true ; 
       } 
     } 
     dist [ u ]   =   -1 ;    // Mark this point as inreachable after one visit. 
     return   false ; 
   } 
   // Hopcroft-Karp maximum matching algorithm. 
   std :: vector < std :: pair < int ,   int >>   hopcroft_karp_maximum_matching ()   { 
     // Build the level graph and then find a blocking flow. 
     while   ( bfs ())   { 
       for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
         if   ( ma [ u ]   ==   -1 )   { 
           dfs ( u ); 
         } 
       } 
     } 
     // Collect the matched pairs. 
     std :: vector < std :: pair < int ,   int >>   matches ; 
     matches . reserve ( n1 ); 
     for   ( int   u   =   0 ;   u   <   n1 ;   ++ u )   { 
       if   ( ma [ u ]   !=   -1 )   { 
         matches . emplace_back ( u ,   ma [ u ]); 
       } 
     } 
     return   matches ; 
   } 
}; 
int   main ()   { 
   std :: ios :: sync_with_stdio ( false ),   std :: cin . tie ( nullptr ); 
   int   n1 ,   n2 ,   m ; 
   std :: cin   >>   n1   >>   n2   >>   m ; 
   BipartiteGraph   gr ( n1 ,   n2 ); 
   for   ( int   i   =   0 ;   i   <   m ;   ++ i )   { 
     int   u ,   v ; 
     std :: cin   >>   u   >>   v ; 
     gr . add_edge ( u ,   v ); 
   } 
   auto   res   =   gr . hopcroft_karp_maximum_matching (); 
   std :: cout   <<   res . size ()   <<   '\n' ; 
   for   ( int   i   =   0 ;   i   <   res . size ();   ++ i )   { 
     std :: cout   <<   res [ i ]. first   <<   ' '   <<   res [ i ]. second   <<   '\n' ; 
   } 
   return   0 ; 
} 
归约为最大流问题 二分图最大匹配问题可以归约为最大流问题。
如图所示,添加两个顶点分别作为源点和汇点。从源点出发,向每个左部点连接一条边;从每个右部点出发,向汇点连接一条边;并为二分图中的每条无向边,都连接一条从左部点指向右部点的边。所有边的容量都是 1 1 
任何可以解决最大流问题的算法都可以用于解决二分图最大匹配问题。容易发现,Kuhn 算法和 Hopcroft–Karp 算法都是最大流问题中相应算法的特例。同样地,预流推进算法  等同样可以用于解决二分图最大匹配问题。但是,应当注意的是,任何最大流算法,在应用于二分图最大匹配问题时,都需要有针对性地进行相应的优化,以避免过大的常数。
线性规划形式 和其他最大流问题一样,二分图 𝐺   = ( 𝑉 , 𝐸 ) G = ( V , E ) 𝑥 𝑒   ∈ { 0 , 1 } x e ∈ { 0 , 1 } 𝑒 e 
m a x { 𝑥 𝑒 } ∑ 𝑒 ∈ 𝐸 𝑥 𝑒 s u b j e c t   t o   ∑ 𝑒 ∼ 𝑣 𝑥 𝑒 ≤ 1 ,   ∀ 𝑣 ∈ 𝑉 , 𝑥 𝑒 ≥ 0 ,   ∀ 𝑒 ∈ 𝐸 . max { x e } ∑ e ∈ E x e subject to  ∑ e ∼ v x e ≤ 1 ,   ∀ v ∈ V , x e ≥ 0 ,   ∀ e ∈ E . 其中,𝑒   ∼ 𝑣 e ∼ v 𝑣 v 𝑒 e 𝑣   ∈ 𝑉 v ∈ V 
反过来却不然。在可行解中,𝑥 𝑒 x e 𝐺 G 
这一线性规划问题的对偶问题可以写作如下形式:
m i n { 𝑦 𝑣 } ∑ 𝑣 ∈ 𝑉 𝑦 𝑣 s u b j e c t   t o   𝑦 𝑢 + 𝑦 𝑣 ≥ 1 ,   ∀ ( 𝑢 , 𝑣 ) ∈ 𝐸 , 𝑦 𝑣 ≥ 0 ,   ∀ 𝑣 ∈ 𝑉 . min { y v } ∑ v ∈ V y v subject to  y u + y v ≥ 1 ,   ∀ ( u , v ) ∈ E , y v ≥ 0 ,   ∀ v ∈ V . 随后就会看到,这正是二分图的最小点覆盖问题。
Dulmage–Mendelsohn 分解 利用二分图的最大匹配,可以将顶点划分为若干互不相交的子集,从而完整刻画该二分图所有最大匹配的分布及结构特征。这就是 Dulmage–Mendelsohn 分解。算法竞赛中,利用这一分解,可以识别最大匹配中的关键点和关键边,进而判断最大匹配的唯一性或求解二分图博弈等问题。
构造方法 设二分图 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 𝑀 M 
如图所示,可以为所有顶点 𝑉   = 𝑋   ∪ 𝑌 V = X ∪ Y 
偶可达点 E E  奇可达点 O O  不可达点 U U  可以证明,这样得到的三个顶点集合 E , O , U E , O , U 
性质  集合 E , O , U E , O , U 𝑀 M  图 𝐺 G U U O O E E 𝐺 G | O |   + | U | / 2 | O | + | U | / 2  图 𝐺 G E E E   ∪ U E ∪ U  证明  按照定义,U U E   ∪ O E ∪ O E E O O 𝑣   ∈ E   ∩ O v ∈ E ∩ O 𝑎 a 𝑣 v 𝑏 b 𝑣 v 𝐺 G 𝑎   ≠ 𝑏 a ≠ b 𝑣 v 𝑎 a 𝑣 v 𝑏 b 𝑀 M E   ∩ O   = ∅ E ∩ O = ∅ 
 设 𝑀 ′ M ′ 𝑀 M Berge 引理的证明  可以说明,𝑀 ′   ⊕ 𝑀 M ′ ⊕ M 𝑀 M 𝑀 ′ M ′ E , O , U E , O , U 𝑀 M 
如果一条匹配边出现在某条从未匹配点 𝑣 v 𝑣 v E E O O U U E O E O U U U U E E O O U U | O |   + | U | / 2 | O | + | U | / 2 
按照定义,E E 𝑎 a 𝑣 v E E 𝑃 P 𝐺 G 𝑎 a E   ∪ U E ∪ U 𝑏 b 𝑃 P 𝑏 b O O 𝐺 G E E E   ∪ U E ∪ U  由此得到的顶点集合的分解 𝑉   = E   ∪ O   ∪ U V = E ∪ O ∪ U Dulmage–Mendelsohn 分解 。在利用前文所述算法求得最大匹配之后,可以通过 BFS 在 𝑂 ( | 𝑉 |   + | 𝐸 | ) O ( | V | + | E | ) 
最大匹配关键点 如果一个顶点 𝑣 v 𝐺 G 
定理  设二分图 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 𝑉   = E   ∪ O   ∪ U V = E ∪ O ∪ U 𝑣   ∈ 𝑉 v ∈ V 𝑣   ∈ O   ∪ U v ∈ O ∪ U 
证明  根据 Dulmage–Mendelsohn 分解的性质可知,在图 𝐺 G O O U U O   ∪ U O ∪ U E E 𝑀 M 𝑎   ∈ E a ∈ E 𝑃 P 𝑎 a 𝑏   ∈ E b ∈ E 𝑀   ⊕ 𝑃 M ⊕ P 𝑎 a E E 
因此,要求出最大匹配的关键点,只需要求出 Dulmage–Mendelsohn 分解即可。
最大匹配关键边 类似地,如果一条边 𝑒 e 𝐺 G 
定理  设二分图 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 𝑉   = E   ∪ O   ∪ U V = E ∪ O ∪ U 𝑀 M 𝑒   ∈ 𝐸 e ∈ E 𝑒 e U U 𝑒 e 𝑀 M 𝑀 M 𝑒 e 
证明  关键边的端点必须是关键点。根据 Dulmage–Mendelsohn 分解的性质,最大匹配的边只能是 E O E O U U U U E E U U U U 𝑀 M 𝑒   ∈ 𝑀 e ∈ M U U U U 𝑀 ′   ≠ 𝑀 M ′ ≠ M 𝑒   ∈ 𝑀   ⊕ 𝑀 ′ e ∈ M ⊕ M ′ Berge 引理的证明  可以说明,𝑀 ′   ⊕ 𝑀 M ′ ⊕ M 𝑀 M U U 𝑒 e 𝑒 e U U U U 𝑒   ∈ 𝑀 e ∈ M 𝑀 M 𝑒 e 
因此,要求出最大匹配的关键边,需要按照如下步骤进行:
求出图 𝐺 G 𝑀 M  按照 𝑀 M 𝐺 G 𝐺 𝑀 G M  进行 BFS 求出 Dulmage–Mendelsohn 分解中的集合 U U  利用 Tarjan 算法  求出有向图 𝐺 𝑀 G M  遍历匹配 𝑀 M U U  得到最大匹配后,后续步骤的时间复杂度为 𝑂 ( | 𝑉 |   + | 𝐸 | ) O ( | V | + | E | ) 
相关问题 利用二分图最大匹配的算法,可以解决其它组合优化问题。
二分图最小点覆盖 最小点覆盖问题是指,在一张无向图中选择最少的顶点,满足每条边至少有一个端点被选。
一般图的最小点覆盖问题是 NP 困难的,但是对于二分图,Kőnig 定理说明它可以归约为最大匹配问题,从而高效求解。定理的证明同时也给出了最小点覆盖的构造。
Kőnig 定理  二分图中,最小点覆盖中的顶点数量等于最大匹配中的边数量。
证明  设二分图 𝐺   = ( 𝑋 , 𝑌 , 𝐸 ) G = ( X , Y , E ) 𝑀 M 𝐺 G 𝑈 U 𝑍 Z 𝐶   = ( 𝑋   ∖ 𝑍 )   ∪ ( 𝑌   ∩ 𝑍 ) C = ( X ∖ Z ) ∪ ( Y ∩ Z ) 
 
 首先,集合 𝐶 C ( 𝑢 , 𝑣 )   ∈ 𝐸 ( u , v ) ∈ E 𝑢   ∈ 𝑋   ∩ 𝑍 u ∈ X ∩ Z 𝑣   ∈ 𝑌   ∖ 𝑍 v ∈ Y ∖ Z 𝑃 𝑢 P u 𝑢 u ( 𝑢 , 𝑣 ) ( u , v ) 𝑃 𝑢 P u ( 𝑣 , 𝑢 ) ( v , u ) 𝑣   ∉ 𝑍 v ∉ Z ( 𝑢 , 𝑣 ) ( u , v ) ( 𝑢 , 𝑣 ) ( u , v ) 𝑃 𝑢 P u 𝑣 v 𝑣   ∉ 𝑍 v ∉ Z 𝐶 C 𝐶 C 
 然后,需要说明 𝐶 C 𝑀 M | 𝑀 | | M | | 𝐶 |   = | 𝑀 | | C | = | M | 𝐶 C 𝐶 C 𝑣   ∈ 𝐶 v ∈ C 𝑣   ∈ 𝑋 v ∈ X 𝑣   ∈ 𝑈   ⊆ 𝑍 v ∈ U ⊆ Z 𝐶 C 𝑣   ∈ 𝑌 v ∈ Y 𝑣 v 𝑀 M 𝑀 M 𝐶 C 
从网络流的角度看,最小点覆盖问题就是最小割问题:选择左部点,相当于切割它与源点的连边;选择右部点,相当于切割它与汇点的连边。从线性规划的角度看,最小点覆盖问题就是最大匹配问题的对偶问题。因此,König 定理可以看作是 最大流最小割定理  的特殊情形,或者更一般地,线性规划的强对偶定理的特殊情形。
二分图最大独立集 最大独立集问题是指,在一张无向图中选择最多的顶点,满足两两之间互不相邻。
对于一般图,成立如下定理:
定理  图 𝐺   = ( 𝑉 , 𝐸 ) G = ( V , E ) 𝐶   ⊆ 𝑉 C ⊆ V 𝑉   ∖ 𝐶 V ∖ C 
证明  点集 𝐶 C 𝐸 E 𝑒 e 𝐶 C 𝐸 E 𝑉   ∖ 𝐶 V ∖ C 𝑉   ∖ 𝐶 V ∖ C 
推论  图 𝐺   = ( 𝑉 , 𝐸 ) G = ( V , E ) 
因此,与最小点覆盖问题一样,最大独立集问题对于一般图是 NP 困难的,但是对于二分图它可以归约为最大匹配问题,从而高效求解。
有向无环图最小路径覆盖 最小路径覆盖问题是指,在一张有向图中,选择最少数量的简单路径,使得所有顶点都恰好出现在一条路径中。
一般的有向图上的最小路径覆盖问题是 NP 困难的,但是对于有向无环图,该问题可以归约为二分图最大匹配问题。对于有向无环图 𝐺   = ( 𝑉 , 𝐸 ) G = ( V , E ) 𝐺 ′   = ( 𝑉 i n , 𝑉 o u t , 𝐸 ′ ) G ′ = ( V in , V out , E ′ ) 
为每个顶点 𝑣   ∈ 𝑉 v ∈ V 𝑣 i n v in 𝑣 o u t v out 𝑉 i n V in 𝑉 o u t V out  为每条有向边 ( 𝑢 , 𝑣 )   ∈ 𝐸 ( u , v ) ∈ E ( 𝑢 o u t , 𝑣 i n ) ( u out , v in ) 𝐸 ′ E ′  为此,有如下结论:
定理  有向无环图 𝐺   = ( 𝑉 , 𝐸 ) G = ( V , E ) 𝐺 ′   = ( 𝑉 i n , 𝑉 o u t , 𝐸 ′ ) G ′ = ( V in , V out , E ′ ) 
证明  二分图 𝐺 ′ G ′ 𝑀 ′ M ′ 𝐺 G 𝐹 F 𝐹 F 𝐹 F 𝐺 G 𝐺 G 𝐹 F 𝐹 F 𝑀 M 𝐹 F 𝐺 G 𝐺 ′ G ′ 
证明是构造性的,因此,很容易根据得到的最大匹配构造出相应的最小路径覆盖。而且,这个构造说明,对于一般的有向图,这个归约不再成立,正是因为二分图中的匹配可能对应着有向图中的环。
特别地,对于集合 𝑋 X 𝑃 P 𝐺   = ( 𝑋 , 𝑃 ) G = ( X , P ) Dilworth 定理 ,图 𝐺 G ( 𝑋 , 𝑃 ) ( X , P ) 
例题 应用二分图匹配的难点在于建图,本节通过一些例题展示建图的技巧。
Luogu P1129 矩阵游戏 有一个 01 方阵,每一次可以交换两行或两列,问是否可以交换使得主对角线(左上到右下)全都是 1。
解法  注意到,当存在 𝑛 n 1 1 1 1 𝑛 n 1 1 
 考虑对于一个 1 1 1 1 1 1 𝑛 n 𝑛 n 1 1 
代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 #include   <algorithm> 
#include   <iostream> 
#include   <queue> 
#include   <vector> 
using   namespace   std ; 
int   ct ,   n ,   t [ 100010 ],   x ,   r [ 100010 ],   ans ,   vis [ 100010 ],   dis [ 100010 ]; 
vector < int >   to [ 100010 ]; 
queue < int >   q ; 
int   DFS ( int   x )   { 
   if   ( vis [ x ])   { 
     return   0 ; 
   } 
   vis [ x ]   =   1 ; 
   for   ( auto   i   :   to [ x ])   { 
     if   ( ! r [ i ])   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
     if   ( dis [ r [ i ]]   ==   dis [ x ]   +   1   &&   DFS ( r [ i ]))   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
   } 
   return   0 ; 
} 
int   BFS ()   { 
   fill ( vis   +   1 ,   vis   +   n   +   1 ,   0 ); 
   fill ( dis   +   1 ,   dis   +   n   +   1 ,   0 ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     if   ( ! t [ i ])   { 
       q . push ( i ); 
       dis [ i ]   =   1 ; 
     } 
   } 
   int   f   =   0 ; 
   for   (;   q . size ();   q . pop ())   { 
     int   tmp   =   q . front (); 
     for   ( auto   i   :   to [ tmp ])   { 
       if   ( ! r [ i ])   { 
         f   =   1 ; 
       } 
       if   ( r [ i ])   { 
         if   ( ! dis [ r [ i ]])   { 
           dis [ r [ i ]]   =   dis [ tmp ]   +   1 ; 
           q . push ( r [ i ]); 
         } 
       } 
     } 
   } 
   return   f ; 
} 
int   main ()   { 
   ios :: sync_with_stdio ( false ); 
   cin . tie ( nullptr ),   cout . tie ( nullptr ); 
   for   ( cin   >>   ct ;   ct -- ;)   { 
     cin   >>   n ; 
     for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
       for   ( int   j   =   1 ;   j   <=   n ;   j ++ )   { 
         cin   >>   x ; 
         if   ( x )   { 
           to [ i ]. push_back ( j ); 
         } 
       } 
     } 
     for   (;   BFS ();)   { 
       for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
         if   ( ! t [ i ]   &&   DFS ( i ))   { 
           ans ++ ; 
         } 
       } 
     } 
     cout   <<   ( ans   ==   n   ?   "Yes"   :   "No" )   <<   '\n' ; 
     for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
       t [ i ]   =   r [ i ]   =   0 ; 
       to [ i ]. clear (); 
     } 
     ans   =   0 ; 
   } 
   return   0 ; 
} 
Gym 104427B Lawyers 有 𝑛 n 𝑛 n 𝑚 m ( 𝑎 , 𝑏 ) ( a , b ) 𝑎 a 𝑏 b 𝑎 a 𝑏 b 
 求是否可以使得每一名律师都被释放。
解法  对于每一个 无序对  ( 𝑎 , 𝑏 ) ( a , b ) 𝑎 a 𝑏 b 𝑏 b 
 只保存有边相连的 ( 𝑎 , 𝑏 ) ( a , b ) 𝑚 m 𝑛 n 
代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 #include   <algorithm> 
#include   <iostream> 
#include   <map> 
#include   <queue> 
#include   <utility> 
#include   <vector> 
using   namespace   std ; 
int   n ,   k ,   u ,   v ,   t [ 200020 ],   r [ 200020 ],   ans ,   vis [ 200020 ],   dis [ 200020 ],   c ; 
map < pair < int ,   int > ,   int >   mp ; 
vector < int >   to [ 200020 ]; 
queue < int >   q ; 
int   DFS ( int   x )   { 
   if   ( vis [ x ])   { 
     return   0 ; 
   } 
   vis [ x ]   =   1 ; 
   for   ( auto   i   :   to [ x ])   { 
     if   ( ! r [ i ])   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
     if   ( dis [ r [ i ]]   ==   dis [ x ]   +   1   &&   DFS ( r [ i ]))   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
   } 
   return   0 ; 
} 
int   BFS ()   { 
   fill ( vis   +   1 ,   vis   +   n   +   1 ,   0 ); 
   fill ( dis   +   1 ,   dis   +   n   +   1 ,   0 ); 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     if   ( ! t [ i ])   { 
       q . push ( i ); 
       dis [ i ]   =   1 ; 
     } 
   } 
   int   f   =   0 ; 
   for   (;   q . size ();   q . pop ())   { 
     int   tmp   =   q . front (); 
     for   ( auto   i   :   to [ tmp ])   { 
       if   ( ! r [ i ])   { 
         f   =   1 ; 
       } 
       if   ( r [ i ])   { 
         if   ( ! dis [ r [ i ]])   { 
           dis [ r [ i ]]   =   dis [ tmp ]   +   1 ; 
           q . push ( r [ i ]); 
         } 
       } 
     } 
   } 
   return   f ; 
} 
void   mxf ()   { 
   for   (;   BFS ();)   { 
     for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
       if   ( ! t [ i ]   &&   DFS ( i ))   { 
         ans ++ ; 
       } 
     } 
   } 
} 
int   main ()   { 
   ios :: sync_with_stdio ( false ); 
   cin . tie ( nullptr ),   cout . tie ( nullptr ); 
   cin   >>   n   >>   k ; 
   for   ( int   i   =   1 ;   i   <=   k ;   i ++ )   { 
     cin   >>   u   >>   v ; 
     if   ( mp . find ({ u ,   v })   ==   mp . end ())   { 
       mp [{ u ,   v }]   =   mp [{ v ,   u }]   =   ++ c ; 
     } 
     to [ v ]. push_back ({ mp [{ u ,   v }]}); 
   } 
   mxf (); 
   cout   <<   ( ans   ==   n   ?   "YES"   :   "NO" )   <<   '\n' ; 
   ans   =   0 ; 
   return   0 ; 
} 
Codeforces 1404E Bricks 用一些 1   × 𝑥 1 × x 𝑛   × 𝑚 n × m 
解法  考虑最终的方案是如何构成的:
 先在所有能覆盖的网格上全部铺上 1   × 1 1 × 1 1   × 𝑥 1 × x 𝑥 x 1   × 1 1 × 1 𝑥   × 1 x × 1 𝑥 x 1   × 1 1 × 1 
 显然,一次行合并和一次列合并不能干涉到同一个砖,而且合并的次数越多,砖块数量越少。于是,可以以行合并作为左部点,列合并作为右部点,以前面的冲突作为边,建出一个二分图。随即原问题变成了一个二分图最大独立集问题。
代码    1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 #include   <algorithm> 
#include   <iostream> 
#include   <queue> 
#include   <vector> 
using   namespace   std ; 
int   n ,   m ,   belr [ 220 ][ 220 ],   beld [ 220 ][ 220 ],   dcnt ,   rcnt ,   vis [ 100010 ],   dis [ 100010 ], 
     t [ 100010 ],   r [ 100010 ],   cnt ; 
char   c [ 220 ][ 220 ]; 
vector < int >   to [ 100010 ]; 
queue < int >   q ; 
int   DFS ( int   x )   { 
   if   ( vis [ x ])   { 
     return   0 ; 
   } 
   vis [ x ]   =   1 ; 
   for   ( auto   i   :   to [ x ])   { 
     if   ( ! r [ i ])   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
     if   ( dis [ r [ i ]]   ==   dis [ x ]   +   1   &&   DFS ( r [ i ]))   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
   } 
   return   0 ; 
} 
int   BFS ()   { 
   fill ( vis   +   1 ,   vis   +   rcnt   +   1 ,   0 ); 
   fill ( dis   +   1 ,   dis   +   rcnt   +   1 ,   0 ); 
   for   ( int   i   =   1 ;   i   <=   rcnt ;   i ++ )   { 
     if   ( ! t [ i ])   { 
       q . push ( i ); 
       dis [ i ]   =   1 ; 
     } 
   } 
   int   f   =   0 ; 
   for   (;   q . size ();   q . pop ())   { 
     int   tmp   =   q . front (); 
     for   ( auto   i   :   to [ tmp ])   { 
       if   ( ! r [ i ])   { 
         f   =   1 ; 
       } 
       if   ( r [ i ])   { 
         if   ( ! dis [ r [ i ]])   { 
           dis [ r [ i ]]   =   dis [ tmp ]   +   1 ; 
           q . push ( r [ i ]); 
         } 
       } 
     } 
   } 
   return   f ; 
} 
int   solve ()   { 
   int   rt   =   0 ; 
   for   (;   BFS ();)   { 
     for   ( int   i   =   1 ;   i   <=   rcnt ;   i ++ )   { 
       if   ( ! t [ i ]   &&   DFS ( i ))   { 
         rt ++ ; 
       } 
     } 
   } 
   return   rt ; 
} 
int   main ()   { 
   ios :: sync_with_stdio ( false ); 
   cin . tie ( nullptr ),   cout . tie ( nullptr ); 
   cin   >>   n   >>   m ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     for   ( int   j   =   1 ;   j   <=   m ;   j ++ )   { 
       cin   >>   c [ i ][ j ]; 
     } 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     for   ( int   j   =   1 ;   j   <=   m ;   j ++ )   { 
       if   ( c [ i ][ j ]   ==   '#' )   { 
         cnt ++ ; 
         if   ( c [ i   +   1 ][ j ]   ==   '#' )   { 
           beld [ i ][ j ]   =   ++ dcnt ; 
         } 
         if   ( c [ i ][ j   +   1 ]   ==   '#' )   { 
           belr [ i ][ j ]   =   ++ rcnt ; 
         } 
       } 
     } 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     for   ( int   j   =   1 ;   j   <=   m ;   j ++ )   { 
       if   ( c [ i ][ j ]   ==   '#' )   { 
         if   ( c [ i   -   1 ][ j ]   ==   '#'   &&   c [ i ][ j   -   1 ]   ==   '#' )   { 
           to [ belr [ i ][ j   -   1 ]]. push_back ( beld [ i   -   1 ][ j ]); 
         } 
         if   ( c [ i   +   1 ][ j ]   ==   '#'   &&   c [ i ][ j   +   1 ]   ==   '#' )   { 
           to [ belr [ i ][ j ]]. push_back ( beld [ i ][ j ]); 
         } 
         if   ( c [ i   +   1 ][ j ]   ==   '#'   &&   c [ i ][ j   -   1 ]   ==   '#' )   { 
           to [ belr [ i ][ j   -   1 ]]. push_back ( beld [ i ][ j ]); 
         } 
         if   ( c [ i   -   1 ][ j ]   ==   '#'   &&   c [ i ][ j   +   1 ]   ==   '#' )   { 
           to [ belr [ i ][ j ]]. push_back ( beld [ i   -   1 ][ j ]); 
         } 
       } 
     } 
   } 
   cout   <<   cnt   -   rcnt   -   dcnt   +   solve ()   <<   '\n' ; 
   return   0 ; 
} 
Codeforces 1139E - Maximize Mex 有 𝑚 m 𝑛 n m e x mex 
解法  先考虑如果没有删除元素时怎么做。
 对于每一个多重集,开一个新点;对于每一个可能的答案,开一个新点。然后,对于某一个对应点 𝑙 𝑖 l i 𝑎 a 𝑙 𝑖 l i 𝑟 𝑎 r a 
 现在加回来删除元素的操作,发现根本搞不了:删了一条边可能引起匹配的巨变,复杂度无法接受。于是,不如反过来,我们每一次加一条边,然后顺过去重新增广。所以本题只能使用 Kuhn 算法。
代码   1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 #include   <algorithm> 
#include   <iostream> 
#include   <vector> 
using   namespace   std ; 
struct   node   { 
   int   u ,   v ; 
}   arr [ 5050 ]; 
int   n ,   m ,   t [ 5050 ],   r [ 5050 ],   d ,   ot [ 5050 ],   kil [ 5050 ],   vis [ 5050 ],   ans ,   out [ 5050 ]; 
vector < int >   to [ 5050 ]; 
int   DFS ( int   x )   { 
   if   ( vis [ x ])   { 
     return   0 ; 
   } 
   vis [ x ]   =   1 ; 
   for   ( auto   i   :   to [ x ])   { 
     if   ( r [ i ]   ==   -1 )   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
     if   ( DFS ( r [ i ]))   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   1 ; 
     } 
   } 
   return   0 ; 
} 
int   main ()   { 
   cin   >>   n   >>   m ; 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     cin   >>   arr [ i ]. u ; 
     if   ( arr [ i ]. u   >   m )   { 
       arr [ i ]. u   =   m   +   1 ; 
     } 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     cin   >>   arr [ i ]. v ; 
   } 
   cin   >>   d ; 
   for   ( int   i   =   1 ;   i   <=   d ;   i ++ )   { 
     cin   >>   ot [ i ]; 
     kil [ ot [ i ]]   =   1 ; 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     if   ( ! kil [ i ])   { 
       to [ arr [ i ]. u ]. push_back ( arr [ i ]. v ); 
     } 
   } 
   fill ( r   +   1 ,   r   +   m   +   1 ,   -1 ); 
   for   (;   ans   <=   m ;)   { 
     fill ( vis ,   vis   +   m   +   2 ,   0 ); 
     if   ( DFS ( ans ))   { 
       ans ++ ; 
     }   else   { 
       break ; 
     } 
   } 
   out [ d ]   =   ans ; 
   for   ( int   i   =   d ;   i   >   1 ;   i -- )   { 
     to [ arr [ ot [ i ]]. u ]. push_back ( arr [ ot [ i ]]. v ); 
     for   (;   ans   <=   m ;)   { 
       fill ( vis ,   vis   +   m   +   2 ,   0 ); 
       if   ( DFS ( ans ))   { 
         ans ++ ; 
       }   else   { 
         break ; 
       } 
     } 
     out [ i   -   1 ]   =   ans ; 
   } 
   for   ( int   i   =   1 ;   i   <=   d ;   i ++ )   { 
     cout   <<   out [ i ]   <<   '\n' ; 
   } 
   return   0 ; 
} 
Luogu P3355 - 骑士共存问题 有一个 𝑛   × 𝑛 n × n 
解法  可以发现,如果对整个棋盘染色使得所有黑格、白格均不相邻,那么马只能够攻击到与其异色的格子。
 然后就可以直接二分图最大独立集了。
代码    1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 
127 
128 #include   <algorithm> 
#include   <iostream> 
#include   <queue> 
#include   <vector> 
using   namespace   std ; 
const   int   kD [ 8 ][ 2 ]   =   {{ -2 ,   -1 },   { -1 ,   -2 },   { 1 ,   -2 },   { 2 ,   -1 }, 
                       { 2 ,   1 },     { 1 ,   2 },     { -1 ,   2 },   { -2 ,   1 }}; 
int   n ,   m ,   ib ; 
int   vis [ 200020 ],   dis [ 200020 ],   t [ 200020 ],   r [ 200020 ],   ans ,   u ,   v ,   cnta , 
     x [ 220 ][ 220 ],   cl [ 220 ][ 220 ],   idx ,   bel [ 220 ][ 220 ]; 
vector < int >   to [ 200020 ]; 
queue < int >   q ; 
int   DFS ( int   x )   { 
   if   ( vis [ x ])   { 
     return   false ; 
   } 
   vis [ x ]   =   true ; 
   for   ( auto   i   :   to [ x ])   { 
     if   ( ! r [ i ])   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   true ; 
     } 
     if   ( dis [ r [ i ]]   ==   dis [ x ]   +   1   &&   DFS ( r [ i ]))   { 
       r [ i ]   =   x ; 
       t [ x ]   =   i ; 
       return   true ; 
     } 
   } 
   return   false ; 
} 
int   BFS ()   { 
   fill ( vis   +   1 ,   vis   +   cnta   +   1 ,   false ); 
   fill ( dis   +   1 ,   dis   +   cnta   +   1 ,   0 ); 
   for   ( int   i   =   1 ;   i   <=   cnta ;   i ++ )   { 
     if   ( ! t [ i ])   { 
       q . push ( i ); 
       dis [ i ]   =   1 ; 
     } 
   } 
   int   f   =   0 ; 
   for   (;   q . size ();   q . pop ())   { 
     int   tmp   =   q . front (); 
     for   ( auto   i   :   to [ tmp ])   { 
       if   ( ! r [ i ])   { 
         f   =   1 ; 
       } 
       if   ( r [ i ])   { 
         if   ( ! dis [ r [ i ]])   { 
           dis [ r [ i ]]   =   dis [ tmp ]   +   1 ; 
           q . push ( r [ i ]); 
         } 
       } 
     } 
   } 
   return   f ; 
} 
void   din ()   { 
   for   (;   BFS ();)   { 
     for   ( int   i   =   1 ;   i   <=   cnta ;   i ++ )   { 
       if   ( ! t [ i ]   &&   DFS ( i ))   { 
         ans ++ ; 
       } 
     } 
   } 
} 
int   main ()   { 
   ios :: sync_with_stdio ( false ); 
   cin . tie ( nullptr ),   cout . tie ( nullptr ); 
   cin   >>   n   >>   m ; 
   for   ( int   i   =   1 ;   i   <=   m ;   i ++ )   { 
     cin   >>   u   >>   v ; 
     x [ u ][ v ]   =   true ; 
   } 
   cl [ 1 ][ 1 ]   =   1 ; 
   if   ( ! x [ 1 ][ 1 ])   { 
     bel [ 1 ][ 1 ]   =   ++ idx ; 
   } 
   for   ( int   i   =   2 ;   i   <=   n ;   i ++ )   { 
     cl [ 1 ][ i ]   =   cl [ 1 ][ i   -   1 ]   ^   1 ; 
     if   ( ! x [ 1 ][ i ]   &&   cl [ 1 ][ i ])   { 
       bel [ 1 ][ i ]   =   ++ idx ; 
     }   else   { 
       if   ( ! x [ 1 ][ i ])   { 
         bel [ 1 ][ i ]   =   ++ ib ; 
       } 
     } 
   } 
   for   ( int   i   =   2 ;   i   <=   n ;   i ++ )   { 
     for   ( int   j   =   1 ;   j   <=   n ;   j ++ )   { 
       cl [ i ][ j ]   =   cl [ i   -   1 ][ j ]   ^   1 ; 
       if   ( ! x [ i ][ j ]   &&   cl [ i ][ j ])   { 
         bel [ i ][ j ]   =   ++ idx ; 
       }   else   { 
         if   ( ! x [ i ][ j ])   { 
           bel [ i ][ j ]   =   ++ ib ; 
         } 
       } 
     } 
   } 
   for   ( int   i   =   1 ;   i   <=   n ;   i ++ )   { 
     for   ( int   j   =   1 ;   j   <=   n ;   j ++ )   { 
       if   ( ! x [ i ][ j ])   { 
         cnta   +=   cl [ i ][ j ]; 
         for   ( int   k   =   0 ;   k   <   8 ;   k ++ )   { 
           if   ( i   +   kD [ k ][ 0 ]   >   0   &&   j   +   kD [ k ][ 1 ]   >   0   && 
               bel [ i   +   kD [ k ][ 0 ]][ j   +   kD [ k ][ 1 ]]   && 
               ! x [ i   +   kD [ k ][ 0 ]][ j   +   kD [ k ][ 1 ]])   { 
             if   ( cl [ i   +   kD [ k ][ 0 ]][ j   +   kD [ k ][ 1 ]])   { 
               to [ bel [ i   +   kD [ k ][ 0 ]][ j   +   kD [ k ][ 1 ]]]. push_back ( bel [ i ][ j ]); 
             }   else   { 
               to [ bel [ i ][ j ]]. push_back ( bel [ i   +   kD [ k ][ 0 ]][ j   +   kD [ k ][ 1 ]]); 
             } 
           } 
         } 
       } 
     } 
   } 
   din (); 
   cout   <<   n   *   n   -   m   -   ans   <<   '\n' ; 
   return   0 ; 
} 
习题 参考资料 2025/10/13 16:54:57 ,更新历史 在 GitHub 上编辑此页! StudyingFather , countercurrent-time , Enter-tainer , H-J-Granger , c-forrest , NachtgeistW , Tiphereth-A , CCXXXI , Early0v0 , thallium , 310552025atNYCU , 5ab-juruo , AngelKitty , Chrogeek , cjsoft , diauweb , ezoixx130 , GCVillager , GekkaSaori , Henry-ZHR , hhc0001 , Ir1d , Konano , ksyx , LovelyBuggies , Makkiy , mgt , minghu6 , P-Y-Y , PotassiumWings , SamZhangQingChuan , sshwy , Suyun514 , weiyong1024 , william-song-shy , Xeonacid , accelsao , billchenchina , GavinZhengOI , Gesrua , Great-designer , Haohu Shen , HeRaNO , iamtwz , kxccc , lychees , ouuan , Peanut-Tang , SukkaW , TianKong-y , XiaoQuQuSD CC BY-SA 4.0  和 SATA