二分图浅学
前言:由于 NOI
大纲中对二分图的要求仅停留在判定,所以本文主要讲解二分图染色。
二分图指:一张图可以分成两个集合,使得两个集合内部没有边相连,边在两个集合之间。
判定二分图的充要条件是:不存在奇环。那么我们可以对于整张图交替染色,如果发现矛盾,存在奇环;否则说明不存在奇环。
其实奇环可以用并查集的扩展域去写,但是二分图染色的题目一般要求求出具体方案,这个用并查集就很难实现(路径压缩后,原来的结构改变)。
根据二分图的概念,集合内没有边相连,所以一般一条边相连的两点,这两点的信息是不同的;如果还要求两点信息相同,那么连一条边权为 的边即可。
下面看几道例题体会二分图染色建模的应用:
[POI2005] DWU-Double-row
题意简述: 个数站成两排(每个数在 个数中最多出现两遍),一次操作可以交换任意一列中两个数,求使每行数不重复的最少操作数。
分析本题的限制条件:
- 每行不能出现重复的数字;
- 一次操作只能交换一列中的数字。
由于是 排,又有一些限制条件,所以想到二分图。
既然每行不能出现重复数字,那么显然要在相同的数字之间连边,这样在染色后,相同数字不可能在一个集合。
并且在相同列的数字之间连边,这样也能保证不会有非法的交换出现。比如下图(红色圈圈起来的是同一排,线连起来的是同一列):
不可能出现非法交换:将 交换。
但是有可能出现非法交换: 交换, 交换,但是由于题目要求每行不出现重复数字,所以这种叫喊是没有意义的,自然不会算进答案里面。
统计答案,一个环连接的一堆点,可能会出现第一个放左,放右答案不同的情况,这时取最小值即可,因为一个环不会对后面的计算造成影响。
点击查看代码
#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
吐槽:题目又长又臭。。。没想出来。。。
题意简述: 对情侣围一圈坐下,有两种食物,情侣不能吃同一种,连续三个人不能吃一种,问一种可行方案。
情侣不能吃同一种是很好处理的,直接在情侣之间连边即可,但是连续三人不能吃同一种。我们考虑如何拆成两人。
如果规定 不能相同, 不能相同,..., 不能相同,能否表示这个限制呢?
显然,满足这个条件,必然可以满足连续三人不能吃同一种,但是,连续三人不同却不一定满足这个条件,比如:
1 1 2 2 1 1 2 2
这样显然是不符合相邻的不同的,但是的确符合连续三个不同。
让我们来考虑,按照上述染色能否构造出答案:
首先判断有没有可能存在奇环,一个人一共两条边,情侣边和组边,如果构成奇环,先考虑 人的情况,那么必然会出现矛盾,所以这种分组方案一定可以整出一种合法方案。
至于上面提到的与连续三个条件不符,也不用考虑了。(反正可以算出答案)
点击查看代码
#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 提高组] 双栈排序
个人感觉难度最高的一道题目。
题目简述:给定两个栈,四种操作:进栈 ,出栈 并输出到序列,进栈 ,出栈 并输出到序列。
先来思考一个问题:如果只有一个栈,那么什么情况下 是不能在一个栈内的。
结论:若 ,且 ,这时候就不行。
因为 要在 之前出栈,且 要在 之前出栈,但是由于 ,所以 不出栈, 就不能出栈,这时产生矛盾。
回到本题,既然题目给了两个栈,那么很直观的想法就是将不能在同一个栈内的 分配到不同的两个栈内。这时我们可以对不可以的数对进行连边,然后二分图染色,注意尽量使得前面的染 ,后面的染 。
在输出的时候,需要贪心一下,也很有讲究:
- 进栈
- 如果进不去,先尝试将栈 弹出;
- 如果栈 弹不出,先尝试将栈 弹出;
- 最后将数字压进栈。
- 进栈
- 不管进不进得去,先尝试将栈 弹出;
- 如果进不去,先将栈 弹出,再将栈 弹出;
- 最后塞进去。
点击查看代码
#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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效