二分图
定义
可以将图分为两部分,相同部分的点之间没有连边的图叫做二分图。通常将两部分的点称为“左部点”和“右部点”。
判定
发现对于每一条边来说,两个端点的部分一定不相同,所以考虑将点进行染色,时间复杂度 \(O(n+m)\)。
性质 \(1\)
由上述操作可以得到二分图不存在奇环。
最大匹配
定义二分图的一个匹配为,由若干条边构成的任意两条边没有公共顶点的集合叫做二分图的匹配。
二分图的最大匹配为所有匹配中集合大小最大的匹配。
对于一个匹配来说,边在集合中的称为“匹配边”,不在的称为“非匹配边”,匹配边的两个端点称为“匹配点”,否则为“非匹配点”。
定义一个匹配的增广路为,一条起点和终点均为非匹配点的路径,使得其中非匹配边和匹配边交替出现。
性质 \(2\)
由定义可知,增广路一定为奇数条边,且非匹配边个数为匹配边个数加 \(1\)。
由上述性质知,二分图的最大匹配不存在增广路,否则将增广路上的边选择状态取反,边的集合大小增加。
因此可以构建一个求二分图最大匹配的基本步骤:
-
初始匹配为空集,即初始匹配中没有任何边。
-
对于该匹配,不断寻找增广路来扩张匹配。
-
寻找不到增广路时,说明该匹配为最大匹配。
问题来到如何寻找匹配增广路。
枚举左部点,考虑依次将左部点所连的边加入匹配中。若该边所连右部点还未匹配,则寻找到增广路;否则考虑能否将该右部点原来匹配的左部点进行更换匹配,从而将当前左部点与右部点匹配,该情况也会寻找到增广路。因此构建递归代码,如下所示:
bool find_g(int u){
vis[u]=true;
for(int i:g[u]){
if(vis[match[i]]) continue;//避免重复走到一个点
if(match[i]==0||find_g(match[i])){
match[i]=u;
return true;
}
}
return false;
}
枚举每个左部点,若 \(find_g(x)\) 为 true,则寻找到增广路,匹配个数加 \(1\)。
下见完整代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INT_MAX (int)(1e18)
int n,m,e,ans;
inline int read(){
int t=0,f=1;
register char c=getchar();
while(c<'0'||c>'9') f=(c=='-')?(-1):(f),c=getchar();
while(c>='0'&&c<='9') t=(t<<3)+(t<<1)+(c^48),c=getchar();
return t*f;
}
const int N=510;
int match[N];
vector<int> g[N];
bool vis[N];
bool find_g(int u){
vis[u]=true;
for(int i:g[u]){
if(vis[match[i]]) continue;
if(match[i]==0||find_g(match[i])){
match[i]=u;
return true;
}
}
return false;
}
signed main(){
n=read(),m=read(),e=read();
for(int i=1;i<=e;i++){
int u=read(),v=read();
g[u].push_back(v);
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) vis[j]=false;
if(find_g(i)) ans++;
}
cout<<ans<<"\n";
return 0;
}
最小边染色
将二分图的每条边进行染色,使得没有相邻(有公共端点)的两条边为相同颜色。
设所有点的最大入度为 \(k\),则有 \(ans \ge k\),不妨大胆假设 \(ans=k\),构造可以如下:
依次考虑每条边,设 \(G(x)\) 为以 \(x\) 为端点的边颜色集合, \(\operatorname{mex}(S)\) 代表 \(S\) 集合中最小的未出现的正整数。
-
若 \(\operatorname{mex}(G(u))=\operatorname{mex}(G(v))\),则可以直接将该边染为 \(\operatorname{mex}(G(u))\)。
-
若 \(\operatorname{mex}(G(u)) \ne \operatorname{mex}(G(v))\),则考虑强行将 \(\left\{ u,v \right\}\) 这条边强行染为 \(\operatorname{mex}(G(u))\)。此时可能有 \(\left\{v,w\right\}\) 与其冲突,即均为 \(\operatorname{mex}(G(u))\)。则考虑将 \(\left\{v,w\right\}\) 染为 \(\operatorname{mex}(G(v))\),两者冲突解决。但可能再次引起与 \(\left\{w,p\right\}\) 与 \(\left\{v,w\right\}\) 的冲突,则将 \(\left\{w,p\right\}\) 染为 \(\operatorname{mex}(G(u))\),然后一直循环下去,用 \(\operatorname{mex}(G(u))\) 与 \(\operatorname{mex}(G(v))\) 交替染色。
接下来证明该染色合法,不妨设 \(u\) 为左部点,\(v\) 为右部点。则依据上方染色来看,所有从左部点出发连向右部点的边颜色为 \(\operatorname{mex}(G(u))\),从右部点出发连向左部点的边颜色为 \(\operatorname{mex}(G(v))\)。
故若连了一圈后转回 \(u\) 点,则原本连向 \(u\) 点的边颜色一定被改变为了 \(\operatorname{mex}(G(v))\),即原本颜色为 \(\operatorname{mex}(G(u))\),与假设不符。若转回 \(v\) 点,与上述一致,依然与假设不符。又由于在更新边颜色的过程中不会重复回到某个点(证明考虑每个点每种颜色只能连一条边,若回到某个点,说明该点一定有一个颜色染了两条边,与原题意不符;由于初始未染色图一定符合题意,归纳证明每一步之后也均符合题意)。所以更新边颜色过程一定为一条链,最后一定会遇到一个点两个颜色有一个未连边,可以合法终止过程。
由于 \(\operatorname{mex}(G(u))\) 始终小于等于 \(k\),所以 \(ans=k\)。
更新边颜色过程考虑使用异或进行交替染色。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INT_MAX (int)(1e18)
const int N=1e3+10;
const int M=1e5+10;
int a,b,m,ans;
int u[M],v[M];
int ru[N<<1],match[N<<1][N],Ans[N<<1][N<<1];
inline int read(){
int t=0,f=1;
register char c=getchar();
while(c<'0'||c>'9') f=(c=='-')?(-1):(f),c=getchar();
while(c>='0'&&c<='9') t=(t<<3)+(t<<1)+(c^48),c=getchar();
return t*f;
}
signed main(){
a=read(),b=read(),m=read();
for(int i=1;i<=m;i++){
u[i]=read(),v[i]=read(),v[i]+=a;
ru[u[i]]++,ru[v[i]]++;
}
for(int i=1;i<=a+b;i++) ans=max(ans,ru[i]);
for(int i=1;i<=m;i++){
int c1=1,c2=1;
while(match[u[i]][c1]) c1++;
while(match[v[i]][c2]) c2++;
match[u[i]][c1]=v[i];
match[v[i]][c2]=u[i];
if(c1==c2) continue;
for(int p=v[i],tmp=c2;p;p=match[p][tmp],tmp^=c1^c2)
swap(match[p][c1],match[p][c2]);
}
for(int i=1;i<=a+b;i++)
for(int j=1;j<=ans;j++)
Ans[i][match[i][j]]=j;
cout<<ans<<"\n";
for(int i=1;i<=m;i++)
cout<<Ans[u[i]][v[i]]<<" ";
cout<<"\n";
return 0;
}
拉丁方
先考虑 \(R=n\) 的情况,此时每一行可以选择的数已经确定,将每一行当作左部点,每种数当作右部点,每个能选择的关系当作连边,可以连出每个点度数均为 \(n-C\) 的二分图,对该二分图跑边染色,最终每条边的颜色就对应该行该数所在列数。
考虑 \(R \ne n\) 的情况,可以考虑先补全前 \(C\) 列,然后问题就转化为 \(R=n\) 的情况。依然考虑将列当为左部点,每种数当作右部点,每个能选择的关系当作连边,此时希望能跑出 \(n-R\) 边染色,这样就可以确定每种数在每列所在行数。
由于左部点的度数均为 \(n-R\),只需考虑右部点即可,若右部点度数大于 \(n-R\),说明其在多列未出现过,但是 \(n-R\) 行至多选择 \(n-R\) 个同一种数,所以此时一定不合法。因此可以跑出 \(n-R\) 边染色。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INT_MAX (int)(1e18)
int n;
inline int read(){
int t=0,f=1;
register char c=getchar();
while(c<'0'||c>'9') f=(c=='-')?(-1):(f),c=getchar();
while(c>='0'&&c<='9') t=(t<<3)+(t<<1)+(c^48),c=getchar();
return t*f;
}
const int N=1e3+10;
const int M=3e5+10;
int R,C,idx;
int a[N][N],ru[N],u[M],v[M];
int match[N][N];
bool mk[N];
void add(int u1,int v1){
u[++idx]=u1,v[idx]=v1;
}
void Solve(int L,int R,int k){
for(int i=1;i<=L+R;i++)
for(int j=1;j<=k;j++)
match[i][j]=0;
for(int i=1;i<=idx;i++){
int c1=1,c2=1;
while(match[u[i]][c1]) c1++;
while(match[v[i]][c2]) c2++;
match[u[i]][c1]=v[i],
match[v[i]][c2]=u[i];
if(c1==c2) continue;
for(int p=v[i],tmp=c2;p;p=match[p][tmp],tmp^=c1^c2)
swap(match[p][c1],match[p][c2]);
}
}
void init(){
idx=0;
for(int i=1;i<=n;i++) ru[i]=0;
}
void solve(){
n=read(),R=read(),C=read();
for(int i=1;i<=R;i++)
for(int j=1;j<=C;j++)
a[i][j]=read();
for(int i=1;i<=C;i++){
for(int j=1;j<=n;j++) mk[j]=false;
for(int j=1;j<=R;j++) mk[a[j][i]]=true;
for(int j=1;j<=n;j++)
if(!mk[j]) add(i,j+C),ru[j]++;
}
for(int i=1;i<=n;i++)
if(ru[i]>n-R){cout<<"No\n";init();return;}
Solve(C,n,n-R);init();
for(int i=1;i<=C;i++)
for(int j=1;j<=n-R;j++)
a[j+R][i]=match[i][j]-C;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) mk[j]=false;
for(int j=1;j<=C;j++) mk[a[i][j]]=true;
for(int j=1;j<=n;j++)
if(!mk[j]) add(i,j+n);
}
Solve(n,n,n-C);init();
for(int i=1;i<=n;i++)
for(int j=1;j<=n-C;j++)
a[i][j+C]=match[i][j]-n;
cout<<"Yes\n";
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
cout<<a[i][j]<<" ";
cout<<"\n";
}
}
signed main(){
int T=read();
while(T--) solve();
return 0;
}

浙公网安备 33010602011771号