二分图

前言:该笔记为笔者在此及以前的所有笔记中最详尽也最为干货的一篇了,共经一周的反复修改及编辑而成。

二分图基础知识

  • 二分图判定:

inline bool dfs(ll u,ll col) {
	color[u]=col;
	for (ll i=0;i<G[u].size();++i) {
		ll v=G[u][i];
		if (!color[v]&&!dfs(v,3-col)) return false;
		else if (color[v]==col) return false;
	}
	return true;
}

其中 $col\in [1,2] ,col\in Z $。

注意判断图是否连通。

  • 二分图最大匹配:

匈牙利算法:

本质为不断寻找增广路的过程。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=5e4+10;
ll n,m,e,ans;
vector<ll> G[maxn];
ll match[maxn],vis[510];
inline bool dfs(ll u) {
	for (ll i=0;i<G[u].size();++i) {
		ll v=G[u][i];
		if (!vis[v]) {
			vis[v]=1;
			if (dfs(match[v])||!match[v]) {
				match[v]=u;
				return true;
			}
		}
	}
	return false;
}
inline ll in() {
    char a=getchar();
	ll t=0,f=1;
	while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
    while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
    return t*f;
}
signed main() {
	n=in(),m=in(),e=in();
	for (ll i=1;i<=e;++i) {
		ll u=in(),v=in();
		G[u].push_back(v);	
        //注意若左右部点编号相同建边要建单向边。
        //建双向边的情况:1.左右编号不一致。2.左右一致时将左与右+n建边
	}
	for (ll i=1;i<=n;++i) {
		memset(vis,0,sizeof(vis));
		if (dfs(i)) ans++;
	}
	printf("%lld",ans); 
	return 0;
}

补充:

  • 二分图最小点覆盖(König 定理):

定义:最小点覆盖:选最少的点,满足每条边至少有一个端点被选。

定理:二分图中,最小点覆盖 \(=\) 最大匹配。

证明需要用到最大流部分知识,不过仔细想想就会觉得有道理。

能使用该模型的两要素:

  1. 能将原图转化为二分图并将对应关系连边,且每条边有两个端点。

  2. 每条边上的两个端点至少选一个。

模板例题

例题中想要将所有的 X 点消灭,意味着要么我要在 X 点横坐标上花费代价,要么在纵坐标上花费代价,即为上述的 \(2\) 条件。将 X 的横坐标向纵坐标连边跑最大匹配即可。

  • 二分图最大独立集:

定义:最大独立集:选最多的点,满足两两之间没有边相连。

定理:二分图中,最大独立集 \(=n-\) 最小点覆盖。

证明:因为在最小点覆盖中,任意一条边都被至少选了一个顶点,所以对于其点集的补集,任意一条边都被至多选了一个顶点,所以不存在边连接两个点集中的点,且该点集最大。证毕。

  • 有向图最小路径覆盖:

定义:给定有向无环图,用尽量少的不相交的有向边,覆盖整张图所有节点。

我们将所有的点 \(1\sim n\),新建 \(n\) 个点 \(n+1\sim 2n\) 使其与点 \(1\sim n\) 一一对应。将每一条边 \((u\to v)\),新建边 \((u\to v+n)\)

定理:原图上的最小路径覆盖 \(=\) \(n-\) 新图上的最大匹配数。

模板:Air Raid


系列做题技巧/注意事项:

  1. 遇到一个(左部)点可以和多个(右部)点匹配时,可以将这个(左部)要匹配的点的个数进行拆点。即 \((u \to v)\to(u\to v+kn),k\in [0,n-1]\)\(k\) 代表这个(左部)点要匹配的点的个数。

模板例题

  1. \(vis\) 数组没必要每次清空,可以这样:
inline bool dfs(ll u) {
	for (ll i=0;i<G[u].size();++i) {
		ll v=G[u][i];
		if (vis[v]!=now) {
			vis[v]=now;
			if (dfs(match[v])||!match[v]) {
				match[v]=u;
				return true;
			}
		}
	}
	return false;
}


…………


for (ll i=1;i<=1e6;++i) {
		now++;
		if (dfs(i)) ans++;
		else break;
	}
  1. 要求字典序升序/降序排列时:升序:将左部每个点连边的右端点升序排序,最后 dfs 的时候倒序进行。降序反之。

例题

  1. 一个小小的复杂度优化:当左部点数量明显大于右部点数量时,改为枚举右部点。

  2. 二分图进行匹配的时候一般要建有向边(意为选择了其中一个端点另外一个端点也会被选或另外一个端点就不能选了),枚举左部点逐次 dfs 进行匹配。若遇到根据题目条件要建无向边的时候,可以观察是或否能将建图之转化为单向。

  3. 多数能用 2-sat 解决的问题二分图也能解决(即满足最小点覆盖模型的二要素)。

