一些常用概念和定理:
Hall定理:
二部图G中的两部分顶点组成的集合分别为X, Y; X={X1, X2, X3,X4, .........,Xm}, Y={y1, y2, y3, y4 , .........,yn}, G中有一组无公共点的边,一端恰好为组成X的点的充分必要条件是:X中的任意k个点至少与Y中的k个点相邻。(1≤k≤m)
匹配:
给定一个二分图G,在G的一个子图M中,M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配。图一中红线为就是一组匹配。
未盖点:
设Vi是图G的一个顶点,如果Vi 不与任意一条属于匹配M的边相关联,就称Vi 是一个未盖点。如图一中的a3、b1。
交错路:
设P是图G的一条路,如果P的任意两条相邻的边一定是一条属于M而另一条不属于M,就称P是一条交错路。如图一中a2->b2->a1->b4。
可增广路:
两个端点都是未盖点的交错路叫做可增广路。
顶点的数目:
图中顶点的总数。
最大独立数(也就是最大独立集和最大团):
从V个顶点中选出k个顶,使得这k个顶互不相邻。 那么最大的k就是这个图的最大独立数。
ps:最大团问题是NP问题,但是二分图中的最大团可以通过多项式算法求解
最小顶点覆盖数:
用最少的顶点数k来覆盖图的所有的边,k就是这个图的最小顶点覆盖数。
最大匹配数:
所有匹配中包含的边数最多的数目称为最大匹配数。
顶点的数目=最大独立数+最小顶点覆盖数(对于所有无向图都有效)
最大匹配数=最小顶点覆盖数(只对二分图有效)
完全图:也就是任意两点都有边的图
完全子图:就是任意两都有边的子图
算法的实现方法:
不断寻找可增广路知道找不到为止,对此我想说,可以从很明显的奇偶找数特征入手。
增广路的一些性质:
(1)有奇数条边。
(2)起点在二分图的左半边,终点在右半边。
(3)路径上的点一定是一个在左半边,一个在右半边,交替出现。
(4)整条路径上没有重复的点。
(5)路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。
(6)把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反,即是俗说的异或操作),则新的匹配数就比原匹配数加了1个。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int dfs(int x)
{
use[x]=1;
int t=a[x].size();
for(int i=0;i<t;i++)
{
int u=a[x][i],w=match[u];
if(w<0 || !use[w] && dfs(w))
{
match[x]=u;
macth[u]=x;
return 1;
}
}
return 0;
}
int zuidapipei()
{
int res=0;
memset(match,-1,sizeof(match));
for(int i=1;i<=n;i++)
{
if(match[i]!=-1) continue;
memset(use,0,sizeof(use));
if(dfs(i))
{
res++;
}
}
return res;
}
最小路径覆盖数问题:
可以把每一个顶点一分为二,可以理解为是出发和目标,然后按照原来的图进行连接转化为二分图,最后求最大匹配即可,其中ans=顶点数-最大匹配数。
简单说明:当一条边也没有是,无疑顶点数=路径数,没出现一对匹配路径数就会减一,每一条二分图上的边都会使路径数减一,也就是二分图上的
每一条边都可以将两条路径和为一条。
一般图匹配:用带花树算法实现(也可以用tutte矩阵,然并卵我不会
相对于二分图匹配不能用匈牙利dfs增广的原因是,遇到奇环时增广会出现同一个点连接两条匹配边的事情,然后匹配翻转取反的时候就会出现矛盾,无法保证
算方法的正确性。针对这个问题,带花树算法将奇环处理成一个缩点,这并不是显性的缩点(显性的缩点不便于环内的匹配),而是用并查集维护这个出现的每一朵花(也就是奇环)。
同时为了匹配边取反我们还需要记录每一个点的前驱。
另外,环上的前驱是要双向的(据说是因为在环上不确定增广的方向
同时还要将环上所有顶点都染成同一种颜色才行(缩点嘛
奇环一共2k+1个点,由于环里的一个点一定会向外匹配,这样环中的另外2k个点就能活跃起来进行匹配了。。。(能形成k条边呢
那么加入奇环就是终点了呢,不会有向外匹配的点了呢?这样环中还是会有2k个点剩余,不影响最终的答案。
至于为什么要用bfs呢(似乎是因为交错树的原因,这种写法实现这种算法更加方便。
贴一下我的带花树板子
#include<algorithm>
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cstdlib>
#include<climits>
#include<vector>
#include<cstdio>
#include<cmath>
#include<queue>
using namespace std;
const int maxn=1005;
int n,m;
int biao[maxn],vs[maxn],fa[maxn],match[maxn],pre[maxn];//pre是非匹配边的前置点
vector<int> a[maxn];
queue<int> que;
inline const int Get_Int() {
int num=0,bj=1;
char x=getchar();
while(x<'0'||x>'9') {
if(x=='-')bj=-1;
x=getchar();
}
while(x>='0'&&x<='9') {
num=num*10+x-'0';
x=getchar();
}
return num*bj;
}
int lca(int x,int y)
{
static int times=0;
++times;
x=fa[x],y=fa[y];
while(vs[x]!=times)
{
if(x)
{
vs[x]=times;
x=fa[pre[match[x]]];
}
swap(x,y);
}
return x;
}
void bloom(int x,int y,int z)//标记花,给双向标记
{
while(fa[x]!=z)
{
pre[x]=y;
y=match[x];
if(biao[y]==1)
{
biao[y]==0;
que.push(y);
}
fa[x]=fa[y]=fa[z];//万一是个花的嵌套怎么办
x=pre[y];
}
}
int bfs(int s)
{
for(int i=1;i<=n;i++) fa[i]=i;
// fill(biao,biao+n+1,-1);
memset(biao,-1,sizeof(biao));
que=queue<int>();
biao[s]=0;
que.push(s);
while(!que.empty())
{
int t=que.front();
// cout<<"t="<<t<<endl;
que.pop();
for(int nex:a[t])
{
if(biao[nex]==-1)
{
biao[nex]=1;
pre[nex]=t;
if(match[nex]==0)
{
for(int to=nex,from=t;to;from=pre[to])
{
match[to]=from,swap(match[from],to);
// cout<<match[u]<<" "ma
}
return true;
}
biao[match[nex]]=0;
que.push(match[nex]);
}
else if(biao[nex]==0&&fa[t]!=fa[nex])
{
// cout<<"找到花了"<<endl;
int gong=lca(t,nex);
bloom(t,nex,gong);
bloom(nex,t,gong);
}
}
}
return false;
}
//int n,m;
int main()
{
// freopen("in.txt","r",stdin);
// cin>>n>>m;
n=Get_Int();
m=Get_Int();
for(int i=1;i<=m;i++)
{
int l,r;
l=Get_Int();
r=Get_Int();
a[l].push_back(r),a[r].push_back(l);
}
int res=0;
for(int i=n;i>=1;i--)
{
if(!match[i])
{
res+=bfs(i);
}
}
printf("%d\n",res);
for(int i=1;i<=n;i++)
if(i!=n) printf("%d ",match[i]);
else printf("%d\n",match[i]);
}