二分图问题

二分图

是一种特殊的无向图,顶点可以被分为两个集合,相同集合内部的点两两之间没有边连接。

形式化定义:令 \(G={U,V}\) 是一个无向图,其中顶点 \(V\) 可以被分为两个不相交的子集 \(A,B\),使得每条边关联的点不同时属于一个子集。

二分图的匹配

给一个二分图 \(G\),在 \(G\) 的一个子图 \(M\) 中,\(M\) 的边集 \(E\) 中的任意两条边都不依附于同一个顶点,称 \(M\) 是一个匹配。

极大匹配:指在当前已完成的匹配下,无法再通过增加未加入匹配的边的方式来增加匹配的边数的匹配称作极大匹配。显然的,同一个二分图中,极大匹配有多种。

最大匹配:所有极大匹配当中边的数量最大的一种匹配。最大匹配也可能有多种。选择这样的边数最大的子集称为二分图的最大匹配问题。

完全匹配(完备匹配):A中的每一个顶点都与B中的一个顶点匹配,或者B中的每一个顶点也与A中的一个顶点匹配,则该匹配为完备匹配。(”或者“的原因是:两个子集的大小不一定相等)

二分图的判定

理论判定:不存在奇数长度回路的图一定是二分图。特殊的:没有环的图也一定是二分图,如树,DAG 之类。

实现:理论判定的应用,讲路的长度变为奇偶性,进而变成染色问题。任意选择一个点作为起点,染其为颜色“1”,对相邻点染颜色”2“;对于染了”2“的点,染其邻接点为”1“。若遇到一个将要染色的点,但其已被染色且将要染的颜色与已染的颜色不同,此时可判定图不是二分图,若未出现颜色的矛盾,图就是一个二分图。使用这种方法还可以区分图的左右子集。

具体可以用 DFS 或者 BFS 实现,不写代码了。

二分图最大匹配

二分图上的交替路:在一个匹配中,从一个未匹配的点出发,一次经过未匹配的、匹配的、未匹配的边,这样的路是交替路。

二分图上的增广路:从一个点出发,通过走交替路到达了一个未匹配的点,这样的路径叫增广路。

如果增广路匹配成功,一定走的是交替路,证明如下:

路径上的一个点接出两条匹配的路径:根据匹配的定义,不存在。

路径上的一个点接出的路径都是未匹配的:此时已经可以成功确定一条联系,增广路算法结束。

综上:不可能存在路径一个点连接的两条边状态相同,故增广路只能走交替路。

增广路的性质:

  1. 增广路有奇数条边。
  2. 增广路上的点交替出现在左右子集。
  3. 起点和终点都是未配对的状态。
  4. 未匹配状态的边的数量始终比已匹配的边的数量多一条。

由于性质 \(4\),我们可以通过将一条增广路上的边的状态取反来达到增加一个匹配数量的结果。

匈牙利算法求解二分图最大匹配

显然的,最大匹配数不会超过点数,故我们只需遍历一遍其中一个点集中的所有点即可。现在介绍匈牙利算法,设二分图 \(G\) 的点数为 \(n\),边数为 \(m\),则此算法可以在 \(O(nm)\) 时间内求解出二分图最大匹配。

匈牙利算法利用了 DFS 和简单的递归思想。定义 DFS 函数 bool dfs(u) 表示询问是否有从 \(u\) 点出发的增广路。定义数组 vis[u],p[u] 分别表示 \(u\) 在 DFS 中是否被访问过,与 \(u\) 点配对的点是哪一个。

在 DFS 过程中,遍历 \(u\) 的每个邻接点 \(v\),若 \(v\) 未被访问过,就尝试与 \(v\) 点建立联系。能够与一个点建立联系当且仅当:

  • \(v\) 还没有和其它顶点配对。
  • \(v\) 已被占用,但是 dfs(p[v]) 返回值为真(这里不能写 'dfs(v)' 我们不能从另一个集合出发找增广路)。也就是说可以找到一条从 \(v\) 出发且经过占用 \(v\) 的点的增广路,由于增广路长为奇数,对应着奇数个点。这意味着:我们可以通过取反这一条增广路,使得 \(v\) 成为一个未被占用的点,且不改变匹配总数。这样就可以使 \(u,v\) 建立新的匹配状态,也就可以增加一个匹配数量。

\(u,v\) 可以根据上面的规则建立联系,就将 p[v] 改为 \(u\)(这一过程相当于取反增广路了),并使函数返回 true 代表存在从 \(u\) 出发的增广路。若未找到则返回 false

