二分图 系列
二分图
简单地,图 GGG 被分为 X,YX,YX,Y 两个集合(分为左部、右部),两部分之间的点才连边。这样的图叫做二分图。
匹配
XXX 部分点连向 YYY 部分点,每个点度数至多为 111,使得 (x,y)(x,y)(x,y) 配对。(x∈X,y∈Yx\in X,y\in Yx∈X,y∈Y)
二分图匹配用匈牙利算法,核心思想如上增广染色的动图,具体解释可前往这里。
1.洛谷 P3386 【模板】二分图最大匹配
匈牙利算法代码示例。
代码1
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1009;
ll n,m,o,u,v;
ll match[N],ans;
bool vis[N];
vector<ll>G[N];
bool dfs(ll u)
{for(auto v:G[u]){if(vis[v])continue;vis[v]=1;if(match[v]==0||dfs(match[v]))//当前无匹配或增广成功 {match[v]=u;return 1;}}return 0;
}
int main()
{scanf("%lld%lld%lld",&n,&m,&o);for(int i=1;i<=o;i++){scanf("%lld%lld",&u,&v);G[u].push_back(v);}for(int i=1;i<=n;i++){memset(vis,0,sizeof(vis));if(dfs(i))ans++;}printf("%lld",ans);return 0;
}
代码2
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1009;
ll n,m,o,u,v;
ll vis[N],match[N],ans;
vector<ll>G[N];
bool dfs(ll u,ll flag)
{if(vis[u]==flag)return 0;vis[u]=flag;for(auto v:G[u]){if(match[v]==0||dfs(match[v],flag))//当前无匹配或增广成功 {match[v]=u;return 1;}}return 0;
}
int main()
{scanf("%lld%lld%lld",&n,&m,&o);for(int i=1;i<=o;i++){scanf("%lld%lld",&u,&v);G[u].push_back(v);}for(int i=1;i<=n;i++)if(dfs(i,i))ans++;printf("%lld",ans);return 0;
}
二分图匹配是基础的,重要的是建模。
这场测试的 F.F.F. 用到了二分图匹配。
我的题单。接下来借题目讲一些典型的 trick。
2.洛谷 P2071 座位安排
题意
车上有 nnn 排座位,有 2n2n2n 个人参加省赛。每排座位只能坐两人,且每个人都有自己想坐的排数。问最多能使多少人坐到自己想坐的位置。
1≤n≤20001\le n\le 20001≤n≤2000。
思路
这个建模很裸啊!左边放人,右边放座位。每排座位两个座位?那就直接建两个点就好了。一个人想坐 444 个位置嘛。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e5+9;
ll n,m,u,v;
ll vis[N],match[N],ans;
ll idx,head[N];
struct edge
{ll to,next;
}e[N<<1];
void addedge(ll u,ll v)
{idx++;e[idx].to=v;e[idx].next=head[u];head[u]=idx;
}
bool dfs(ll u,ll flag)
{if(vis[u]==flag)return 0;vis[u]=flag;for(int i=head[u];i;i=e[i].next){ll v=e[i].to;if(match[v]==0||dfs(match[v],flag)){match[v]=u;return 1;}}return 0;
}
int main()
{scanf("%lld",&n);m=2*n;for(int i=1;i<=m;i++){scanf("%lld%lld",&u,&v);//左边放人,右边作为,二分图匹配 addedge(i,u+n*3);//每个座位两个人,建两个座位 addedge(i,u+n*4);addedge(i,v+n*3);addedge(i,v+n*4);}for(int i=1;i<=m;i++){memset(vis,0,sizeof(vis));if(dfs(i,i))ans++;}printf("%lld",ans);return 0;
}
3.洛谷 P1129 ZJOI2007 矩阵游戏
题意
小 Q 是一个非常聪明的孩子,除了国际象棋,他还很喜欢玩一个电脑益智游戏――矩阵游戏。矩阵游戏在一个 n×nn \times nn×n 黑白方阵进行(如同国际象棋一般,只是颜色是随意的)。每次可以对该矩阵进行两种操作:
- 行交换操作:选择矩阵的任意两行,交换这两行(即交换对应格子的颜色)。
- 列交换操作:选择矩阵的任意两列,交换这两列(即交换对应格子的颜色)。
游戏的目标,即通过若干次操作,使得方阵的主对角线(左上角到右下角的连线)上的格子均为黑色。
对于某些关卡,小 Q 百思不得其解,以致他开始怀疑这些关卡是不是根本就是无解的!于是小 Q 决定写一个程序来判断这些关卡是否有解。
1≤n≤2001 \leq n \leq 2001≤n≤200,1≤T≤201 \leq T \leq 201≤T≤20。
思路
把行看作左部,列看作右部,考虑二分图匹配。交换行、或者列,实质上不改变二分图匹配对数。
使得对角线上 nnn 个格子都有,也即最大匹配数 ≥n\ge n≥n。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=309;
ll T,n,a;
ll vis[N],match[N];
vector<ll>G[N];
bool dfs(ll u,ll flag)
{if(vis[u]==flag)return 0;vis[u]=flag;for(auto v:G[u]){if(match[v]==0||dfs(match[v],flag)){match[v]=u;return 1;}}return 0;
}
int main()
{scanf("%lld",&T);while(T--){scanf("%lld",&n);for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){scanf("%lld",&a);if(a)G[i].push_back(j);}}ll ans=0;for(int i=1;i<=n;i++)if(dfs(i,i))ans++;if(ans>=n)printf("Yes\n");else printf("No\n");for(int i=1;i<=n;i++)G[i].clear();memset(match,0,sizeof(match));memset(vis,0,sizeof(vis));}return 0;
}
交替染色
下面的题目主要会用到交替染色的操作,广义上,也是一种二分图思想。一般这些题都是很巧妙和“有趣”。
下面是一段交替染色(0/10/10/1),要求相邻颜色不能相同的 dfs 代码。
void dfs(ll u,ll c)
{vis[u]=1;col[u]=c;for(int i=head[u];i;i=e[i].next){ll v=e[i].to;if(!vis[v])dfs(v,c^1);else if(col[u]==col[v])sayno}
}
4.AT_abc282_d Make Bipartite 2
题意
给定一个包含 NNN 个顶点和 MMM 条边的简单无向图 GGG(即不包含自环和重边)。对于 i=1,2,…,Mi = 1, 2, \ldots, Mi=1,2,…,M,第 iii 条边连接顶点 uiu_iui 和顶点 viv_ivi。
请输出满足下列两个条件的整数对 (u,v)(u, v)(u,v) 的个数,其中 1≤u<v≤N1 \leq u < v \leq N1≤u<v≤N:
- 在图 GGG 中,顶点 uuu 和顶点 vvv 之间不存在边。
- 在图 GGG 中添加一条连接顶点 uuu 和顶点 vvv 的边后,所得的图仍然是二分图。
什么是二分图?无向图被称为二分图,当且仅当可以将所有顶点染成黑色或白色,使得不存在连接同色顶点的边。
2≤N≤2×1052 \leq N \leq 2 \times 10^52≤N≤2×105,0≤M≤min{2×105,N(N−1)/2}0 \leq M \leq \min\{2 \times 10^5, N(N-1)/2\}0≤M≤min{2×105,N(N−1)/2},1≤ui,vi≤N1 \leq u_i, v_i \leq N1≤ui,vi≤N。
思路
直接相邻点染黑白呢。
连通块内染黑白,然后黑点白点之间乘法原理连边。也可以把当前连通块连接外面的其它点,变成异色点。最后减去原来就有的 mmm 即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9;
ll n,m;
vector<ll>G[N];
bool vis[N];
ll col[N],cnt0,cnt1;
void dfs(ll u,ll c)
{vis[u]=1;col[u]=c;cnt0+=(c==0);cnt1+=(c==1);for(auto v:G[u]){if(!vis[v])dfs(v,c^1);else if(col[u]==col[v]){puts("0");exit(0);}}
}
int main()
{scanf("%lld%lld",&n,&m);for(int i=1;i<=m;i++){ll u,v;scanf("%lld%lld",&u,&v);G[u].push_back(v);G[v].push_back(u);}ll lnk=0,crk=0;for(int i=1;i<=n;i++){if(!vis[i]){cnt0=cnt1=0;dfs(i,0);lnk+=cnt0*cnt1;//连通块内黑白 crk+=(cnt0+cnt1)*(n-cnt0-cnt1);//块内连块外 }}printf("%lld",lnk+crk/2-m);//里面和外面、外面和里面都被重复计数了,记得/2return 0;
}
5.AT_arc165_c Social Distance on Graph
题意
有一个包含 NNN 个顶点的简单连通无向图,顶点编号为 111 到 NNN。图中有 MMM 条带权无向边,第 iii 条边连接顶点 AiA_iAi 和 BiB_iBi,权值为 WiW_iWi。对于连接两个顶点的简单路径,其权值定义为路径上所有边权的总和。
现在要对每个顶点染成红色或蓝色。请你求出满足以下条件的整数 XXX 的最大值:
- 对于任意两个染成相同颜色的不同顶点,它们之间的任意一条简单路径的权值都不少于 XXX。
简单路径的定义如下:对于图 GGG 上的顶点 X,YX,YX,Y,顶点序列 v1,v2,…,vkv_1,v_2,\ldots,v_kv1,v2,…,vk,满足 v1=Xv_1=Xv1=X,vk=Yv_k=Yvk=Y,且对于 1≤i≤k−11\leq i\leq k-11≤i≤k−1,viv_ivi 和 vi+1v_{i+1}vi+1 之间有边连接,这样的序列称为从 XXX 到 YYY 的步行。如果 v1,v2,…,vkv_1,v_2,\ldots,v_kv1,v2,…,vk 均互不相同,则称为从 XXX 到 YYY 的简单路径(或简称路径)。
3≤N≤2×1053 \leq N \leq 2 \times 10^53≤N≤2×105,N−1≤M≤min(N(N−1)2,2×105)N-1 \leq M \leq \min\left(\frac{N(N-1)}{2}, 2 \times 10^5\right)N−1≤M≤min(2N(N−1),2×105),1≤Ai<Bi≤N1 \leq A_i < B_i \leq N1≤Ai<Bi≤N,1≤Wi≤1091 \leq W_i \leq 10^91≤Wi≤109。
思路
求最小值的最大类型,考虑二分限制 mid=Xmid=Xmid=X。发现有边权 w<midw<midw<mid 就把它染成异色边。如此检验即可。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+9,inf=1e9+2,Inf=2e9;
ll n,m;
struct edge
{ll to,next,w;
}e[N<<1];
ll idx,head[N];
void addedge(ll u,ll v,ll w)
{idx++;e[idx].to=v;e[idx].next=head[u];e[idx].w=w;head[u]=idx;
}
ll mi[N],ci[N];
ll col[N];
bool vis[N],flag;
void dfs(ll u,ll c,ll dist)
{vis[u]=1;col[u]=c;for(int i=head[u];i;i=e[i].next){ll v=e[i].to,w=e[i].w;if(!vis[v]){if(w<dist)dfs(v,c^1,dist);}else if(w<dist&&col[u]==col[v]){flag=0;}}
}
bool check(ll mid)
{for(int i=1;i<=n;i++)vis[i]=0,col[i]=-1;flag=1;for(int i=1;i<=n;i++){if(!vis[i])dfs(i,0,mid);}return flag;
}
int main()
{scanf("%lld%lld",&n,&m);for(int i=1;i<=n;i++)mi[i]=ci[i]=inf;for(int i=1;i<=m;i++){ll u,v,w;scanf("%lld%lld%lld",&u,&v,&w);addedge(u,v,w);addedge(v,u,w);if(w<mi[u])ci[u]=mi[u],mi[u]=w;else if(w<ci[u])ci[u]=w;if(w<mi[v])ci[v]=mi[v],mi[v]=w;else if(w<ci[v])ci[v]=w;}ll l=0,r=Inf,ans=0;for(int i=1;i<=n;i++)if(ci[i]!=inf)r=min(r,mi[i]+ci[i]);while(l<=r){ll mid=(l+r)>>1;if(check(mid))ans=mid,l=mid+1;else r=mid-1;}printf("%lld",ans);return 0;
}
6.CF1354E Graph Coloring
题意
给定一个无自环、无重边的无向图,该图包含 nnn 个顶点和 mmm 条边。同时给定三个整数 n1n_1n1、n2n_2n2 和 n3n_3n3。
你能否将每个顶点标记为数字 1、2 或 3,使得:
- 每个顶点恰好被标记为 1、2 或 3 中的一个数字;
- 标记为 1 的顶点总数恰好为 n1n_1n1;
- 标记为 2 的顶点总数恰好为 n2n_2n2;
- 标记为 3 的顶点总数恰好为 n3n_3n3;
- 对于每条边 (u,v)(u, v)(u,v),有 ∣colu−colv∣=1|col_u - col_v| = 1∣colu−colv∣=1,其中 colxcol_xcolx 表示顶点 xxx 的标记。
如果存在合法的标记方案,第一行输出 YES
,下一行输出方案(如果存在多种合法的标记方案,输出任意一种即可);如果不存在合法方案,输出 NO
。
1≤n≤50001 \le n \le 50001≤n≤5000,0≤m≤1050 \le m \le 10^50≤m≤105。
思路
不难发现 1,31,31,3 点对于相邻的 222 点都是等效的。于是用 1,21,21,2 给原图交替染色,类似二分图思想。保证现在每条边都能做到 ∣colu−colv∣=∣2−1∣=1|col_u-col_v|=|2-1|=1∣colu−colv∣=∣2−1∣=1。
交替染色后,发现有 tottottot 个连通块,已知每个连通块黑点、白点的数量(二者可互换,因为不同连通块之间的黑白点不会相互干预),就用这两种颜色的数量进行 01 背包,设 fi,jf_{i,j}fi,j 表示前 iii 个连通块能否凑出 jjj 个 222 点,看看是否能够凑出 n2n_2n2 个 222 点。
f[0][0]=1;
for(int i=1;i<=tot;i++)
{for(int j=tar[2];j>=0;j--)//tar[2]:2点数量{//01背包,先物品后体积实现有序入背包if(j>=blk[i].size())f[i][j]|=f[i-1][j-blk[i].size()];if(j>=wht[i].size())f[i][j]|=f[i-1][j-wht[i].size()];}
}
if(!f[tot][tar[2]])sayno
题目还要求给出方案,这个也是好做的。我们根据转移得到的 fff,倒序考虑前 iii 个连通块进行回溯。如果用黑点作为 222 点可以合法回溯就用黑点,把该连通块黑点全部变成 222 点,否则用白点,因为只需要一种方案所以先后顺序不影响正确性。
for(int i=1;i<=n;i++)
ans[i]=1;//先默认全是1点
for(int i=tot;i>=1;i--)
{if(tar[2]>=blk[i].size()&&f[i-1][tar[2]-blk[i].size()])//用黑点可以回溯{tar[2]-=blk[i].size();for(auto x:blk[i])ans[x]=2;}else if(tar[2]>=wht[i].size()&&f[i-1][tar[2]-wht[i].size()])//白点{tar[2]-=wht[i].size();for(auto x:wht[i])ans[x]=2;}else sayno
}
if(tar[2]>0)sayno
最后输出方案即可,111 的个数满足了就用 333,上文说过 1,31,31,3 是等效的。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define sayno {puts("NO");exit(0);}
const ll N=5002,M=1e5+9;
ll n,m,tar[5];
ll tot;
struct edge
{ll to,next;
}e[M<<1];
ll idx,head[M];
void addedge(ll u,ll v)
{idx++;e[idx].to=v;e[idx].next=head[u];head[u]=idx;
}
bool vis[N];
ll col[N];
vector<ll>wht[N],blk[N];
bool f[N][N];
void dfs(ll u,ll c)
{vis[u]=1;col[u]=c;if(c==0)blk[tot].push_back(u);else wht[tot].push_back(u);for(int i=head[u];i;i=e[i].next){ll v=e[i].to;if(!vis[v])dfs(v,c^1);else if(col[u]==col[v])sayno}
}
ll ans[N];
int main()
{scanf("%lld%lld",&n,&m);for(int i=1;i<=3;i++)scanf("%lld",&tar[i]);for(int i=1;i<=m;i++){ll u,v;scanf("%lld%lld",&u,&v);addedge(u,v);addedge(v,u);}for(int i=1;i<=n;i++){if(!vis[i]){tot++;dfs(i,0);}}f[0][0]=1;for(int i=1;i<=tot;i++){for(int j=tar[2];j>=0;j--){if(j>=blk[i].size())f[i][j]|=f[i-1][j-blk[i].size()];if(j>=wht[i].size())f[i][j]|=f[i-1][j-wht[i].size()];}}if(!f[tot][tar[2]])saynofor(int i=1;i<=n;i++)ans[i]=1;for(int i=tot;i>=1;i--){if(tar[2]>=blk[i].size()&&f[i-1][tar[2]-blk[i].size()]){tar[2]-=blk[i].size();for(auto x:blk[i])ans[x]=2;}else if(tar[2]>=wht[i].size()&&f[i-1][tar[2]-wht[i].size()]){tar[2]-=wht[i].size();for(auto x:wht[i])ans[x]=2;}else sayno}if(tar[2]>0)saynoputs("YES");ll cnt=0;for(int i=1;i<=n;i++){if(ans[i]==2)printf("2");else {cnt++;if(cnt<=tar[1])printf("1");else printf("3");}}return 0;
}
7.AT_tenka1_2015_qualA_c 天下一美術館
题意
天下一美术馆的馆长 Seto 决定为新展区设计一个黑白马赛克艺术区的布局。
为了让展区更美观,他希望相邻的艺术品尽量相似。
为此,Seto 馆长定义了两幅艺术品之间的“相异度”。
马赛克艺术品由 M×NM \times NM×N 的格子组成,每个格子涂成黑色或白色。
我们考虑将一幅马赛克艺术品转换为另一幅的操作,所需的最小代价即为这两幅艺术品的相异度。
允许的操作有三种,每种操作的代价均为 111:
- 将黑格变为白格;
- 将白格变为黑格;
- 交换上下左右相邻的两个格子。
请为 Seto 馆长编写程序,计算给定的两幅马赛克艺术品之间的相异度。
思路
先计算原图和目标图的差异点有 tottottot 个。
我们发现交换相邻的,在这两个格子都和目标不相同的情况下,可以从 222 步节省到 111 步。
于是考虑有多少对相邻点可以快速操作。可以直接将相邻点连边。因为这个图是只有 0/10/10/1 的图,天然地成为二分图的左右两部。我们发现已经参与过相邻点交换的点,再进行一次相邻交换显然是不优的。
因此相邻交换次数就是这个二分图最大匹配数 cntcntcnt,在这个图上直接跑二分图匹配即可。在 tottottot 的基础上节省了 cntcntcnt 次,答案就是 tot−cnttot-cnttot−cnt。
代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=76,M=4905;
ll n,m;
ll a[N][N],b[N][N];
bool chg[N][N];
ll tot,id[N][N];
struct point
{ll x,y;
}p[M];
struct edge
{ll to,next;
}e[M<<1];
ll idx,head[M];
void addedge(ll u,ll v)
{idx++;e[idx].to=v;e[idx].next=head[u];head[u]=idx;
}
ll dx[5]={1,0,-1,0};
ll dy[5]={0,-1,0,1};
bool vis[M];
ll match[M];
bool dfs(ll u)
{for(int i=head[u];i;i=e[i].next){ll v=e[i].to;if(vis[v])continue;vis[v]=1;if(match[v]==0||dfs(match[v]))//当前无匹配或增广成功 {match[v]=u;return 1;}}return 0;
}
int main()
{scanf("%lld%lld",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%lld",&a[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%lld",&b[i][j]);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){if(a[i][j]!=b[i][j]){p[++tot]=(point){i,j};chg[i][j]=1;id[i][j]=tot;}}}for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){if(chg[i][j]&&a[i][j])//匹配相邻01,1左0右 {for(int k=0;k<4;k++){ll nx=i+dx[k],ny=j+dy[k];if(chg[nx][ny]&&a[i][j]!=a[nx][ny])addedge(id[i][j],id[nx][ny]),cout<<id[i][j]<<"->"<<id[nx][ny]<<endl;}}}}ll cnt=0;for(int i=1;i<=tot;i++){memset(vis,0,sizeof(vis));if(a[p[i].x][p[i].y]&&dfs(i))cnt++;//最大匹配数节省cnt次直接翻转 }printf("%lld\n",tot-cnt);return 0;
}