初见 | 图论 | 二分图初步
前言
数据结构学累了,于是决定回归图论一下,大概的目标就先是二分图,然后过网络流初步。
缺省源
因为挺长的所有下面就不再放了罢。
#include <iostream>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <string.h>
#define Heriko return
#define Deltana 0
#define Romano 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false)
using namespace std;
template<typename J>
I void fr(J &x)
{
short f=1;
char c=getchar();
x=0;
while(c<'0' or c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while (c>='0' and c<='9')
{
x=(x<<3)+(x<<1)+c-'0';
c=getchar();
}
x*=f;
}
template<typename J>
I void fw(J x,bool k)
{
if(x<0) putchar('-'),x=-x;
static short stak[35];
short top=0;
do
{
stak[top++]=x%10;
x/=10;
}
while(x);
while(top) putchar(stak[--top]+'0');
if(k) putchar('\n');
else putchar(' ');
}
二分图
什么是二分图?
实际上蓝书上的定义看的我挺晕的,这里就采用 OI-Wiki 上的解释:
节点由两个集合组成,且两个集合内部没有边的图。[1]
换句话说,即:
如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。[2]
这里给出一张图来帮助理解一下~
两个框框圈起来的点就是上面所说的两个集合啦(
可以清晰的发现,即使两个集合的点之间有很多边,但是每个集合内部的点都是互相没有连边哒。
如果还是不能理解的话,先大约记住这张图的样子,到了下面的判定部分就会让你恍然大明白。(因为我就是这样ww)
如何判定二分图?
前置芝士
我们先来介绍一个二分图性质,因为这对我们下面的判定非常重要:
二分图不存在长度为奇数的环。[1]
于是我们用性质就反过来判定,即:
存在长度为奇数的环的图一定不是二分图。
不过要注意一下,这里说的长度是构成一个环的边数和,
如果边有不同的权值,那么我们称这些值的和为这个环的权值和,别搞混了。
当然你也可以理解为这里的边都默认长度为 1 ~
这条性质的证明太显然了不会描述 QwQ,不过为了帮助理解,这里就简单口胡一下罢。
口胡
大约是反证的思想(?)
我们假设我们有一个二分图,并且已经分为了两个集合。
假设每一条边都是在两个集合之间的,且完全符合二分图的定义(两个集合内部没有边),并且我们有长度为奇数的环。
然后我们在这个奇数环上任选一个结点开遍历这张图,考虑如何才能回到这个起始集合。
显然的是我们走偶数次才能够回到原来的集合(或者说是回到起始的点),但是这个环的长度是奇数,因此我们是走不回原来的集合的。
那么要不然这个图不是二分图,要不然这不是环......
于是我们推出了矛盾,口胡毕。
然后我们就可以愉快的判定了(
具体实现
一个显而易见的暴力做法是枚举答案集合,但也很显然的是这玩意复杂度爆炸,于是我们考虑 DFS。
我们利用刚才的性质,加以染色的思想进行 DFS:
假如我们在 DFS 染色的过程中遇不到矛盾的情况,那么这就是一个二分图;反之此图有奇数环,不是二分图。
在这里演示的时候我们使用蓝色和红色来对结点进行染色。
过程演示
我们先选取一个结点对他进行染色:
然后我们对和它相连的结点涂上相反的颜色,目前还没有矛盾:
然后对下一个同集合的结点进行同样的染色过程:
发现 DFS 到的是个新的点,染上不同的颜色:
重复上面的操作,发现这是一个二分图(即没有矛盾):
有矛盾的图就不放了(因为我不想画了ww)
最后用动图再展示一边全过程罢:
Code
有了上面的图,代码就不难理解了,这个 DFS 的时间复杂度是 \(O(nm)\) 的。
bool DFS(int x,int c)
{
color[x]=c;
for(int i=head[x];i;i=r[i].nex)//邻接表存图。
{
int y=r[i].to;
if(!color[y])
{
if(!DFS(y,(c==1?2:1))) Heriko Deltana;//这里 1 代表蓝色,2代表红色。
}
else if(color[y]==color[x]) return 0;
}
return 1;
}
这里推荐一个例题,虽然不是很板子。
P1330 封锁阳光大学
P1330 封锁阳光大学 [普及+/提高]
思路简述
这道题的数据如果是个联通的无向图,那么可以直接用二分图的判定 DFS 染色来做,但是数据并没有保证图是联通的。
但是我们可以仍然去参考染色的思想,实际上 DFS 根本不用改太多,只不过每次染色的时候加上当前是染了什么颜色即可。
然后每次 ans
加上两个颜色中比较小的那个即可(因为我们要最少的河蟹)。
(以上摘自我的杂题录 [简单记录 | #99] )
Code
template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}
CI MXX=1e5+5;
int n,m;
struct node
{
int to,nex;
}
r[MXX];
int head[MXX],cnt,ans;
int color[MXX],c1,c2;
I void add(int x,int y)
{
r[++cnt].to=y;
r[cnt].nex=head[x];
head[x]=cnt;
}
bool DFS(int x,int c)
{
color[x]=c;
if(color[x]==1) ++c1;
else ++c2;
for(R int i=head[x];i;i=r[i].nex)
{
int y=r[i].to;
if(!color[y])
{if(!DFS(y,(c==1?2:1))) Heriko Deltana;}
else if(color[y]==color[x]) Heriko Deltana;
}
Heriko Romano;
}
bool flag;
int u,v;
S main()
{
fr(n),fr(m);
for(R int i=1;i<=m;++i)
{
fr(u),fr(v);
add(u,v);
add(v,u);
}
for(R int i=1;i<=n;++i)
{
if(!color[i])
{
c1=c2=0;
if(!DFS(i,1))
{
puts("Impossible");
Heriko Deltana;
}
ans+=Hmin(c1,c2);
}
}
fw(ans,1);
Heriko Deltana;
}
二分图最大匹配
前备芝士
为了理解二分图最大匹配,我们需要先定义一些东西。
匹配
在图论中,一个「匹配」(matching)是一个边的集合 \(S\),其中任意两条边都没有公共顶点。例如,图 3、图 4 中红色的边就是图 2 的匹配。[2]
匹配边 & 匹配点
所有的边 \(u\in S\),就是匹配边;同理,所有匹配边的端点即为匹配点。
举个栗子,对于图 4,红边都是对于图 2 的匹配边,而红边所连的结点都是匹配点。
非匹配边 & 非匹配点
所有不是匹配边的边是非匹配边;同理,不是匹配点的都是非匹配点。
一句非常废话的废话,但是感觉我不管怎么描述都是说废话(
最大匹配
一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 4 是一个最大匹配,它包含 4 条匹配边。[2]
增广路
如果在二分图中存在一条连接两个非匹配点的路径 \(path\),使得非匹配边与匹配边在 \(path\) 上交替出现,那么称 \(path\) 是匹配 \(S\) 的一条增广路,也称交错路。[3]
增广路显然是有如下的性质:
-
长度为奇数。
-
第 \(1,3,5,\cdots,len\) 条边是非匹配边,反之则都是匹配边。
由性质 2 可得,当我们把所有的边的状态取反之后,这仍是一组匹配,并且匹配的边数++,于是我们可以想到利用这个性质去找最大的匹配。
于是就有推论:
二分图的一组匹配 \(S\) 是最大匹配,当且仅当图中不存在 \(S\) 的增广路。[3]
然后我们就可以引出我们求二分图最大匹配的算法。
匈牙利算法
因为利用了上面那个推论,所以又被称为增广路算法。
思路简述
这个算法就是利用上面的推论和性质来不断的接近最大匹配,主要过程如下:
-
设 \(S\) 为空集,即现在图上的所有边都为非匹配边。
-
寻找增广路 \(path\),然后把路径上的边的状态取反,得到一个更大的匹配 \(S'\)。
-
重复步骤 2 直到图上不存在增广路。
其实找增广路的算法本质上很暴力:DFS 每个结点,不断尝试,如果存在更优的解法就改变当前的决策。
匈牙利算法的正确性是基于贪心的,这个贪心算法的重要特性是:在一次寻找增广路的 DFS 过程中,当一个结点成为匹配点后,它最多会因为后面寻找增广路时被更改匹配对象,即:
如果某个选择已经被搭配,就尝试将原有搭配拆散重新搭配,直到找到第一个可行的搭配。
下面放上一张 \(\texttt{Dfkuaid}\) 的图来大约描述一下这个过程:
Code
是板子题的 Code 呢:[ 洛谷 P3386 ]
CI MXX=5e4+5,NXX=1005;
struct node
{
int to,nex;
}
r[MXX];
int head[NXX],ans,match[NXX],cnt,y;
bool vis[NXX];
I void add(int x,int y)
{
r[++cnt].to=y;
r[cnt].nex=head[x];
head[x]=cnt;
}
bool DFS(int x)
{
for(R int i=head[x];i;i=r[i].nex)
if(!vis[y=r[i].to])
{
vis[y]=1;
if(!match[y] or DFS(match[y]))
{
match[y]=x;
Heriko Romano;
}
}
Heriko Deltana;
}
int n,m,e,u,v;
S main()
{
fr(n),fr(m),fr(e);
for(R int i=1;i<=e;++i)
{
fr(u),fr(v);
add(u,v);
}
for(R int i=1;i<=n;++i)
{
mst(vis,0);
if(DFS(i)) ++ans;
}
fw(ans,1);
Heriko Deltana;
}
转为网络最大流模型
爬了,还不会网络流,学完回来填个坑 🕳 罢。
来填坑啦~
二分图匹配建模
二分图匹配的建模有以下两个要素:
-
“0 要素”:结点能分为两个独立的集合,且每个集合内有 0 条边。(也就是满足这是一个二分图)
-
“1 要素”:每个结点只能与 1 条匹配边相连
下面给出一道例题:
P1129 矩阵游戏 [ZJOI2007]
P1129 矩阵游戏 [ZJOI2007][提高+/省选-]
思路简述
是一道显然的二分图最大匹配!考虑到我们每次能够调换的是行和列,于是我们在输入时遇到 1 时就把 i 和 j+n 建边,然后正常的跑匈牙利,最后判断一下 ans>=n
是否成立即可。
(以上摘自我的杂题录 [简单记录 | #100] )
Code
CI MXX=20005;
struct node
{
int nex,to;
}
r[MXX<<1];
int T,n,head[MXX],cnt,match[MXX];
I void add(int x,int y)
{
r[++cnt].to=y;
r[cnt].nex=head[x];
head[x]=cnt;
}
bool vis[MXX];
bool DFS(int x)
{
for(R int i=head[x];i;i=r[i].nex)
{
int y=r[i].to;
if(!vis[y])
{
vis[y]=1;
if(!match[y] or DFS(match[y]))
{
match[y]=x;
Heriko Romano;
}
}
}
Heriko Deltana;
}
int x,ans;
S main()
{
fr(T);
while(T--)
{
mst(head,0);cnt=0; mst(match,0);ans=0;
fr(n);
for(R int i=1;i<=n;++i)
for(R int j=1;j<=n;++j)
{
fr(x);
if(x) add(i,j+n);
}
for(R int i=1;i<=n;++i)
{
mst(vis,0);
if(DFS(i)) ++ans;
}
if(ans>=n) puts("Yes");
else puts("No");
}
Heriko Deltana;
}
End
芜湖,写完了,欢迎各位大佬纠错捉虫!
参考资料
-
[1] 二分图 —— OI-Wiki
-
[2] 二分图的最大匹配、完美匹配和匈牙利算法 —— Renfei Song
-
[3] 《算法进阶指南》0x68 二分图匹配 —— 李煜东
-
[4] [ 图论入门 ] 二分图简析 —— Dfkuaid