不要忘了我们在递归过程中,既然我们已经设计好了一次递归的流程,那么整个问题也解决了。

下面是匈牙利算法求解二分图最大匹配的模板代码:

Show me the code
const int N=1999;
vector<int> edge[N];
int ans;
bool vis[N];
int p[N];
bool dfs(int u){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(!vis[v]){
      vis[v]=1;
      //没有匹配 可以找到经过匹配点的增广路
      if(!p[v]||dfs(p[v])){//这里不应写作 dfs(v)
        p[v]=u;
        return true;
      }
    }
  }
  return false;
}
int main(){
  
  int n,m,e;
  cin>>n>>m>>e;
  for(int i=1;i<=e;i++){
    //建图
  }
  for(int i=1;i<=n;i++){//只需要遍历一个子集即可
    memset(vis,0,sizeof vis);
    if(dfs(i))ans++;
  }
  cout<<ans;

  return 0;
}

网络最大流算法求解二分图最大匹配

你说得对,但是 \(O(nm)\) 还是太慢了,有没有更强力一点的算法?

有的朋友,有的,我们可以试着把这个问题变成网络最大流模型。

建立超级源点和超级汇点,将源点连上一个子集中的所有点,另一个子集里的所有点连上汇点,边的容量皆为 \(1\)。原来的每条边从左往右连边,容量也皆为 \(1\),最大流即最大匹配。

使用求解网络最大流的 Dinic 算法,可以在 \(O(\sqrt{n}m)\) 的时间内求解出二分图最大匹配。

以下是使用 Dinic 快速求解二分图最大匹配的模板代码:

Show me the code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4;
const int M=1e5;
struct e{
	int u;int v;int w;int nxt;
}edge[M*2];
int idx=-1;
int n,m,cm;
int _head[N];
void add(int u,int v,int w){
	idx++;
	edge[idx].u=u;
	edge[idx].v=v;
	edge[idx].w=w;
	edge[idx].nxt=_head[u];
	_head[u]=idx;
	return ;
}
int dis[N];int cur[N];int s,t;
bool bfs(){
	memset(dis,0,sizeof dis);
	queue<int> q;
	q.push(s);
	dis[s]=1;
	cur[s]=_head[s];
	while(q.size()){
		int u=q.front();
		q.pop();
		for(int i=_head[u];i!=-1;i=edge[i].nxt){
			int v=edge[i].v;
			int w=edge[i].w;
			if(!dis[v]&&w){
				dis[v]=dis[u]+1;
				cur[v]=_head[v];
				if(v==t)return 1;
				q.push(v);
			}
		}
	}
	return 0;	
}
int dfs(int u,int maxf){
	if(u==t){
		return maxf;
	}
	int ccnt=0;
	for(int i=cur[u];i!=-1&&ccnt<maxf;i=edge[i].nxt){
		int v=edge[i].v;
		int w=edge[i].w;
		cur[u]=i;
		if(dis[u]+1==dis[v]&&w){
			int gos=dfs(v,min(maxf-ccnt,w));
			if(gos==0)dis[v]=0;
			edge[i].w-=gos;
			edge[i^1].w+=gos;
			ccnt+=gos;
		}
	}
	return ccnt;
}
int dinic(){
	int res=0;
	int flow=0;
	while(bfs()){
		while(flow=dfs(0,1e9)){
			res+=flow;
		}
	}
	return res;
}
int main(){
	
	cin>>n>>m>>cm;
	memset(_head,-1,sizeof _head);	
	for(int i=1;i<=cm;i++){
		int u,v;
		cin>>u>>v;
		add(u,v+n,1);
		add(v+n,u,0); 
	}	
	for(int i=1;i<=n;i++){
		add(0,i,1);
		add(i,0,0);
	}
	for(int i=1;i<=m;i++){
		add(i+n,n+m+1,1);
		add(n+m+1,i+n,0);
	}
	s=0;
	t=n+m+1;
	cout<<dinic();

	return 0;
}

问题时间

【模板传送门】二分图最大匹配

没什么好说的,注意题目中输入图的方式比较特别。

P1129 [ZJOI2007] 矩阵游戏

题意
给定 \(n \times n\) 的 0-1 矩阵,判断是否可以通过行交换和列交换操作使得主对角线全为 1。

数据范围

  • 矩阵规模 \(n \leq 200\)
  • 测试用例数 \(T \leq 20\)

二分图经典题。

