算法思想
二分图的定义:如果一个图的所有顶点可以分为
X
X
X和
Y
Y
Y两个集合,且对于所有边来说,边的两点都不属于同一集合,那么这个图就是二分图
更准确的定义如下:
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)为无向图,
V
V
V为点集,
E
E
E为边集,如果
V
V
V可被分割成两个互不相交的子集
(
V
1
,
V
2
)
(V_1,V_2)
(V1?,V2?),且图中每条边
(
i
,
j
)
(i,j)
(i,j)两端点分别属于这两个子集,则称
G
G
G为一个二分图
二分图的相关概念如下:
- 匹配:一个只有边的集合,集合中任意两条边无公共点,在一个图的所有匹配中,边数最多的匹配叫做该图的最大匹配
- 独立集:两两互不相连的节点集合
- 边覆盖:任意节点都至少是某条边端点的边集合,也就是不存在孤立点
- 点覆盖:任意边都至少有一个点的点集合,即不存在孤立边
- 最小路径覆盖:在一个DAG中,如果存在一个简单路集合(顶点不相交),图的每个顶点都在路上,则该路为一个路径覆盖,最小路径覆盖为所含路径条数最少的路径覆盖
二分图染色
对于给定的一个图,首先需要判断它是否为二分图才能进行相关操作,对于一个无向图来说,当且仅当不存在有奇数条边构成的环时,它才是二分图,这是充要条件,举个例子简单证明
如图,当为奇数环时,根据二分图的定义,可以看到?处无法被赋值
根据二分图的性质可以知道,所有的点被分成了两个集合,那么相连的两个点必然不属于同一集合,因此,可以从任意一点开始,将其设置为1,其邻接点必然为0,以此类推,如果最后出现无法赋值的点,那么必然不是二分图,可以用DFS实现
代码
bool DFS(int u,int c)
{
col[u]=c;
for(int i=head[u];~i;i=e[i].next)
{
int v=e[i].to;
if(col[v]==col[u])return 0;
if(col[v]==-1&&!DFS(v,!col[u]))return 0;
}
return 1;
}
当然,如果给的图不是连通图,那么就需要对每个未遍历点进行判断
for(int i=1;i<=n;i++)
if(col[i]==-1)if(!DFS(i,1))return 0;
二分图相关定理
- K?nig: 最小点覆盖=二分图最大匹配
- 二分图最小边覆盖: 顶点数-二分图最大匹配
- 最小路径覆盖: 顶点数-二分图最大匹配数
- 最大独立集: 总点数-二分图最大匹配数
求出最小路径覆盖集合只需要沿着增广路输出即可,输出所有的匹配路
代码
void print(int u)
{
u+=n;
do
printf("%d ",u=u-n);
while(vis[u]=1,u=match[u]);
}
for(int i=1;i<=n;i++)
if(!vis[i])print(i);
最小可相交路径覆盖:Floyd求原图传递闭包
最大匹配算法
首先说明最大匹配的概念,最大匹配指的是在二分图中包含边数最多的一组匹配,是二分图的一类问题,如图,给出了可匹配的关系,求最大匹配可以将原图转换为最大流问题,添加源点和汇点,边权设为1,然后直接跑最大流即可,时间复杂度为
O
(
V
2
E
)
O(V^2E)
O(V2E),但是,给出的图是二分图,可以利用二分图的性质求解,下面为用二分图性质求解的匈牙利算法和KM算法
匈牙利算法
首先给出交替路和增广路概念 交替路:从一个未匹配点出发,依次经过非匹配边,匹配边,非匹配边,…形成的路径为交替路 增广路:从一个未匹配点出发,走交替路,如果途径另一个与起点不同的未匹配点,则这条交替路则称为增广路 注意二分图与求解最大流中的增广路定义不同 给出一个例子进行简单的算法思路说明,这是一个简单的图例,给出了可匹配关系,从节点1开始寻找可匹配点,1可以找到4,之后2再找可匹配点5 但是对于节点3,它的唯一可匹配点只有4,4此时已经和1匹配,那么尝试将1与其他的节点匹配
1可以与5匹配,但是5已经与2匹配,那么尝试将2与其他的节点匹配 2可以与6匹配,所以最后就可以得到最大匹配的结果 这就是匈牙利算法基本思路,根据概念来看,增广路的存在代表可以通过修改当前匹配来增加新的匹配,增广路的本质就是一条路径的起点和终点都是未配对的点,在上述过程中,匹配关系有时需要更改,从已匹配变成未匹配,这个操作被称为“反色”,易证“反色”后的匹配仍然合法且大于等于原匹配
算法步骤:
- 初始化节点,从左集合中首个点$$开u始检查
- 检查
u
u
u的邻接点
v
v
v,若
v
v
v未匹配,直接匹配两点,否则从
v
v
v的邻接点出发找增广路,存在增广路就更新匹配关系,即反色,然后匹配
u
,
v
u,v
u,v
- 找不到最大匹配,即可得到一个最大匹配
代码
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(!match[v]||maxmatch(match[v]))
{
match[v]=u;
return 1;
}
}
}
return 0;
}
KM算法
KM算法的目的是求出二分图的最大权完美匹配,也就是二分图的两个点集大小需要相同,一般来说,二分图不可能保证两边一定大小相同,所以需要将个数少的一边补点,再将不存在的边权设为0
接下来是与KM相匹配的概念与定理
可行顶标: 给每个节点有个点权
f
(
i
)
f(i)
f(i),对于所有的边
u
?
v
u-v
u?v,满足所有的边权
w
(
u
,
v
)
≤
f
(
u
)
+
f
(
v
)
w(u,v)\le f(u)+f(v)
w(u,v)≤f(u)+f(v)
相等子图: 在一组可行顶标下原图的生成子图,包含所有点但只满足
w
(
u
,
v
)
=
f
(
u
)
+
f
(
v
)
w(u,v)= f(u)+f(v)
w(u,v)=f(u)+f(v)的边
u
?
v
u-v
u?v
如果相等子图有完美匹配,则其必然为原图的最大权完美匹配
根据定理,求解最大权完美匹配过程中,需要不断调整可行顶标,使得相等子图拥有完美匹配
一开始不可能直接确定好所有的顶标,所以需要反复确认,修改顶标,当一条边满足
w
(
u
,
v
)
=
f
(
u
)
+
f
(
v
)
w(u,v)= f(u)+f(v)
w(u,v)=f(u)+f(v),代表其为相等子图的一条边,将其加入图中
设二分图左点集合顶标为
l
(
i
)
l(i)
l(i),右集合顶标为
r
(
i
)
r(i)
r(i),初始化时,因为可行顶标需要满足点权和大于等于边权,不妨设
l
(
i
)
l(i)
l(i)全部为0,
r
(
i
)
r(i)
r(i)为对应点所连的边权最大值
调整顶标过程中,目的是构造一个相等子图,也就是不断的向原先构造的子图中添加新边,使得
l
(
i
)
+
r
(
j
)
=
w
(
i
,
j
)
l(i)+r(j)=w(i,j)
l(i)+r(j)=w(i,j),那么在初始化之后,需要找到一条边
u
?
v
u-v
u?v使得
u
u
u不在二分图最大匹配中,而
v
v
v在二分图最大匹配中(u点权为0,v点权为最大边权,为了获得最大匹配,默认以v为基准匹配)
对于这样的一条边,我们希望将其加入正在构造的相等子图,就要使得边满足条件,那么顶标和需要减少
△
x
=
l
(
u
)
+
r
(
v
)
?
w
(
u
,
v
)
△x=l(u)+r(v)-w(u,v)
△x=l(u)+r(v)?w(u,v)
因为默认
v
v
v在最大匹配中,改变其点权会影响其他的边(比如边权最大的那条边,如果
v
v
v点权变了,那么边权最大的那条边的两端点可能就不满足可行顶标的定义了),所以只对
u
u
u进行操作,也就是
l
(
u
)
+
△
x
l(u)+△x
l(u)+△x或者
r
(
u
)
?
△
x
r(u)-△x
r(u)?△x(
u
u
u不一定就在左边),为了放置修改完导致部分顶标不符合定义,所以每次找的
△
x
△x
△x尽量小
代码(假
O
(
n
3
)
O(n^3)
O(n3))
bool maxmatch(int u) {
visr[u]=1;
for(int i=1; i<=n; i++) {
if(visl[i])continue;
int d=l[i]+r[u]-gragh[u][i];
if(d==0) {
visl[u]=1;
if(!match[i]||maxmatch(match[i])) {
match[i]=u;
return 1;
}
} else delta[i]=min(delta[i],d);
}
return 0;
}
int KM() {
memset(match,0,sizeof(match));
memset(l,0,sizeof(l));
for(int i=1; i<=n; i++) {
r[i]=gragh[i][1];
for(int j=2; j<=n; j++)
r[i]=max(r[i],gragh[i][j]);
}
for(int i=1; i<=n; i++) {
for(int j=1; j<=n; j++)delta[j]=inf;
while(1) {
memset(visl,0,sizeof(visl));
memset(visr,0,sizeof(visr));
if(maxmatch(i))break;
int d=inf;
for(int j=1; j<=n; j++)
if(!visl[j])d=min(d,delta[j]);
for(int j=1; j<=n; j++) {
if(visr[j])r[j]-=d;
if(visl[j])l[j]+=d;
else delta[j]-=d;
}
}
}
for(int i=1; i<=n; i++)res+=gragh[match[i]][i];
return res;
}
二分图匹配的复杂度最差为
O
(
n
2
)
O(n^2)
O(n2)(maxmatch函数),而while(1)循环的最多可以到
O
(
n
)
O(n)
O(n)(尝试与所有节点进行构造匹配),因此整个算法的复杂度最高可达到
O
(
n
4
)
O(n^4)
O(n4)
可以发现,在修改边的时候,因为每次DFS的起点相同,所以可能每次只修改了一条边,因此有重复搜索的情况,那么将DFS改成BFS,就可以减少搜索的重复性,减少复杂度
代码(
O
(
n
3
)
O(n^3)
O(n3))
int pre[maxn],match[maxn],delta[maxn],graph[maxn][maxn],n,m,res,l[maxn],r[maxn];
int a[maxn],p[maxn],b[maxn],c[maxn];
bool visl[maxn],visr[maxn];
void maxmatch(int u) {
int x,y=0,d,id=0;
memset(pre,0,sizeof(pre));
for(int i=1; i<=n; i++)delta[i]=inf;
match[y]=u;
while(1) {
x=match[y];
d=inf;
visr[y]=1;
for(int i=1; i<=n; i++) {
if(visr[i])continue;
if(delta[i]>l[x]+r[i]-graph[x][i]) {
delta[i]=l[x]+r[i]-graph[x][i];
pre[i]=y;
}
if(delta[i]<d) {
d=delta[i];
id=i;
}
}
for(int i=0; i<=n; i++)
if(visr[i])l[match[i]]-=d,r[i]+=d;
else delta[i]-=d;
y=id;
if(match[y]==-1)break;
}
while(y) {
match[y]=match[pre[y]];
y=pre[y];
}
}
int KM() {
memset(match,-1,sizeof(match));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1; i<=n; i++) {
memset(visr,0,sizeof(visr));
maxmatch(i);
}
for(int i=1; i<=n; i++)
if(match[i]!=-1)res+=graph[match[i]][i];
return res;
}
二分图博弈
二分图博弈为一类博弈模型,与二分图的最大匹配有关,其基本模型如下:给出一张二分图和起点,博弈双方轮流操作,每次只能选择与上次被选择点相邻的点,且不能选择已经访问过的点,无法继续操作者判负
设起点为S,有一个结论:如果二分图的任意一个最大匹配一定有S,即S为最大匹配必须点,那么先手必胜,否则先手必败
下面来证明这个结论 如图,给出了一个最大匹配,假设起点S=1,那么按照策略,先手选择2,此时,后手要么无点可选,要么选3(如果2-3存在的话,之后会证明这一点),又到了先手从3开始选择,可以看到,只要先手一直按照寻找匹配点的策略选下去,到最后总会有后手无点可选
如果起点不属于最大匹配中,先手必输,因为先手从起点的下一步必是图中的匹配点之一(之后证明),那么情况和上一段完全相同,只不过是后手按照匹配点的策略来选,到最后总会使先手无点可选 现在进行更详细的证明,如果最大匹配一定包括S,按照先前所说,后手不可能选到最大匹配之外的点,如图,如果后手可以选到非匹配点,例如图中的9,那么图中的最大匹配方案就不止一个:1-2,3-4,5-6,7-8和1-2,4-9,5-6,7-8都可以作为最大匹配,违背了最大匹配一定包括S 如果最大匹配不一定包括S,在这种情况下,可能是有唯一最大匹配,但是S并不在最大匹配中,或者S并不是所有最大匹配的必须点,如图,从9开始,先手的下一步必然是匹配点,否则的话
就会出现下图所属的情况,存在一条边9-10不属于最大匹配,显然,这是不合理的,因为9和10都不属于匹配点,但是它们能够构成匹配,原先的最大匹配就不是最大的了,因此矛盾 接下来的问题就是如何确定起点是否属于最大匹配,方法很简单,直接对有起点和没有起点的原图做最大匹配,如果匹配数相同,代表起点不一定在最大匹配上,否则一定在最大匹配上,可以采用Dinic和匈牙利求解 跑Dinic时不用根据是否有起点建两次图,而是建图的时候保存涉及起点的边,跑完一次Dinic后再建边,查看Dinic是否增加流量
总结一下,对于二分图博弈,分两种大的情况来考虑(实际上是一种情况,但是这样分开更清晰)
- 有起点
有起点的情况下,需要判断起点是否一定属于最大匹配,即为最大匹配必须点,如果是,那么先手必胜,否则先手必输 - 无起点
无起点的情况下,先手需要选择起点,如果先手先选择最大匹配点,后手只需要选择对应的最大匹配的匹配点即可,先手必败,如果图为完全匹配,先手一定会选择最大匹配点,如果不是完全匹配,先手选择非最大匹配点或最大匹配非必须点,那么后手总会首先到最大匹配,所以先手可胜
训练
LuoguP1330
题目大意:略
思路:由题设可知,河蟹之间是不能相连的,那么所有的河蟹可以被划分成两个集合,这恰好符合二分图,那么直接判断给出的图是否是二分图即可,如果不是则无解,若是则选取颜色少的节点,由于题目不保证连通,所以要对每个连通块都进行二分图染色
代码
#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=4e5+10;
int n,m,cnt,res,head[maxn/10],col[maxn/10],dress[2];
struct node {
int next,to;
} e[maxn<<1];
void Add(int from,int to) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
bool DFS(int u,int c) {
col[u]=c;
dress[c]++;
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(col[v]==col[u])return 0;
if(col[v]==-1&&!DFS(v,!col[u]))return 0;
}
return 1;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
memset(head,-1,sizeof(head));
memset(col,-1,sizeof(col));
while(m--) {
int u,v;
cin >>u>>v;
Add(u,v);
Add(v,u);
}
for(int i=1; i<=n; i++)
if(col[i]==-1) {
if(!DFS(i,1)) {
res=0;
break;
}
res+=min(dress[0],dress[1]);
dress[0]=dress[1]=0;
}
res==0? cout <<"Impossible": cout <<res;
return 0;
}
LuoguP1285
题目大意:略
思路:很重要的一点,如果两个队员不是双向的,那么这两个人一定不能在同一组,因此,在建图的时候,每个点与必不能同一组的点建边,这样的话整个图就可以分成两个点集,也就是二分图,经过染色,整个图也就被分成的多个拥有黑白相间的连通块,显然,每个块只能取全黑或全白,这样的话,整个模型就可以抽象成01背包模型,有k个连通块,每个连通块要么全选黑,要么选白,要求最后分组的差最小,这里的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]可以解释为在第i个连通块上,能否选j个人,其余见代码
代码
#include <bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
int n,acc[121][2],pre[121][121],res,col[121],block,out[121];
bool gragh[121][121],dp[121][121],mark[121];
vector<int>path[121][2];
void DFS(int u,int f,int c) {
acc[block][col[u]=c]++;
path[block][c].push_back(u);
for(int i=1; i<=n; i++)
if(!gragh[i][u]&&i!=f&&u!=i) {
if(col[i]==-1)DFS(i,u,c^1);
else if(col[i]==c) {
cout <<"No solution";
exit(0);
}
}
}
int main() {
cin >>n;
memset(col,-1,sizeof(col));
for(int i=1; i<=n; i++) {
int k;
while(cin >>k&&k)gragh[i][k]=1;
}
for(int i=1; i<=n; i++)
for(int j=i+1; j<=n; j++)
if(gragh[i][j]!=gragh[j][i])gragh[i][j]=gragh[j][i]=0;
for(int i=1; i<=n; i++)
if(col[i]==-1) {
block++;
DFS(i,0,1);
}
dp[0][0]=1;
for(int i=1; i<=block; i++)
for(int j=0; j<=n/2; j++) {
int p=j-acc[i][0];
if(p>=0&&dp[i-1][p])dp[i][j]=1,pre[i][j]=0;
p=j-acc[i][1];
if(p>=0&&dp[i-1][p])dp[i][j]=1,pre[i][j]=1;
}
for(int i=n/2; i>=0; i--)
if(dp[block][i]) {
res=i;
break;
}
int t=res;
for(int i=block; i>0; i--) {
for(int j=0; j<path[i][pre[i][t]].size(); j++)
mark[out[++out[0]]=path[i][pre[i][t]][j]]=1;
t-=acc[i][pre[i][t]];
}
sort(out+1,out+out[0]+1);
cout <<out[0]<<" ";
for(int i=1; i<=out[0]; i++)
cout <<out[i]<<" ";
cout <<endl<<n-out[0]<<" ";
for(int i=1; i<=n; i++)
if(!mark[i])cout <<i<<" ";
return 0;
}
POJ1274
题目大意:牛棚与牛直接存在对应关系,每只牛对牛棚有自己的喜好,一个牛棚只能分配给一头牛,一头牛只能分给一个牛棚,计算最多能将牛分配给牛棚多少头
思路:二分图问题,根据对应关系构造二分图然后用匈牙利算法算最大匹配即可
代码
#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=4e4+10;
int n,m,cnt,head[500],match[500];
struct node {
int next,to;
} e[maxn<<1];
void Add(int from,int to) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
bool vis[500];
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(!match[v]||maxmatch(match[v]))
{
match[v]=u;
return 1;
}
}
}
return 0;
}
int main() {
while(~scanf("%d%d",&n,&m)) {
int res=0;
memset(head,-1,sizeof(head));
memset(match,0,sizeof(match));
cnt=0;
for(int i=1; i<=n; i++) {
int s;
scanf("%d",&s);
while(s--) {
int x;
scanf("%d",&x);
Add(i,x+n);
}
}
for(int i=1; i<=n; i++) {
memset(vis,0,sizeof(vis));
if(maxmatch(i))res++;
}
printf("%d\n",res);
}
return 0;
}
POJ1325
题目大意:给出两条机器A,B,A有n种工作模式(0~ n-1),B有m种(0~ m-1),一开始都在工作模式0下工作,现在给定k个作业,给出每个作业三元组约束
(
i
,
x
,
y
)
(i,x,y)
(i,x,y),表示可以在A上以x处理或者在B上以y处理,完成工作需要重启以修改机器工作模式,需要安排作业的顺序并分配适当机器使重启次数最少,输出最少的次数
思路:一开始在工作模式0工作,因此可以在工作公式0工作的可忽略,求最少的重启次数,等价于求二分图的最小点覆盖数,等价于求最大匹配,跑匈牙利算法即可
代码
#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=4e4+10;
int n,m,cnt,k,head[500],match[500];
struct node {
int next,to;
} e[maxn<<1];
void Add(int from,int to) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
bool vis[500];
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(!match[v]||maxmatch(match[v])) {
match[v]=u;
return 1;
}
}
}
return 0;
}
int main() {
while(~scanf("%d",&n)&&n) {
scanf("%d%d",&m,&k);
int res=0;
memset(head,-1,sizeof(head));
memset(match,0,sizeof(match));
cnt=0;
while(k--) {
int i,x,y;
scanf("%d%d%d",&i,&x,&y);
if(x==0||y==0)continue;
Add(x,y+n);
}
for(int i=0; i<n; i++) {
memset(vis,0,sizeof(vis));
if(maxmatch(i))res++;
}
printf("%d\n",res);
}
return 0;
}
LuoguP1263
题目大意:略
思路:如果没有墙,把每一行看做左集合,列为右集合,对于空地将所在行与列连边,匈牙利一遍即可,但是墙将行列割裂开来,因此需要将割裂产生的连通块视做单独的个体,也就是修改遍历范围,具体见代码
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e4+10;
int m,n,cnt,maze[250][250],match[maxn],head[maxn],res,acch,accz;
int col[maxn],parth[250][250],partz[250][250];
bool vis[maxn];
struct node {
int next,to;
} e[maxn<<1];
void Add(int from,int to) {
e[++cnt].next=head[from];
e[cnt].to=to;
head[from]=cnt;
}
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(!match[v]||maxmatch(match[v])) {
match[v]=u;
return 1;
}
}
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
memset(head,-1,sizeof(head));
memset(col,-1,sizeof(col));
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
cin >>maze[i][j];
for(int i=1; i<=n; i++)maze[i][0]=2;
for(int i=1; i<=m; i++)maze[0][i]=2;
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]<2) {
if(maze[i][j-1]>1)parth[i][j]=++acch;
else parth[i][j]=parth[i][j-1];
}
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]<2) {
if(maze[i-1][j]>1)partz[i][j]=++accz;
else partz[i][j]=partz[i-1][j];
}
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]==0)Add(parth[i][j],partz[i][j]);
for(int i=1; i<=acch; i++) {
for(int j=1; j<=accz; j++)vis[j]=0;
res+=maxmatch(i);
}
cout <<res<<endl;
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]==0&&parth[i][j]==match[partz[i][j]])cout <<i<<" "<<j<<endl;
return 0;
}
2019icpc南京J
题目大意:略
思路:KM模板题,但是需要用到真正的
O
(
n
3
)
O(n^3)
O(n3)模板
代码
#include <bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=512;
int pre[maxn],match[maxn],delta[maxn],graph[maxn][maxn],n,res,l[maxn],r[maxn];
int a[maxn],p[maxn],b[maxn],c[maxn];
bool visl[maxn],visr[maxn];
void maxmatch(int u) {
int x,y=0,d,id=0;
memset(pre,0,sizeof(pre));
for(int i=1; i<=n; i++)delta[i]=inf;
match[y]=u;
while(1) {
x=match[y];
d=inf;
visr[y]=1;
for(int i=1; i<=n; i++) {
if(visr[i])continue;
if(delta[i]>l[x]+r[i]-graph[x][i]) {
delta[i]=l[x]+r[i]-graph[x][i];
pre[i]=y;
}
if(delta[i]<d) {
d=delta[i];
id=i;
}
}
for(int i=0; i<=n; i++)
if(visr[i])l[match[i]]-=d,r[i]+=d;
else delta[i]-=d;
y=id;
if(match[y]==-1)break;
}
while(y) {
match[y]=match[pre[y]];
y=pre[y];
}
}
int KM() {
memset(match,-1,sizeof(match));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1; i<=n; i++) {
memset(visr,0,sizeof(visr));
maxmatch(i);
}
for(int i=1; i<=n; i++)
if(match[i]!=-1)res+=graph[match[i]][i];
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++)cin >>a[i];
for(int i=1; i<=n; i++)cin >>p[i];
for(int i=1; i<=n; i++)cin >>b[i];
for(int i=1; i<=n; i++)cin >>c[i];
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
for(int k=1; k<=n; k++)
if(b[i]+c[j]>a[k])graph[i][j]+=p[k];
cout <<KM();
return 0;
}
LuoguP6577
题目大意:略
思路:KM模板题
代码
#include <bits/stdc++.h>
#define int long long
#define inf 1e18
using namespace std;
const int maxn=512;
int pre[maxn],match[maxn],delta[maxn],graph[maxn][maxn],n,m,res,l[maxn],r[maxn];
int a[maxn],p[maxn],b[maxn],c[maxn];
bool visl[maxn],visr[maxn];
void maxmatch(int u) {
int x,y=0,d,id=0;
memset(pre,0,sizeof(pre));
for(int i=1; i<=n; i++)delta[i]=inf;
match[y]=u;
while(1) {
x=match[y];
d=inf;
visr[y]=1;
for(int i=1; i<=n; i++) {
if(visr[i])continue;
if(delta[i]>l[x]+r[i]-graph[x][i]) {
delta[i]=l[x]+r[i]-graph[x][i];
pre[i]=y;
}
if(delta[i]<d) {
d=delta[i];
id=i;
}
}
for(int i=0; i<=n; i++)
if(visr[i])l[match[i]]-=d,r[i]+=d;
else delta[i]-=d;
y=id;
if(match[y]==-1)break;
}
while(y) {
match[y]=match[pre[y]];
y=pre[y];
}
}
int KM() {
memset(match,-1,sizeof(match));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1; i<=n; i++) {
memset(visr,0,sizeof(visr));
maxmatch(i);
}
for(int i=1; i<=n; i++)
if(match[i]!=-1)res+=graph[match[i]][i];
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
graph[i][j]=-inf;
while(m--) {
int x,y,w;
cin >>x>>y>>w;
graph[x][y]=w;
}
cout <<KM()<<endl;
for(int i=1; i<=n; i++)
cout <<match[i]<<" ";
return 0;
}
LuoguP4014
题目大意:略
思路:最大总收益直接用KM求解,最小收益需要对边权取反然后跑KM,跑完之后对结果取反即可
代码
#include <bits/stdc++.h>
#define int long long
#define inf 1e18
using namespace std;
const int maxn=512;
int pre[maxn],match[maxn],delta[maxn],graph[maxn][maxn],n,m,res,l[maxn],r[maxn];
bool visl[maxn],visr[maxn];
void maxmatch(int u) {
int x,y=0,d,id=0;
memset(pre,0,sizeof(pre));
for(int i=1; i<=n; i++)delta[i]=inf;
match[y]=u;
while(1) {
x=match[y];
d=inf;
visr[y]=1;
for(int i=1; i<=n; i++) {
if(visr[i])continue;
if(delta[i]>l[x]+r[i]-graph[x][i]) {
delta[i]=l[x]+r[i]-graph[x][i];
pre[i]=y;
}
if(delta[i]<d) {
d=delta[i];
id=i;
}
}
for(int i=0; i<=n; i++)
if(visr[i])l[match[i]]-=d,r[i]+=d;
else delta[i]-=d;
y=id;
if(match[y]==-1)break;
}
while(y) {
match[y]=match[pre[y]];
y=pre[y];
}
}
int KM() {
memset(match,-1,sizeof(match));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
for(int i=1; i<=n; i++) {
memset(visr,0,sizeof(visr));
maxmatch(i);
}
for(int i=1; i<=n; i++)
if(match[i]!=-1)res+=graph[match[i]][i];
return res;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
cin >>graph[i][j];
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
graph[i][j]*=-1;
cout <<-KM()<<endl;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
graph[i][j]*=-1;
res=0;
cout <<KM()<<endl;
return 0;
}
LuoguP1971
题目大意:略
思路:利用相对位移的思想,将棋子的移动视为空格的移动,那么先手将空格移动到白子,这与黑子的性质类似,因此将空格看做黑,这样整个迷宫就可以转换成黑白两子间两两匹配的问题,黑白染色,建二分图,根据二分图博弈的思路,分别判断当前移动的点位置在移动前是否为最大匹配必须点,如果是,当前手必赢,否则必输
判断当前点位置是否为最大匹配必须点,可以将点从图中删掉并删掉原先的匹配关系,查看原匹配关系的另一个点是否能组成另一个最大匹配,如果能,代表当前点并不是必须点,则当前手必输(按照最优策略的话),因为当前手下一步必然入最大匹配,记录每一步的当前手是否必输必赢,判断一开始的先手是否错误,需要判断第k轮是否先手必胜而下完后后手必胜,即两个状态都为1,代表这一步先手错了,记录即可
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5e3+10;
int n,m,k,sx,sy,cnt,head[maxn],match[maxn];
vector<int>res;
char maze[50][50];
bool color[50][50],vis[maxn/2],block[maxn/2],win[maxn/2];
struct node {
int next,to;
} e[maxn];
void Add(int from,int to) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
int Hash(int i,int j) {
return (i-1)*m+j;
}
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(block[v])continue;
if(!vis[v]) {
vis[v]=1;
if(!match[v]||maxmatch(match[v])) {
match[v]=u;
match[u]=v;
return 1;
}
}
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
memset(head,-1,sizeof(head));
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++) {
cin >>maze[i][j];
if(maze[i][j]=='.')
sx=i,sy=j;
else if(maze[i][j]=='O')
color[i][j]=1;
}
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(color[i][j]) {
if(i+1<=n&&!color[i+1][j]) {
Add(Hash(i,j),Hash(i+1,j));
Add(Hash(i+1,j),Hash(i,j));
}
if(j+1<=m&&!color[i][j+1]) {
Add(Hash(i,j+1),Hash(i,j));
Add(Hash(i,j),Hash(i,j+1));
}
if(i>1&&!color[i-1][j]) {
Add(Hash(i,j),Hash(i-1,j));
Add(Hash(i-1,j),Hash(i,j));
}
if(j>1&&!color[i][j-1]) {
Add(Hash(i,j),Hash(i,j-1));
Add(Hash(i,j-1),Hash(i,j));
}
}
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(color[i][j]&&!match[Hash(i,j)]) {
memset(vis,0,sizeof(vis));
maxmatch(Hash(i,j));
}
cin >>k;
for(int i=1; i<=2*k; i++) {
int id=Hash(sx,sy);
block[id]=1;
if(match[id]==0)
win[i]=0;
else {
int t=match[id];
match[id]=match[t]=0;
memset(vis,0,sizeof(vis));
if(maxmatch(t))win[i]=0;
else win[i]=1;
}
cin >>sx>>sy;
}
for(int i=1; i<=k; i++)
if(win[2*i-1]&&win[2*i])
res.push_back(i);
int len=res.size();
cout <<len<<endl;
for(int i=0; i<len; i++)
cout <<res[i]<<endl;
return 0;
}
LuoguP4055
题目大意:略
思路:首先声明一下:
该题数据有问题!该题数据有问题!该题数据有问题!
数据忽略了类似下面两种的数据
3 3 3 3
.## .##
.## .##
..# #..
因此,洛谷上的部分题解可能思路是对的,但是代码从逻辑上是错的,下面给出的代码我自己也不能保证就是对的,因为没有更全面的数据
下面是思路
根据题意,相邻的可行格显然可以连边,那么采用黑白染色的方法将整个可行域分成黑白两部分,这样就可以构造一个二分图了,题目也就转变成了二分图博弈,但是本题的先手是先选点,不是从起点出发 为了先手必胜,那么先手就需要使后手走之后使当前点为最大匹配的起点,这样就和二分图博弈的模型相符合了,那么先手需要找的点就是与最大匹配相连,但可以不属于最大匹配的点,如果整个图是完全匹配,显然是没有这样的点的
最大匹配可能有多个,如果先手走了某个最大匹配的必须点,显然后手就从博弈模型中的“起点”开始走了,先手会输,所以,先手需要走所有最大匹配都非必须的点
为了找所有最大匹配的必须点,可以从一个非必须点开始搜索,假设其颜色为黑,如果经过一个白点搜索还能搜到黑,那么另一个黑点必然也是非必须点,因为这两个点等价,那么它们与公共点组成的边就可以替换原图中最大匹配的一条边,可以画图尝试
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m,match[maxn],head[maxn],cnt,ans,res,color[maxn];
int point[1212][1212];
char maze[1212][1212];
bool vis[maxn],une[maxn];
struct node {
int next,to;
} e[maxn];
void Add(int from,int to) {
e[++cnt].to=to;
e[cnt].next=head[from];
head[from]=cnt;
}
bool maxmatch(int u) {
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(!vis[v]) {
vis[v]=1;
if(match[v]==-1||maxmatch(match[v])) {
match[v]=u;
return 1;
}
}
}
return 0;
}
void DFS(int u) {
if(une[u])return ;
une[u]=1;
for(int i=head[u]; ~i; i=e[i].next) {
int v=e[i].to;
if(match[v]!=-1)
DFS(match[v]);
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
memset(match,-1,sizeof(match));
memset(head,-1,sizeof(head));
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
cin >>maze[i][j];
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]=='.') {
point[i][j]=++ans;
if((i+j)%2==0)color[ans]=1;
}
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(maze[i][j]=='.') {
if(point[i+1][j]) {
Add(point[i][j],point[i+1][j]);
Add(point[i+1][j],point[i][j]);
}
if(point[i][j+1]) {
Add(point[i][j],point[i][j+1]);
Add(point[i][j+1],point[i][j]);
}
}
for(int i=1; i<=ans; i++)
if(color[i]) {
memset(vis,0,sizeof(vis));
if(match[i]==-1)
res+=maxmatch(i);
}
if(cnt/2==res||(ans/2==res&&cnt/2+1==ans)) {
cout <<"LOSE"<<endl;
return 0;
}
cout <<"WIN"<<endl;
for(int i=1; i<=ans; i++)
if(match[i]!=-1)match[match[i]]=i;
for(int i=1; i<=ans; i++)if(match[i]==-1)DFS(i);
for(int i=1; i<=n; i++)
for(int j=1; j<=m; j++)
if(une[point[i][j]])
cout <<i<<" "<<j<<endl;
return 0;
}
总结
二分图最基础的知识点就是最大匹配和染色,其余的知识点都是在这两点上延伸拓展的,需要深刻理解,由LuoguP1285可以看到,二分图可以将整个图变成一个个具有双重性质的连通块,而如果将这些连通块套入背包模型,便可以用DP的思路来解决问题,这是非常巧妙的 二分图,准确来说,是一种思想,一种思维,如何将问题转换成二分图,如何套用二分图模型解决问题,这是需要掌握的
KM算法是最大匹配的拓展,确切的来说,它利用了顶标与边权来进行取值约束,通过不断的修改顶标间接的修改所取的边与匹配关系,最后获得最大权匹配
二分图的问题大多数可以用网络流的其他算法来解决,但是由于二分图的特殊性,二分图本身的相关算法还是更加适配自身的问题
参考文献
- 《啊哈算法》
- 图论 —— 二分图
- 《算法训练营:海量图解+竞赛刷题》
- 二分图学习笔记
- 2019浙师大暑期ACM集训第二讲 图论(二) 二分图和网络流
- 「二分图」学习笔记
- P1285 队员分组 题解
- P1263 [CEOI2002] Royal guards 题解
- 同济大学2020ICPC暑假训练 8月16日 二分图及其应用
- 算法学习笔记(74): 二分图博弈
- 二分图博弈算法讲解及证明 (21夏 学习笔记)
- 1971兔兔与蛋蛋题解
|