二分图

定义

顶点集 \(V\) 可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。

有奇环的图一定不是二分图。奇环里的每个点各自出发都会把环内每个点分成不同的两个集合,所以不是二分图。

反之,没有奇环的图一定是二分图。

二分图的判定:染色法

本质其实就是 DFS ,我们从任意一个点出发,假设染成黑色,那么就把它相邻的点染成白色,如此类推。如果有个点即将要染的色和它本身就有的色不匹配,说明这个图就不是二分图。

by 董晓算法

二分图最大匹配

应用算法:匈牙利算法 : $ O(nE)$ , Dinic:\(O(n\sqrt E)\)

匈牙利算法

前置

匹配点:匹配边两端的端点

增广路径,即:一边的非匹配点到另一边的非匹配点的一条非匹配边和匹配边交替经过的路径. (开头和结尾一定是非匹配点且不在一个集合中)

思路

(1)找出一条增广路,通过将增广路取反,得到新的 \(M'\) 来代替原 \(M\),新的匹配数相对于原匹配数多1。(因为增广路中非匹配边比匹配边多 \(1\) ,我们通过取反,就能让匹配边变成非匹配边,非匹配边变成匹配边,这样匹配数就会多 \(1\))

(2)重复(1)操作,直至匹配过程中找不到增广路为止。

#include<bits/stdc++.h>
//#define int long long
#define ll long long
#define next nxt
#define re register
#define il inline
const int N = 5e4 + 5;
using namespace std;
int max(int x,int y){return x > y ? x : y;}
int min(int x,int y){return x < y ? x : y;}

int match[N],vis[N],cur[N];
int n,m,e,u,v,ans;
struct node{
	int u,v,next;
}edge[N<<1]; int head[N],num_edge;


il int read()
{
	int f=0,s=0;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f |= (ch=='-');
	for(; isdigit(ch);ch=getchar()) s = (s<<1) + (s<<3) + (ch^48);
	return f ? -s : s;
}

il void add(int from,int to)
{
	edge[++num_edge] = (node){from,to,head[from]};
	head[from] = num_edge;
}

il bool dfs(int x)
{
	for(re int i=head[x];i;i=edge[i].next)
	{
		int y = edge[i].v;
		if(vis[y]) continue;
		vis[y] = 1;
		if(!match[y] || dfs(match[y]))//找增广路
		{
			match[y] = x;//match数组表示右部点现在和哪个点匹配
			return true;
		}
	}
	return false;
}

signed main()
{
	n = read() , m = read() , e = read();
	for(re int i=1;i<=e;i++)
	{
		u = read() , v = read();
		add(u,v);
	}
	for(re int i=1;i<=n;i++)
	{
		memset(vis , 0 , sizeof vis);//vis数组判右部点是否访问
		if(dfs(i)) ans++;
	}
	cout << ans;
	return 0;
}

Dinic 算法

建立超源和超汇,超源向左边的点连流量为 \(1\) 的边,如果 \((u,v) \in E\) ,从 \(u\)\(v\) 连一条流量为 \(1\) 的边,右部点向超汇连流量为 \(1\) 的点,跑最大流即可。

最大匹配相关

最小点覆盖集

定义:给定二分图 \(G=(V,E)\),若点集 \(C⊆V\) 满足对于任意 \((u,v)\in E\) 都有 \(u∈C\)\(v∈C\),则称 \(C\)\(G\) 的点覆盖集。其中 \(|C|\) 最小的就是最小点覆盖集。

通俗地讲,就是找出最少的点,使得所有边至少有一个端点在这个点集里。

性质:由 König 定理得,最大匹配 = 最小点覆盖集

证明:这篇博客

最大独立集:

定义:给定二分图 \(G=(V,E)\) ,若点集 \(I⊆V\) 满足任意两点不直接相连,则称 \(I\)\(G\) 的独立集,独立集大小为 \(|I|\)

通俗地讲,就是二分图中选 \(n\) 个点,使得他们之间没有直接的边相连,那么最大的 \(n\) 就是这个二分图的最大独立集。

性质:总点数 \(-\) 最小点覆盖集 \(=\) 最大独立集

即:总点数 \(-\) 最大匹配 \(=\) 最大独立集 , 最大匹配 \(=\) 总点数 \(-\) 最大独立集

感性证明:我们可以看出最小点覆盖集和最大独立集的定义刚好是对立的,我们选出最少的点把另一些点给隔开了。因此最小点覆盖集和最大独立集其实是互补的。

二分图的补图

定义:原先二分图两个点之间如果有边就删掉,没边就加上(左边的每个点之间都有边,右边同样)。

最大团

定义:选出尽量多的点,使得两两之间都有边

性质:最大团 = 补图的最大独立集

解释:补图的最大独立集之间两两之间没有边,那么就说明之前的图之间两两有边

最小边覆盖和最小路径覆盖

好博客,这个我也不是很理解,等以后如果做到这种题了再来总结整理。

完备匹配

给定一张二分图,其左部,右部节点数均为 \(n\) 个节点,若最大匹配数 \(=n\) ,则称该二分图具有完备匹配。

多重匹配

给定一张包含 \(N\) 个左部点 ,\(M\) 个右部点的二分图,从中选出尽量多的边使第 \(i(1\leq i \leq N)\) 个左部点至多与 \(kl_i\) 条选出的边相连,第 \(j(1\leq j \leq M)\) 个右部节点至多与 \(kr_j\) 条选出的边相连。这就是二分图的多重匹配。

简而言之,就是一个点可以和多个点进行匹配。

面对多重匹配,一般有四种方法,这里只简述其中两种:

  • 1.拆点。把第 \(i\) 个左部点拆成 \(kl_i\) 个本质相同的点,右部点同理,然后做普通的最大匹配。在有些题目中,可能同一个点在不同的时期有不同的价值,如AcWing374 导弹防御塔 ,这个题就只能考虑用拆点法来做。

  • 2.网络流。这是最一般且高效的方法,超级源点向 \(i\) 点连一条流量为 \(kl_i\) 的边, \(j\) 向超级汇点连一条流量为 \(kr_j\) 的边,跑网络流即可。

二分图最大权完美匹配:KM算法

前置定义

可行顶标:给每个节点分配一个权值 \(la_x,lb_y\) ,对于所有边满足 \((x,y)\) 满足 \(la_x + lb_y \ge w(x,y)\)

交错树:KM 算法是对匈牙利算法的改造,如果从某个左部节点出发寻找匹配失败,那么在 DFS 的过程中,所有访问过的节点,以及为了访问这些节点而经过的边,共同构成一棵树。

这棵树的根是一个左部点,所有叶子节点也是一个左部点(因为最终匹配失败),并且树上第 \(1,3,5\dots\) 层的边都是非匹配边,第 \(2,4,6\dots\) 层的边都是匹配边。这就是交错树,把这个树记为 \(T\)

相等子图:给一组可行顶标下原图的生成子图,包含所有点但只包含满足 \(la_x + lb_y = w(x,y)\)

定理:如果相等子图存在完美匹配,该匹配就是二分图的最大权完美匹配。(证明用到线性规划的知识点,我不懂/oh)

算法流程

1.首先先构造出一组可行顶标,初始化。左部顶标 $ = \max(\text{出边边权最大值})$ ,右部顶标 $= 0 $

2.枚举左部点,沿着交替路走,找增广路。

  • 若能走到一个非匹配点,证明有一条新增广路,记录匹配点对(加入相等子图中)
  • 若不能走到一个非匹配点,记录 \(\Delta = la_x + lb_y - w(x,y)\) ,根据最小的 \(\Delta\) 更新交替路上的顶标值(左减右加),不断松弛,直至找到增广路。

怎么理解呢?

考虑匈牙利算法的流程,容易发现以下两条结论。

  1. 除了根节点以外,\(T\) 中其他的左部点都是从右部点沿着匹配边访问到的,即在程序过程中调用了 \(dfs(match[y])\) ,其中 \(y\) 是一个右部点(因为是交替路,所以只能这么走)。

  2. \(T\) 中所有的右部点都是从左部点沿着非匹配边访问到的(因为不能改变原有匹配,所以只能走非匹配边)。

在找增广路的过程中,我们不改变原来的匹配,要在找到匹配的基础上去找新的匹配,所以一个右部点找到的左部点是固定的,不好去拓展新的匹配。所以我们要从性质二入手,考虑怎么让左部点沿着非匹配边找到访问到更多的右部点。

这个步骤就是上述提及到的算法流程的第二步了,找到一个最小的 \(\Delta\) ,把 \(T\) 中左部点减小一个 \(\Delta\) ,右部点增大一个 \(\Delta\) ,这样以后会有什么的变化?不妨分类讨论一波。

  • 1.右部点 \(j\) 沿着匹配边,递归访问 \(i = match[j]\)

    显然对于一条匹配边,要么 \(i,j \in T\) , 要么就 \(i,j \notin T\) ,左减右加,对匹配没有影响,还是一个匹配。

  • 2.左部点 \(i\) 沿着非匹配边,尝试寻找一个右部点 \(j\)

    应用性质1,因为左部点的选择是被动的,所以只需考虑 \(i \in T\) 的情况。

    (1).若 \(i,j \in T\) , 则 \(la_i+lb_j\) 不变,所以之前能选的现在依旧能选。

    (2).若 \(i \in T , j \notin T\) ,则 \(la_i+lb_j\) 减小了。之前不能选的边,减小后就有可能可以选了。

这就是算法流程第二步的原理所在,应用这个原理,就可以在 dfs 的过程中不断找到新的匹配点。

EK算法的复杂度是 \(O(N^4)\)\(O(NM^2)\) , 随机数据下是 \(O(N^3)\)

实际上,还存在着一种 bfs 写法的 \(O(N^3)\) 写法,但是对于这种写法我还是一知半解。因此在这就只贴个代码了。

dfs:

#include<bits/stdc++.h>
#define int long long
#define ll long long
#define next nxt
#define re register
#define il inline
const int N = 5e2 + 5;
const int M = 1e5 + 5;
const int INF = 1e12;
using namespace std;
int max(int x,int y){return x > y ? x : y;}
int min(int x,int y){return x < y ? x : y;}

int match[N],visx[N],visy[N];//分别判断左部点和右部点是否在交替路中
int G[N][N],lx[N],ly[N],d[N];//邻接矩阵存图和两个顶标,以及松弛数组
int n,m,u,v,w,ans;

il int read()
{
	int f=0,s=0;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f |= (ch=='-');
	for(; isdigit(ch);ch=getchar()) s = (s<<1) + (s<<3) + (ch^48);
	return f ? -s : s;
}

il bool dfs(int x)
{
	visx[x] = 1;//x在交替路中
	for(re int y=1;y<=n;y++)
	{
		if(visy[y]) continue;
		if(lx[x] + ly[y] == G[x][y])//判断是否是相等子图
		{
			visy[y] = 1;//是的话就把y放在交替路中
			if(!match[y] || dfs(match[y]))//看看能不能延伸交替路
			{
				match[y] = x;
				return true;
			}
		}
		else d[y] = min(d[y],lx[x]+ly[y]-G[x][y]);//如果不是相等子图,找到最小松弛数
	}
	return false;
}

il int KM()
{
	for(re int i=1;i<=n;i++) lx[i] = -INF;
	for(re int i=1;i<=n;i++)
		for(re int j=1;j<=n;j++)
			lx[i] = max(lx[i],G[i][j]);//构造一组解
	for(re int i=1;i<=n;i++)
	{
		while(true)
		{
			memset(visx , 0 , sizeof visx);
			memset(visy , 0 , sizeof visy);
			for(re int j=1;j<=n;j++) d[j] = INF;
			if(dfs(i)) break;//找到解才退出
			int delta = INF;
			for(re int j=1;j<=n;j++) if(!visy[j]) delta = min(delta,d[j]);//找不到就一直松弛
			for(re int j=1;j<=n;j++)
			{
				if(visx[j]) lx[j] -= delta;//松弛
				if(visy[j]) ly[j] += delta;
			}
		}
	}
	for(re int i=1;i<=n;i++) ans += G[match[i]][i];
	return ans;
}

signed main()
{
	n = read() , m = read();
	for(re int i=1;i<=n;i++) 
		for(re int j=1;j<=n;j++)
			G[i][j] = -INF;
	for(re int i=1;i<=m;i++)
	{
		u = read() , v = read() , w = read();
		G[u][v] = w;
	}
	cout << KM() << "\n";
	for(re int i=1;i<=n;i++) cout << match[i] << " ";
	return 0;
}

bfs:

#include<bits/stdc++.h>
#define int long long
#define ll long long
#define next nxt
#define re register
#define il inline
const int N = 5e2 + 5;
const int M = 1e5 + 5;
const int INF = 1e12;
using namespace std;
int max(int x,int y){return x > y ? x : y;}
int min(int x,int y){return x < y ? x : y;}

int match[N],visx[N],visy[N];
int G[N][N],lx[N],ly[N],pre[N],d[N];
int n,m,u,v,w,ans;

il int read()
{
	int f=0,s=0;
	char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f |= (ch=='-');
	for(; isdigit(ch);ch=getchar()) s = (s<<1) + (s<<3) + (ch^48);
	return f ? -s : s;
}

il void bfs(int u)
{
	int x , y = 0 , yy = 0 , delta;
	memset(pre , 0 , sizeof pre);
	for(re int i=1;i<=n;i++) d[i] = INF;
	match[y] = u;//假设从右部0点开始找交替路
	while(true)
	{
		x = match[y] , delta = INF , visy[y] = 1;
		for(re int i=1;i<=n;i++)
		{
			if(visy[i]) continue;
			if(d[i] > lx[x] + ly[i] - G[x][i])
			{
				d[i] = lx[x] + ly[i] - G[x][i];
				pre[i] = y;//找到了一个新右部点,pre数组存储在这条交替路中上一个右部点的编号
			}
			if(delta > d[i]) { delta = d[i]; yy = i; }//找最小边
		}
		for(re int i=0;i<=n;i++)
		{
			if(visy[i]) lx[match[i]] -= delta , ly[i] += delta;//松弛
			else d[i] -= delta;
		}
		y = yy;//从最小边开始找,减少冗余搜索
		if(match[y] == -1) break;
	}
	while(y) { match[y] = match[pre[y]]; y = pre[y]; }//倒推更新增广路,因为bfs不能回溯
}

il int KM()
{
	memset(match , -1 , sizeof match);
	memset(lx , 0 , sizeof lx);
	memset(ly , 0 , sizeof ly);
	for(re int i=1;i<=n;i++)
	{
		memset(visy , 0 , sizeof visy);
		bfs(i);
	}
	for(re int i=1;i<=n;i++) if(match[i] != -1) ans += G[match[i]][i];
	return ans;
}

signed main()
{
	n = read() , m = read();
	for(re int i=1;i<=n;i++) 
		for(re int j=1;j<=n;j++)
			G[i][j] = -INF;
	for(re int i=1;i<=m;i++)
	{
		u = read() , v = read() , w = read();
		G[u][v] = w;
	}
	cout << KM() << "\n";
	for(re int i=1;i<=n;i++) cout << match[i] << " ";
	return 0;
}
posted @ 2023-05-25 20:27  Bloodstalk  阅读(61)  评论(0编辑  收藏  举报