首先你可以发现如果一些黑块初始在同一行,那么无论我们怎么操作,这些块一定还是在同一行;同理,如果一些黑块初始在同一列,那么那么无论我们怎么操作,这些块一定还是在同一列。

因为只要考虑主对角线上格子的颜色状态,下面只考虑每一行,每一行的节点可以移动到所有列上,但是如果我们选择一个黑块并让它位于对角线上,此时同一行上其它的格子都是没有用的了,因为你不能再交换这一行和另一行,交换这一行的两列在此时也是无意义的。

于是,对于每一行,我都要找出这样的一个点,让它位于对角线上,但是位于对角线上只能通过交换列实现,这也就是说,我们选择的这些点都需要不同行也不同列

原问题就可以等价于:最多在图中可以找到多少点,使得这些点不同行也不同列。

学过网络流的朋友应该已经会了这个题了。

我们可以建一个二分图,左边是行,右边是列,如果一个点是黑色的,就从左边连右边,一条边匹配了 \(i\)\(j\) 列 相当于我们选择了一个点占有了 \(i\)\(j\) 列,于是匹配数就可以代表我们选出来了多少个点不同行也不同列,如果匹配数是 \(n\) 就代表有解了。

code

Show me the code
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
	ll x=0,f=1;
	char c=getchar();
	while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int crush[205];
bool vis[205];
vector<int> edge[205];
bool _find(int u){
	for(int i=0;i<edge[u].size();i++){
		int fav=edge[u][i];
		if(!vis[fav]){
			vis[fav]=1;
			if(!crush[fav]||_find(crush[fav])){
				crush[fav]=u;
				return 1;
			}
		}
	}
	return 0;
}
int main(){

	int T;
	cin>>T;
	while(T--){
		memset(crush,0,sizeof crush);
		memset(vis,0,sizeof vis);
		memset(edge,0,sizeof edge);
		int n;
		cin>>n;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				int c;
				c=rd;
				if(c==1)edge[i].push_back(j);
			}
		}
		int cnt=0;
		for(int i=1;i<=n;i++){
			memset(vis,0,sizeof vis);
			if(_find(i))cnt++;
		}
		if(cnt!=n)cout<<"No"<<'\n';
		else cout<<"Yes"<<'\n';
	}

	return 0;
}

P1640 [SCOI2010] 连续攻击游戏

题意
给定 \(N\) 个装备,每个装备有两个属性值 \(a_i,b_i \in [1,10000]\)。需要选择若干装备,每个装备使用其中一个属性,使得使用的属性值构成连续的 \(1,2,...,k\) 序列。求最大的 \(k\)

数据范围

  • 装备数 \(N \leq 10^6\)
  • 属性值 \(1 \leq a_i,b_i \leq 10^4\)

当然要顺序考虑每一种属性的伤害能不能打出,如果我们考虑到了属性 \(i\),我们显然只会关心有属性 \(i\) 的装备,如果该装备已被使用,我们要尝试把这个装备空出来。

欸这不就是匈牙利的过程吗,而且匈牙利算法还可以保证前面已经匹配过的属性值不会再失配。这是匈牙利算法的一个很好的性质,我们可以选择加入点的顺序,保证前缀的所有点匹配后就不会再失配。

于是我们从属性给所有有这个属性的装备连下边,大胆跑下匈牙利即可。

这个数据范围竟然能过(

code

Show me the code
#define psb push_back
#define mkp make_pair
#define rep(i,a,b) for( int i=(a); i<=(b); ++i)
#define per(i,a,b) for( int i=(a); i>=(b); --i)
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
  ll x=0,f=1;
  char c=getchar();
  while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
  while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
  return x*f;
}
const int N=1e6+5+10000;
vector<int> edge[N];
int p[N];bool vis[N];
bool dfs(int u){
  for(int i=0;i<edge[u].size();i++){
    int v=edge[u][i];
    if(!vis[v]){
      vis[v]=1;
      if(!p[v]||dfs(p[v])){
        p[v]=u;
        return 1;
      }
    }
  }
  return 0;
}
int main(){
  
  int n;
  cin>>n;
  rep(i,1,n){
    int a,b;
    cin>>a>>b;
    edge[a].push_back(i+10000);
    edge[b].push_back(i+10000);
  }
  rep(i,1,10000){
    memset(vis,0,sizeof vis);
    if(!dfs(i)){cout<<i-1;return 0;}
    else continue;
  }
  cout<<10000;
  
  return 0;
}
posted @ 2025-05-07 17:30  hm2ns  阅读(14)  评论(0)    收藏  举报