二分图及其应用
基本概念
二分图又称二部图
定义:
设G=(U,V,E)是一个无向图,U和V是点的集合,E是边的集合。
如果符合:
- 集合U,V之间有边。
- U集合内部没有边。
- V集合内部没有边。
则称图G为二分图。
例如:
作用:
进行匹配,比如说给程序员分配工作,为动物分配主人。
判断是否为二部图
思想
染色法
步骤:
- 任意取一个结点染成红色。
- 重复下面过程,直到所有结点都被染色
-
- 将红色结点的连接点染成蓝色。
-
- 将蓝色结点的连接点染成红色。
-
- 如果发现某结点和邻接点的颜色相同,则终止程序,该图不是二分图。
- 如果没有发现某结点和邻接点的颜色相同,则该图是二分图。
举例:
该图是二分图。
代码
思想:
利用DFS/BFS
图示:
- 先选任意结点入队,染成红色。
- 重复下面过程,直到队列没有元素
- 将队头元素出队,将他的未染色的邻居染色并且入队
- 判断他已经染色的邻居是否与他颜色不同。
循环1
循环2
循环3
....不贴图了
代码:
#include<stdio.h>
#include<string.h>
const int maxn=1e5+5;
const int maxm=1e5+5;
int head[maxn],point[maxm<<1],nxt[maxm<<1],size;
int c[maxn]; //color,每个点的黑白属性,-1表示还没有标记,0/1表示黑白
int num[2]; //在一次DFS中的黑白点个数
bool f=0; //判断是否出现奇环
void init(){
memset(head,-1,sizeof(head));
size=0;
memset(c,-1,sizeof(c));
}
void add(int a,int b){
point[size]=b;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
nxt[size]=head[b];
head[b]=size++;
}
void dfs(int s,int x){
if(f)return;
c[s]=x;
num[x]++;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(c[j]==-1)dfs(j,!x);
else if(c[j]==x){
f=1;
return;
}
}
}
//下面是主函数内的调用过程
for(i=1;i<=n&&(!f);i++){
if(c[i]==-1){
num[0]=num[1]=0;
dfs(i,1);
}
}
匹配
基本概念
匹配:
给定一个二分图,在图G的一个子图G’中,
如果G’的边集中的任意两条边都不依附于同一个顶点,
也就是说:每个顶点只被一条边连接
则称G’的边集为G的一个匹配。
例如:
最大匹配:
在所有的匹配中,边数最多的那个匹配
称为二分图的最大匹配。
例如:
注意:最大匹配可能不唯一。
无权二部图中的最大匹配
转换成最大流即可
有权二部图中的最大/小匹配
方法:
匈牙利算法
有权二部图中的最大匹配是指:权值和最大的匹配。
最大匹配和最小匹配可以相互转换
:只需要将权值取负号即可。
二分图的最小顶点覆盖=最大匹配数
作用:
实现整体效益最大化。比如说
概述
交替路:
从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边...形成的路径叫交替路。*
增广路:
从一个未匹配点出发,走交替路,以另一个非匹配点结束,则这条交替路称为增广路(agumenting path)。
显然,这种路径可以通过匹配边非匹配边的互换来产生更多的匹配。
也可以理解为是把增广路里的匹配边集合与这条路径上所有边的集合做对称差。
增广路定理: 匹配数最大 等价于 不存在增广路
也就是说:
对于一个不是最大匹配的匹配,一定存在增广路
也就是说:
没有了增广路,一定是最大匹配
点击查看证明
不存在增广路的必要性很好证明,如果有增广路,因为增广路结构是非匹配边,匹配边......非匹配边,那么把所有匹配边和非匹配边交换就行了
这样匹配数加一,则原来的匹配不是最大匹配
充分性稍难,证明看维基
匈牙利算法
匈牙利算法实际上就是一个不断找增广路直到找不到的过程。
代码:
int M, N; //M, N分别表示左、右侧集合的元素数量
int Map[MAXM][MAXN]; //邻接矩阵存图
int p[MAXN]; //记录当前右侧元素所对应的左侧元素
bool vis[MAXN]; //记录右侧元素是否已被访问过
bool match(int i)
{
for (int j = 1; j <= N; ++j)
if (Map[i][j] && !vis[j]) //有边且未访问
{
vis[j] = true; //记录状态为访问过
if (p[j] == 0 || match(p[j])) //如果暂无匹配,或者原来匹配的左侧元素可以找到新的匹配
{
p[j] = i; //当前左侧元素成为当前右侧元素的新匹配
return true; //返回匹配成功
}
}
return false; //循环结束,仍未找到匹配,返回匹配失败
}
int Hungarian()
{
int cnt = 0;
for (int i = 1; i <= M; ++i)
{
memset(vis, 0, sizeof(vis)); //重置vis数组
if (match(i))
cnt++;
}
return cnt;
}
稳定婚配问题
问题描述:
有N男N女,每个人都按照他对异性的喜欢程度排名。现在需要写出一个算法安排这N个男的、N个女的结婚,要求两个人的婚姻应该是稳定的。
何为稳定?
当前假设1号男生的对象是1号女生,2号男生的对象是2号女生。
但如果1号男生对2号女生的好感度大于对1号女生的好感度,并且2号女生对1号男生的好感度也大于对2号男生的好感度,
那么1号男生会和2号女生在一起,这场婚姻就是不稳定的,反之就是稳定。
要求:
男生和女生数量一致。
男生和女生的喜欢都是单项的
Gale-Shapley算法
算法步骤:
- 每个单身男生向自己最喜欢的女生求婚
要求:
-
- 可以向任何女生求婚,即使女生已经结婚
-
- 一个男生只能向一个女生求婚一次。
- 当一个女生有多个男生追求时(丈夫也算追求者),选取最喜欢的那个。
- 所有男生都划掉已经求婚过的女生
- 重复123直到没有单身狗。
(结果对男生更有利)
算法复杂度: On2
(一共有n个男生,每个男生有n个女生的心动排名)
代码:
#include <bits/stdc++.h>
#define mod 1000000007
#define MAXN 1000
typedef long long ll;
using namespace std;
int ManArray[MAXN][MAXN], GirArray[MAXN][MAXN]; //ManArray[i][j]代表编号为i的男生的第j位心仪女生是几号,GirArray[i][j]代表编号为i的女生的第j位心仪男生是几号
int Man[MAXN], Gir[MAXN]; //Man[i]代表i号男生所匹配到的女生是几号,Gir[i]代表i号女生所匹配到的男生是几号
int ManStarPos[MAXN]; //ManStarPos[i]代表i号男生现在匹配到的女生是他心目中的第几号心仪女生
int n;
stack<int> q; //存放单身男生的编号
//求编号为ManI的男生在编号为GirI的女生的心中的排名
int GetPositionFromLaday(int GirI, int ManI)
{
for (int i = 0; i < n; i++)
if (GirArray[GirI][i] == ManI)
return i;
return -1;
}
//为编号为ManI的男生匹配女生
void ManLookGir(int ManI)
{
int NowGir = ManArray[ManI][ManStarPos[ManI]]; //得到这个男生应该匹配的女生的编号
if (Gir[NowGir] == -1) //如果这个女生单身,那么就匹配上
{
Man[ManI] = NowGir;
Gir[NowGir] = ManI;
}
else //如果这个女生现在已经有男朋友了
{
//得到现男友在这个女孩心中的排名
int OldMan = GetPositionFromLaday(NowGir, Gir[NowGir]);
//得到我们要匹配的男生在这个女孩心中的排名
int NowMan = GetPositionFromLaday(NowGir, ManI);
if (OldMan < NowMan) //如果这个女孩更喜欢现任男友,那么这个女孩不换男朋友
{
ManStarPos[ManI]++;
q.push(ManI);
}
else //这个女孩更喜欢我们要匹配的这个男生,那么女孩换男朋友
{
ManStarPos[Gir[NowGir]]++;
q.push(Gir[NowGir]); //现任男友单身
Man[ManI] = NowGir;
Gir[NowGir] = ManI;
}
}
}
int main()
{
cin >> n;
//初始化
memset(Man, -1, sizeof(Man));
memset(Gir, -1, sizeof(Gir));
memset(ManStarPos, 0, sizeof(ManStarPos));
//输入每个男生心目中对女生的排序
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> ManArray[i][j];
//输入每个女生心目中对男生的排序
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> GirArray[i][j];
//刚开始对每个男生都进行一次匹配,从每个男生最心仪的女生开始匹配。
for (int i = 0; i < n; i++) ManLookGir(i);
//对剩下的单身男生进行匹配
while (!q.empty())
{
int i = q.top();
q.pop();
ManLookGir(i);
}
for (int i = 0; i < n; i++)
cout << "Man NO.: " << i << " Laday NO.: " << Man[i] << endl;
}
最小点覆盖问题
另外一个关于二分图的问题是求最小点覆盖:我们想找到最少的一些点,使二分图所有的边都至少有一个端点在这些点之中。倒过来说就是,删除包含这些点的边,可以删掉所有边。
性质:
最大团 = 补图的最大独立集
最小边覆盖 = 二分图最大独立集 = |V| - 最小路径覆盖
最小路径覆盖 = |V| - 最大匹配数
最小顶点覆盖 = 最大匹配数
最小顶点覆盖 + 最大独立数 = |V|
最小割 = 最小点权覆盖集 = 点权和 - 最大点权独立集