前言:本章继续分享一些并查集的题目,重要的构建思维
题目:红色警报 L2-013 红色警报 - 团体程序设计天梯赛-练习集
思路:这道题的难点在于如何判断被攻占的城市“会改变整个国家的连通性”,我们可以知道并查集的个数==国家的连通分支数,那么我们则可以通过攻占城市前后的并查集数目判断城市是否会改变国家的连通性(注意我的代码条件设置)。用一个二维数组存储城市通路,定义一个一维数组lost记录城市攻占情况,每次城市被攻占都要重新计算并查集数目(除去攻占城市后的)。
代码:
#include <iostream>
#include<vector>
using namespace std;
int n;
int f[500],lost[500];
vector<pair<int,int> >v;
int find(int x){
if(f[x]!=x){
f[x]=find(f[x]);
}
return f[x];
}
void merge(int x,int y){
x=find(x),y=find(y);
if(x!=y){
f[x]=y;
}
}
void init(){ //初始化父节点
for(int i=0;i<n;i++){
f[i]=i;
}
}
int count(){ //计算并查集个数
int c=0;
for(int i=0;i<n;i++){
if(f[i]==i){
c++;
}
}
return c;
}
int main()
{
int m,k,x,y;
cin>>n>>m;
init(); //初始化父节点
while(m--){
cin>>x>>y;
v.push_back({x,y}); //记录连通道路
merge(x,y);
}
int last=count(); //记录初始并查集个数
cin>>k;
while(k--){
cin>>x;
if(k==0){ //最后一个城市
cout<<"Game Over";
return 0; //结束程序
}
lost[x]=1; //标记城市被攻占
init(); //重新计算并查集数目
for(int i=0;i<v.size();i++){
if(lost[v[i].first]||lost[v[i].second]){ //如果道路有一端被攻占,不连通本次
continue;
}
merge(v[i].first,v[i].second);
}
int now=count(); //记录并查集数目
if(last==now||last+1==now){ //被攻占的城市本就是孤立城市||被攻占的城市形成一个新的集合
printf("City %d is lost.\n",x);
}else{
printf("Red Alert: City %d is lost!\n",x);
}
last=now; //更新上一次连通数目
}
return 0;
}
题目:拆路 NC235745
思路: 不知道大家有没有想用第一题的套路呢?存储每条路,每次删除操作去掉路,再重新建立并查集,可是这里有Q次操作(10^5),每次都重新操作肯定TLE。再升级一下想法,存储每次操作,并进行,剩下的肯定就是一定存在的路,建立并查集,再从后往前遍历操作,如果是Q则输出,否则添加新路(此操作以前存在的路),那么便不用重复操作了。(set容器大家也可以去学习下了)
代码:
#include<iostream>
#include<set> //set基于红黑树,查找比vector快
#include<vector>
using namespace std;
const int N=1e5+5;
int val[N],f[N]; //分别存储城镇的繁荣度和父节点
set<pair<int,int> >v; //存储存在道路
vector<int>result; //逆序存储查询结果
struct adjust{
char op;
int a,b;
}cz[N]; //存储操作
int find(int x){
if(f[x]!=x){
f[x]=find(f[x]);
}
return f[x];
}
void merge(int x,int y){
x=find(x),y=find(y);
if(val[x]>val[y]){ //以繁荣度大的城镇为父节点
f[y]=x;
}else{
f[x]=y;
}
}
int main()
{
int n,m,i,x,y,q;
cin>>n>>m;
for(i=1;i<=n;i++){
cin>>val[i];
f[i]=i; //初始化父节点
}
for(i=0;i<m;i++){
cin>>x>>y;
v.insert({x,y});
}
cin>>q;
for(i=0;i<q;i++){
cin>>cz[i].op;
if(cz[i].op=='Q'){
cin>>cz[i].a;
}else{
cin>>cz[i].a>>cz[i].b;
v.erase({cz[i].a,cz[i].b}); //边可能以x y或y x给出
v.erase({cz[i].b,cz[i].a});
}
}
for(auto i:v){ //基于范围的for循环,i遍历v中的每个元素,auto自动推导i的类型
x=i.first,y=i.second; //大家不要用dev-c++哦,那个IDE(集成开发环境)版本太低,会报错,主播什么时候有空教大家使用下别的编译器
merge(x,y); //存在的边建立并查集
}
for(i=q-1;i>=0;i--){
if(cz[i].op=='Q'){
result.push_back(val[find(cz[i].a)]); //集合父节点的繁荣度(用find别忘了)
}else{
x=cz[i].a,y=cz[i].b; //添加边
merge(x,y);
}
}
for(i=result.size()-1;i>=0;i--){ //逆序输出
cout<<result[i]<<endl;
}
return 0;
}
题目:叠积木 NC235622
思路: f[i]数组记录i的孩子节点(往下),cnt[i]记录以i为底的列有多少积木,dep[i]记录i的下方有几块积木。merge时维护附加信息,难点在于理解find函数。我画了个图示(身边暂时没有正常纸张),大家粗略看一下叭。find函数的第三个语句用于处理merge时x没有改变的上方节点,而如果已经指向底部孩子节点则不用改变(加的是0)
代码:
#include <iostream>
using namespace std;
int f[30010],cnt[30010],dep[30010];
int find(int x){ //在路径压缩时更新路径节点的深度
if(f[x]==x) return x;
int t=find(f[x]); //底部节点
dep[x]+=dep[f[x]]; //更新x的深度+=孩子深度
f[x]=t; //路径压缩x指向底部
return t; //返回底部
}
void merge(int x,int y){
x=find(x),y=find(y); //x列底部节点,y列底部节点
if(x!=y){
f[x]=y; //x的孩子节点指向y
dep[x]=cnt[y]; //x的下方有cnt[y]块积木
cnt[y]+=cnt[x]; //更新y列积木总数
}
}
int main()
{
int q,x,y;
char opt;
for(int i=1;i<=30000;i++){
f[i]=i; //初始化自己为孩子节点
cnt[i]=1; //初始化每列1个木块
}
cin>>q;
while(q--){
cin>>opt;
if(opt=='M'){
cin>>x>>y;
merge(x,y); //把x列放到y列上方
}else{
cin>>x;
find(x); //调用find确保更新
cout<<dep[x]<<endl;
}
}
return 0;
}
呜呜呜,都好难,动脑神功,想想想想想……
题目:食物链 NC16884
法1:带权并查集
思路:x和y之间存在3种可能的关系,用权重0,1,2分别代表关系:是同类,x吃y,x被y吃。而这三类动物还构成环形的食物链,我们可以用路径权值和%3代表节点与节点的关系。例如x-0-y-1-z-2-w,w为根节点,从节点x找到根节点w的过程中,x到z=(0+1)%3=1,x吃z,继续,x到z到w=(1+2)%3=0,x与w是同类,再查找的过程中进行路径压缩(大家可以自己再试些例子)。而将2个并查集进行合并时,两个根节点的关系怎么推呢?(又画了个图示,大家简单看看吧) 若x-y的关系为t,xa+ab+by=t(注意,是带方向的,by=-yb,我们存储的是子节点到根节点的关系,而负数取模可能会得到错误关系,所以我们用(3-yb)代表by,最后%3),那么ab=(t+yb-xa+3)%3(+3保证正数)
代码:
#include <iostream>
using namespace std;
const int N=5e4+5;
int f[N],d[N]; //f[]存储父节点,d[]存储与父节点的关系
int find(int x){
if(f[x]==x) return x;
int root=find(f[x]);
d[x]=(d[x]+d[f[x]])%3;
f[x]=root;
return f[x];
}
void merge(int x,int y,int c){
int fx=find(x),fy=find(y);
d[fx]=(c+d[y]-d[x]+3)%3;
f[fx]=fy;
}
int main()
{
int n,k,c,x,y,cnt=0,i;
cin>>n>>k;
for(i=1;i<=n;i++) f[i]=i; //初始化
while(k--){
cin>>c>>x>>y;
if(x>n||y>n||c==2&&x==y){
cnt++;
continue;
}
if(c==1){ //是同类,若在不同集合,则只需合并,若在同一集合,判断是否是真话
int fx=find(x),fy=find(y);
if(fx==fy){ //在同一集合
if(d[x]!=d[y]){
cnt++;
}
}else{
merge(x,y,0);
}
}else{ //x吃y
if(find(x)==find(y)){ //在同一集合,判断
if((d[x]-d[y]+3)%3!=1){ //不是吃的关系
cnt++;
}
}else{
merge(x,y,1);
}
}
}
cout<<cnt;
return 0;
}
总结:对每一关系设置权重,n个关系就%n代表关系,find时路径压缩,演示例子得到关系式。
法2:扩展域并查集
思路:开一个大小为3*n的数组f,a表示自身一类,a+n表示被a吃的一类(捕食),a+2n表示吃a的一类(天敌)。判断是否是真话,如果A,B是一类,则A,B+n还有A,B+2n不是一类
#include <iostream>
using namespace std;
const int N=5e4+5;
int f[N*3];
int find(int x){
if(f[x]!=x){
f[x]=find(f[x]);
}
return f[x];
}
void merge(int x,int y){
x=find(x),y=find(y);
f[x]=y;
}
int main()
{
int n,k,c,x,y,cnt=0;
cin>>n>>k;
for(int i=1;i<=3*n;i++) f[i]=i; //初始化
while(k--){
cin>>c>>x>>y;
if(x>n||y>n||c==2&&x==y){
cnt++;
continue;
}
if(c==1){ //是同类
if(find(x)==find(y+n)||find(x)==find(y+2*n)){ //是假话
cnt++;
continue;
}
merge(x,y); //真话,合并双方每一类
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}else{ //x吃y
if(find(x)==find(y)||find(x)==find(y+n)){ //x,y是同类,或者x与y的捕食类同类
cnt++;
continue;
}
merge(x,y+2*n);
merge(x+n,y);
merge(x+2*n,y+n);
}
}
cout<<cnt;
return 0;
}
感觉是扩展域并查集容易理解,但用扩展域并查集容易导致内存超限,还是推荐用带权并查集,好好总结,好好吸收!
最后,并查集还适用于解决最小生成树问题,推荐大家看这篇博客kruskal算法透彻理解(含并查集及最小生成树的解释)-优快云博客,讲得很细致,我就不赘述了。