SDSC整理(Day3 二分图)
二分图
二分图基础部分
定义
二分图,又称为二部图,是指节点由两个集合组成,且两个集合内部没有边的图。
性质
- 不存在长度为奇数的环。
因为每一条边都是从一个集合走到另一个集合,只有走偶数次才可能回到同一个集合。
- 可以被黑白染色。
判定
这是一个二分图。
无知的我:这是二分图???
对,这是二分图。
很明显用眼来判断是一个错误的选择
那我们怎么来判断呢???
看性质 \(2\) ,我们可以对图进行黑白染色,只要染色成功就是二分图。
因为二分图不一定联通,所以要每个点都判断一下。
什么,你不会染色!!!
code
/*
判断二分图
date:2022.7.30
worked by respect_lowsmile
*/
#include<iostream>
using namespace std;
const int N=1e5+5;
struct node
{
int to,next;
};
node edge[N<<2];
int num,n,m;
int head[N],color[N],vis[N];
void add(int u,int v)
{
num++;
edge[num].to=v;
edge[num].next=head[u];
head[u]=num;
}
void dfs(int now,int c)
{
vis[now]=1,color[now]=c;
for(int i=head[now];i;i=edge[i].next)
{
int v=edge[i].to;
if(vis[v]&&color[v]==color[now])
{
printf("NO");
exit(0);
}
if(!vis[v]) dfs(v,c^1);
}
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=m;++i)
{
int x,y;
scanf("%d %d",&x,&y);
add(x,y),add(y,x);
}
for(int i=1;i<=n;++i)
if(!vis[i])
dfs(i,1);
printf("YES");
return 0;
}
例题:
二分图最大匹配问题
网络流可做,直接建超级源点和超级汇点跑最大流
此篇完
一些概念
匹配:在图论中,一个匹配是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图的所有匹配中,所含匹配数最多的匹配成为二分图的最大匹配。
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。(也就是左右的每一个点都相互一一对应)
我们可以把二分图的匹配问题类比成找对象
匈牙利算法( \(O(n\times e+m)\) )
过程
匈牙利算法的过程是,枚举每一个左部点 \(u\) ,然后枚举该左部点连出的边,对于一个出点 \(v\) ,如果它没有被先前的左部点匹配,那么直接将 \(u\) 匹配 \(v\) ,否则尝试让 \(v\) 的“原配”左部点去匹配其他右部点,如果“原配”匹配到了其他点,那么将 \(u\) 匹配 \(v\) ,否则 \(u\) 失配。
其本质是不断寻找增广路来扩大匹配数。
举个例子:
首先我们去匹配 \(1\) ,我们把 \(1\) 和 \(5\) 连在一起。
然后我们去匹配 \(2\) ,我们发现 \(2\) 也与 \(5\) 相连,所以我们让 \(1\) 更换匹配对象, \(1\) 还和 \(7\) 相连,所以我们把 \(1\) 与 \(7\) 相连,把 \(2\) 与 \(5\) 相连。
然后我们匹配 \(3\) ,我们发现 \(3\) 也与 \(5\) 相连,所以我们让 \(2\) 更换匹配对象, \(2\) 还与 \(6\) 相连,所以我们把 \(2\) 与 \(6\) 相连,把 \(3\) 与 \(5\) 相连。
然后我们去匹配 \(4\) ,我们发现 \(4\) 与 \(5\) 相连,所以我们让 \(3\) 更换匹配对象,但是 \(3\) 除了与 \(5\) 相连之外不与其他任何一条边相邻,所以 \(3\) 不会交换匹配对象,但是 \(4\) 只与 \(5\) 相连,所以 \(4\) 匹配失败。
算法结束,最大匹配数是 \(3\) 。
图片好丑
总结
观察图可以发现:
-
如果后来和之前的发生了矛盾,就让之前的退让并去寻找新的匹配点
-
如果退让的点没有别的点可以匹配,则拒绝退让,让新来的去找别的匹配点
-
如果新来的找不到其他匹配点,匹配失败。
\(code\)
/*
二分图最大匹配
date:2022.7.30
worked by respect_lowsmile
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N=5e4+5;
struct node
{
int to,next;
};
node edge[N<<1];
int num,ans,n,m,e;
int head[N],vis[N],mat[N];//vis标记有没有走过,mat[i]表示与i配对的点的编号
void add(int u,int v)
{
num++;
edge[num].to=v;
edge[num].next=head[u];
head[u]=num;
}
int dfs(int now)
{
for(int i=head[now];i;i=edge[i].next)//遍历每一条边
{
int v=edge[i].to;
if(vis[v]) continue;//如果被找过就跳过
vis[v]=1;//标记这个点
if(!mat[v]||dfs(mat[v]))//这个点没有被匹配或者匹配的这个点还有其他的匹配
{
mat[v]=now;//当前点匹配
return 1;//返回匹配成功
}
}
return 0;//否则返回匹配失败
}
void match()
{
for(int i=1;i<=n;++i)//枚举左边的每一个点
{
memset(vis,0,sizeof(vis));//因为当前点找过的点下一个点还可以找,所以每个点开始找的时候就清一下标记
if(dfs(i)) ans++;//匹配成功就累加
}
printf("%d",ans);
}
int main()
{
scanf("%d %d %d",&n,&m,&e);
for(int i=1;i<=e;++i)
{
int x,y;
scanf("%d %d",&x,&y);
add(x,y);
}
match();
return 0;
}
二分图最大权完美匹配
几个概念
二分图的最大权匹配:指二分图中边权和最大的匹配。
可行顶标:给每一个节点 \(i\) 分配一个权值 \(val[i]\) ,对于所有的边 \([u,v]\) 满足 \(w(u,v)\le val[u]+val[v]\) 。
相等子图:在一组可行顶标下原图的生成子图,包含所有点但只包含满足 \(w[u,v]\le val[u]+val[v]\) 的边 \((u,v)\) 。
。。。
我们不说这么高深的术语。
KM算法( \(O(n^3)\) )
我们举个例子来模拟一下算法过程。
有这么一张图:
首先,左边的每一个点有一个期待值,为它所连的所有边当中权值最大的边。
右边的每一个点的期待值都为 \(0\) 。(这就是可行顶标)
然后我们开始配对。
首先给左边的第一个点配对,配对的规则是两个点的期待值之和必须等于边权。
\(1-4\) ,\(4+3\ne 0\) ,不能匹配。
\(1-6\) ,\(4+0=4\) ,可以匹配, 此时 \(1-6\) 匹配。
然后我们给 \(2\) 号点配对。
根据配对规则, \(2\) 只能与 \(6\) 匹配,所以尝试让 \(1\) 号点换匹配对象,
但是 \(1\) 号点找不到匹配对象,所以 \(2\) 号点匹配失败。
参与匹配的点有 \(1\) , \(2\) , \(6\) 。
此时,我们只能降一下左边点的期待值才能成功匹配。
所降的值就是任意一个左点能换到的任意一个没有被选择的右点所需降低的最小值。
这里就是 \(1-4\) , \(2-4\) , \(2-5\) 的最小值,很明显是 \(1\) 。
我们把左边参与匹配的点的期待值降 \(1\) ,同时把右边的点的期待值加上 \(1\) ,重新匹配。
我们重新开始匹配第二个点,还是 \(1-6\) 已经匹配。
此时 \(2\) 号点与 \(4\) 号点匹配成功。
然后我们匹配 \(3\) 号点。
\(3\) 号点没有可以匹配的点。。。。。。
参与匹配的点只有 \(3\) 。
我们把 \(3\) 的期望值降 \(1\) ,重新匹配。
我们重新匹配第三个点, \(1-6\) 已匹配, \(2-4\) 已匹配。
\(3\) 只能和 \(6\) 匹配,要求 \(1\) 换匹配点,\(1\) 又只能和 \(4\) 匹配,要求 \(2\) 换匹配点, \(2\) 没有能换的点,匹配失败。
参与匹配的点有 \(1\) , \(2\) , \(3\) , \(4\) ,\(6\) 。
此时左边的点降 \(1\) ,右边的点加 \(1\) ,重新匹配。
我们重新匹配第三个点, \(1-6\) 已匹配, \(2-4\) 已匹配。
\(3\) 只能和 \(6\) 匹配,要求 \(1\) 换匹配点,\(1\) 又只能和 \(4\) 匹配,要求 \(2\) 换匹配点, \(2\) 此时可以与 \(5\) 匹配,匹配成功。
算法结束。
最大权完美匹配为 \(9\) 。
代码实现
这个算是模板题吧。
$ code $
/*
KM算法
date:2022.7.30
worked by respect_lowsmile
*/
#include<iostream>
#include<cstring>
using namespace std;
const int N=25;
const int INF=0x3f3f3f3f;
int num,n,del,ans;
int p[N][N],q[N][N],w[N][N],visl[N],mat[N],visr[N],vall[N],valr[N];
//visl,visr分别标记左边点和右边点是否走过,mat表示匹配的点的编号,w表示权值,vall,valr表示左边点和右边点的期待值(可行顶标)
int dfs(int now)//匈牙利算法改进
{
visl[now]=1;//标记左边点
for(int i=1;i<=n;++i)//枚举每个右边的点
{
if(visr[i]) continue;//没有边相连就返回
if(vall[now]+valr[i]==w[now][i])//如果满足配对条件
{
visr[i]=1;//标记右边的点
if(!mat[i]||dfs(mat[i]))//如果这个点没有配对或者这个点可以和别的右边的点配对
{
mat[i]=now;//配对
return 1;//配对成功
}
}
else del=min(del,vall[now]+valr[i]-w[now][i]);//更新要改变的期待值
}
return 0;//返回配对失败
}
void KM()//KM算法
{
for(int i=1;i<=n;++i)//更新权值和期待值
{
valr[i]=0,vall[i]=-INF;
for(int j=1;j<=n;++j)
vall[i]=max(vall[i],w[i][j]);
}
for(int i=1;i<=n;++i)//匹配每一个点
{
while(1)
{
memset(visl,0,sizeof(visl));//清楚标记
memset(visr,0,sizeof(visr));
del=INF;//初始化改变值
if(dfs(i)) break;//匹配成功就结束
for(int j=1;j<=n;++j)//相应地更新期待值
{
if(visl[j]) vall[j]-=del;
if(visr[j]) valr[j]+=del;
}
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
scanf("%d",&p[i][j]);
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
scanf("%d",&q[i][j]);
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
w[i][j]=p[i][j]*q[j][i];
KM();
for(int i=1;i<=n;++i) ans+=w[mat[i]][i];
printf("%d",ans);
return 0;
}