前言:由于 NOI
大纲中对二分图的要求仅停留在判定,所以本文主要讲解二分图染色。
二分图指:一张图可以分成两个集合,使得两个集合内部没有边相连,边在两个集合之间。
判定二分图的充要条件是:不存在奇环。那么我们可以对于整张图交替染色,如果发现矛盾,存在奇环;否则说明不存在奇环。
其实奇环可以用并查集的扩展域去写,但是二分图染色的题目一般要求求出具体方案,这个用并查集就很难实现(路径压缩后,原来的结构改变)。
根据二分图的概念,集合内没有边相连,所以一般一条边相连的两点,这两点的信息是不同的;如果还要求两点信息相同,那么连一条边权为 \(0\) 的边即可。
下面看几道例题体会二分图染色建模的应用:
[POI2005] DWU-Double-row
题意简述:\(2n\) 个数站成两排(每个数在 \(2n\) 个数中最多出现两遍),一次操作可以交换任意一列中两个数,求使每行数不重复的最少操作数。
分析本题的限制条件:
- 每行不能出现重复的数字;
- 一次操作只能交换一列中的数字。
由于是 \(2\) 排,又有一些限制条件,所以想到二分图。
既然每行不能出现重复数字,那么显然要在相同的数字之间连边,这样在染色后,相同数字不可能在一个集合。
并且在相同列的数字之间连边,这样也能保证不会有非法的交换出现。比如下图(红色圈圈起来的是同一排,线连起来的是同一列):
不可能出现非法交换:将 \(b,c\) 交换。
但是有可能出现非法交换:\(d,b\) 交换,\(a,c\) 交换,但是由于题目要求每行不出现重复数字,所以这种叫喊是没有意义的,自然不会算进答案里面。
统计答案,一个环连接的一堆点,可能会出现第一个放左,放右答案不同的情况,这时取最小值即可,因为一个环不会对后面的计算造成影响。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,sum1,sum2;
int a[N],f[N],h[N],col[N];
vector<int> g[N];
void add(int x,int y) {
g[x].push_back(y);
}
void dfs(int nd,int c) {
col[nd]=c;
if(col[nd]!=f[nd]) sum1++;
else sum2++;
for(auto x:g[nd]) {
if(col[x]==-1) {
dfs(x,c^1);
}
}
}
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
if(h[a[i]]) {
add(h[a[i]],i);
add(i,h[a[i]]);
}
h[a[i]]=i;
f[i]=1;
}
for(int i=n+1;i<=2*n;i++) {
cin>>a[i];
if(h[a[i]]) {
add(h[a[i]],i);
add(i,h[a[i]]);
}
h[a[i]]=i;
f[i]=0;
}
//左1右0
for(int i=1;i<=n;i++) {
add(i,i+n);
add(i+n,i);
}
memset(col,-1,sizeof(col));
int ans=0;
for(int i=1;i<=2*n;i++) {
sum1=0; sum2=0;
if(col[i]==-1) {
dfs(i,0);
sum1/=2;
sum2/=2;
ans+=min(sum1,sum2);
}
}
cout<<ans;
return 0;
}
Arpa’s overnight party and Mehrdad’s silent entering
吐槽:题目又长又臭。。。没想出来。。。
题意简述:\(n\) 对情侣围一圈坐下,有两种食物,情侣不能吃同一种,连续三个人不能吃一种,问一种可行方案。
情侣不能吃同一种是很好处理的,直接在情侣之间连边即可,但是连续三人不能吃同一种。我们考虑如何拆成两人。
如果规定 \(1,2\) 不能相同,\(3,4\) 不能相同,...,\(2i,2i+1\) 不能相同,能否表示这个限制呢?
显然,满足这个条件,必然可以满足连续三人不能吃同一种,但是,连续三人不同却不一定满足这个条件,比如:
1 1 2 2 1 1 2 2
这样显然是不符合相邻的不同的,但是的确符合连续三个不同。
让我们来考虑,按照上述染色能否构造出答案:
首先判断有没有可能存在奇环,一个人一共两条边,情侣边和组边,如果构成奇环,先考虑 \(3\) 人的情况,那么必然会出现矛盾,所以这种分组方案一定可以整出一种合法方案。
至于上面提到的与连续三个条件不符,也不用考虑了。(反正可以算出答案)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n;
int a[N],b[N];
vector<int> g[N];
void add(int x,int y) {
g[x].push_back(y);
}
int col[N];
void dfs(int x,int c) {
col[x]=c;
for(int y:g[x]) {
if(!col[y]) {
dfs(y,3-c);
}
}
}
int main() {
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i]>>b[i];
add(a[i],b[i]);
add(b[i],a[i]);
}
for(int i=1;i<=n;i++) {
add(2*i,2*i-1);
add(2*i-1,2*i);
}
for(int i=1;i<=2*n;i++) {
if(!col[i]) dfs(i,1);
}
for(int i=1;i<=n;i++) {
cout<<col[a[i]]<<' '<<col[b[i]]<<endl;
}
return 0;
}
[NOIP2008 提高组] 双栈排序
个人感觉难度最高的一道题目。
题目简述:给定两个栈,四种操作:进栈 \(1\),出栈 \(1\) 并输出到序列,进栈 \(2\),出栈 \(2\) 并输出到序列。
先来思考一个问题:如果只有一个栈,那么什么情况下 \(i,j\) 是不能在一个栈内的。
结论:若 \(i<j<k\),且 \(a_k<a_i<a_j\),这时候就不行。
因为 \(a_k\) 要在 \(a_i,a_j\) 之前出栈,且 \(a_i\) 要在 \(a_j\) 之前出栈,但是由于 \(i<j\),所以 \(j\) 不出栈,\(i\) 就不能出栈,这时产生矛盾。
回到本题,既然题目给了两个栈,那么很直观的想法就是将不能在同一个栈内的 \(i,j\) 分配到不同的两个栈内。这时我们可以对不可以的数对进行连边,然后二分图染色,注意尽量使得前面的染 \(1\),后面的染 \(2\)。
在输出的时候,需要贪心一下,也很有讲究:
- 进栈 \(1\)
- 如果进不去,先尝试将栈 \(1\) 弹出;
- 如果栈 \(1\) 弹不出,先尝试将栈 \(2\) 弹出;
- 最后将数字压进栈。
- 进栈 \(2\)
- 不管进不进得去,先尝试将栈 \(1\) 弹出;
- 如果进不去,先将栈 \(1\) 弹出,再将栈 \(2\) 弹出;
- 最后塞进去。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1100;
int n;
int a[N],f[N],col[N];
int g[N][N];
int dfs(int nd,int c) {
col[nd]=c;
for(int i=1;i<=n;i++) {
if(g[nd][i]) {
if(!col[i]) dfs(i,3-c);
if(col[i]!=3-c) return 0;
}
}
return 1;
}
int num;
vector<char> ans;
stack<int> s[3];
void Push(int x,int opt) {
s[opt].push(x);
if(opt==1) ans.push_back('a');
else ans.push_back('c');
}
void Pop(int opt) {
s[opt].pop();
if(opt==1) ans.push_back('b');
else ans.push_back('d');
num++;
}
int main() {
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
f[n+1]=n+1;
for(int i=n;i>=1;i--) {
f[i]=min(f[i+1],a[i]);
}
for(int i=1;i<=n;i++) {
for(int j=i+1;j<n;j++) {
if(a[i]<a[j]&&f[j+1]<a[i]) {
g[i][j]=g[j][i]=1;
}
}
}
for(int i=1;i<=n;i++) {
if(!col[i]&&!dfs(i,1)) {
cout<<0;
return 0;
}
}
num=1;
for(int i=1;i<=n;i++) {
int id=col[i];
if(id==2) {
while(s[1].size()&&s[1].top()==num) Pop(1);
}
if(!s[id].size()) Push(a[i],id);
else if(s[id].top()>a[i]) Push(a[i],id);
else {
while(s[id].size()&&s[id].top()==num) Pop(id);
if(id==2) {
while(s[1].size()&&s[1].top()==num) Pop(1);
}
if(!s[id].size()) Push(a[i],id);
else if(s[id].top()>a[i]) Push(a[i],id);
else {
while(s[3-id].size()&&s[3-id].top()==num) Pop(3-id);
while(s[id].size()&&s[id].top()==num) Pop(id);
if(!s[id].size()) Push(a[i],id);
else if(s[id].top()>a[i]) Push(a[i],id);
}
}
}
while(num<=n) {
while(s[1].size()&&s[1].top()==num) Pop(1);
while(s[2].size()&&s[2].top()==num) Pop(2);
}
for(auto x:ans) cout<<x<<' ';
return 0;
}
Graph Coloring
吐槽:紫题,但是完全没有难度。
首先一个边是连接在两个点之间的,如果这条边原来是蓝色的,现在要将其变成红色,那么两个点显然只有一个需要变,也就是两个点的状态不同,连一条边即可;同理,若原来是蓝色,变完之后还是蓝色,两点状态相同,不用连边。
最后跑一遍二分图染色计算贡献即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m;
int cnt0,cnt1;
int w[N],v[N],c[N];
int col[N];
struct node {
int to,w;
};
vector<node> g[N];
vector<int> ans1,ans2,cod;
void add(int x,int y,int z) {
g[x].push_back({y,z});
}
int dfs(int nd,int c) {
col[nd]=c;
cod.push_back(nd);
if(c==0) cnt0++;
else cnt1++;
for(auto t:g[nd]) {
int y=t.to,w=t.w;
if(w==0) {
if(col[y]==-1&&!dfs(y,c)) return 0;
if(col[y]!=c) return 0;
}
else {
if(col[y]==-1&&!dfs(y,!c)) return 0;
if(col[y]!=1-c) return 0;
}
}
return 1;
}
void Print(vector<int> a) {
cout<<a.size()<<endl;
for(int x:a) cout<<x<<' ';
}
int main() {
cin>>n>>m;
for(int i=1;i<=m;i++) {
string s; cin>>w[i]>>v[i]>>s;
if(s=="B") c[i]=0;
else c[i]=1;
}
//color=B
for(int i=1;i<=m;i++) {
if(c[i]==0) {
add(w[i],v[i],0);
add(v[i],w[i],0);
}
else {
add(w[i],v[i],1);
add(v[i],w[i],1);
}
}
memset(col,-1,sizeof(col));
int f1=1;
for(int i=1;i<=n;i++) {
if(col[i]==-1) {
cnt0=0; cnt1=0;
cod.clear();
if(!dfs(i,0)) {
f1=0;
break;
}
int p=-1;
if(cnt0<cnt1) p=0;
else p=1;
for(int x:cod) {
if(col[x]==p) ans1.push_back(x);
}
}
}
//color R
for(int i=1;i<=n;i++) g[i].clear();
for(int i=1;i<=m;i++) {
if(c[i]==1) {
add(w[i],v[i],0);
add(v[i],w[i],0);
}
else {
add(w[i],v[i],1);
add(v[i],w[i],1);
}
}
memset(col,-1,sizeof(col));
int f2=1;
for(int i=1;i<=n;i++) {
if(col[i]!=-1) continue;
cnt0=cnt1=0;
cod.clear();
if(!dfs(i,0)) {
f2=0;
break;
}
int p=-1;
if(cnt0<cnt1) p=0;
else p=1;
for(int x:cod) {
if(col[x]==p) ans2.push_back(x);
}
}
if(!f1&&!f2) cout<<-1;
else if(f1&&!f2) {
Print(ans1);
}
else if(!f1&&f2) {
Print(ans2);
}
else {
if(ans1.size()<ans2.size()) Print(ans1);
else Print(ans2);
}
return 0;
}