典例剖析/经典思想在例题上的体现:

网格图上问题:

  • 网格图上将横纵坐标连有向边

P7368 [USACO05NOV]Asteroids G

观察到要么小行星的横坐标要被消除,要么就是纵坐标,即想要把这个行星删除并且花费最小代价时,要在其 横坐标行纵坐标列 选择一个,即满足最小点覆盖模型的两要素。所以将每个星星的横坐标向纵坐标连边即可。

  • 有障碍物时将同一连通块内染相同颜色后横纵连边连边

P2825 [HEOI2016/TJOI2016]游戏

我们将每一行和每一列联通的(能互相攻击到的)染为一个颜色,再根据题意将横纵能攻击到的连通块连有向边。

具体的为:每一个坐标 \((i,j)\) 在可放置时会进行两次染色:其所在行能互相攻击到的为一颜色记为 \(colx\),其所在列能互相攻击到的为同一颜色记为 \(coly\)。将这两个颜色连边意为:如果选择在颜色为 \(colx\) 连通块放置,则颜色为 \(coly\) 的连通块则不能放置。

  • 两种情况选一种时将这两个连有向边

P2172 部落战争

观察到将一个点放置后他能攻击到的点就不用放置了,所以将一个点与他能攻击到的点连有向边即可,意为选择其一另一个就不用再选了。

  • 将双向建边转为单向建边

P3355 骑士共存问题

若我们将每一个坐标向他能攻击到的八个方向建边,那么能互相攻击到的两个坐标会建成双向边,造成的后果是选出最小点覆盖的答案是双倍的。

解决方案:

  1. 将最小点覆盖的数值 \(÷2\) 再进行答案计算。

  2. 将横纵坐标值和为奇数的染为颜色 \(1\),否则为 \(2\)

观察到坐标和的奇偶性相同的不会互相攻击,那么我们可以只将颜色为 \(1\) 的向颜色为 \(2\) 的连有向边即可(即为将 \(1\) 颜色的点作为左部,\(2\) 的点作为右部)


二分图带权完美匹配(KM算法)

这个算法可谓很恶心,本人学习过程比较艰难。

模板:

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=7000;
ll n,visa[maxn],visb[maxn];
ll w[88][88],ex[maxn],ey[maxn]; 
ll match[maxn],slack[maxn];
ll Ans;
struct node {ll x,y;}mat[maxn];
inline bool cmp(node a,node b) {return a.x<b.x;}
inline bool dfs(ll u) {
	visa[u]=1;
	for (ll i=1;i<=n;++i) {
		if (visb[i]) continue;
		if (ex[u]+ey[i]==w[u][i]) {
			visb[i]=1;
			if (!match[i]||dfs(match[i])) {
				match[i]=u;
				return true;
			}
		}
		else slack[i]=min(ex[u]+ey[i]-w[u][i],slack[i]);
	}
	return false;
}
inline ll KM() {
	for (ll i=1;i<=n;++i) {
		match[i]=0,ex[i]=-inf,ey[i]=0;
		for (ll j=1;j<=n;++j) ex[i]=max(ex[i],w[i][j]);
	}
	for (ll i=1;i<=n;++i) {
		while (1) {
			for (ll j=1;j<=n;++j) visa[j]=visb[j]=0,slack[j]=inf;
			if (dfs(i)) break;
			ll delta=inf;
			for (ll j=1;j<=n;++j) if (!visb[j]) delta=min(delta,slack[j]);
			for (ll j=1;j<=n;++j) {
				if (visa[j]) ex[j]-=delta;
				if (visb[j]) ey[j]+=delta;
			}
		}
	}
	ll ans=0;
	for (ll i=1;i<=n;++i) ans+=w[match[i]][i];
	return ans;
}
inline ll in() {
    char a=getchar();
	ll t=0,f=1;
	while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
    while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
    return t*f;
}
signed main() {
	n=in();
	memset(w,-0x3f,sizeof(w));
	for (ll i=1;i<=n;++i) for (ll j=1;j<=n;++j) w[i][j]=in();
	Ans=KM();
	printf("%lld\n",Ans);
	return 0;
}

bfs 版本要比 dfs 在极端情况更优秀(理论上两种都是 \(O(n^3)\),但dfs在极端情况下会被卡成 \(O(n^4)\)

未学 bfs 版本因为码量略大。

  • KM 算法有时会比网络流算法更优。

后记:

  • 近乎所有的二分图问题都可通过网络流解决,并且大部分情况网络流算法复杂度会更优

  • 同时,二分图重在建图,这些建图模型在网络流问题中亦通用。

  • 最后的最后:二分图问题中在学习重点的不是算法,而是二分图的建图思想

posted @ 2023-06-06 18:27  Pwtking  阅读(88)  评论(0)    收藏  举报