专题五 并查集(kuangbin专题)

目录

1,无线网络

2,可疑人员

3,多少张桌子

4,多少个答案是错误的

5,食物链

6,真正的骗子

7,超市

 8,奇偶游戏

9,导航噩梦

 10,研究虫子

11,石头剪刀布

12,连接问题

13,小希的迷宫

14,这是一颗树吗


1,无线网络

题目链接:https://www.acwing.com/problem/content/description/4269/

这题很简单,就是一个朴素的并查集,每次进行打开电脑的操作时,我们遍历每一台已开机的电脑,如果有跟他距离小于给定条件的,就将他们加到一个集合中去 ,对于查询两个电脑是否能实现通信,就判断这两台电脑是否在一个集合中

代码如下:

#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N=1010;

int n,d;
int x[N],y[N],f[N],v[N];
char ch;
int p,q;

double get_dis(int a,int b)//求a和b的距离
{
    double dx=x[a]-x[b],dy=y[a]-y[b];
    return sqrt(dx*dx+dy*dy);
}
int find(int x)//并查集找到集合操作,并将路径上的点进行路径压缩,将集合中的点都指向根节点
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&d);
    for(int i=1;i<=n;i++)f[i]=i;//并查集初始化
    for(int i=1;i<=n;i++)
        scanf("%d%d",&x[i],&y[i]);
    while(cin>>ch)
    {
        if(ch=='O')
        {
            scanf("%d",&p);
            v[p]=1;//已经开机的电脑要标记一下
            for(int i=1;i<=n;i++)//遍历所有已经开机的电脑,如果有符合条件的就加入到一个集合中
                if(v[i]&&get_dis(p,i)<=d)
                    f[find(i)]=find(p);
        }
        else
        {
            scanf("%d%d",&p,&q);//查询两台电脑是否在一个集合中
            int fp=find(p),fq=find(q);
            if(fp==fq)puts("SUCCESS");
            else puts("FAIL");
        }
    }
    return 0;
}

2,可疑人员

题目链接:https://www.acwing.com/problem/content/4270/

对于每个社团,我们先找到第一个人 所在的集合,将社团中其他人所在的集合都与第一个人所在的集合合并,最终遍历所有人,就可以找到与0号学生在一个集合中的人

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=30010;

int n,m,k;
int f[N];

int find(int x)
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    while(scanf("%d%d",&n,&m),n||m)//多组测试数据
    {
        for(int i=0;i<=n;i++)f[i]=i;//并查集初始化
        
        while(m--)//m行
        {
            int x;
            scanf("%d%d",&k,&x);
            int fx=find(x);//先找到第一个点所在得集合
            k--;
            while(k--)
            {
                scanf("%d",&x);
                f[find(x)]=fx;//将一个社团中后面每个点都与第一个点合并到一个集合中
            }
        }
        int res=0;
        for(int i=0;i<n;i++)res+=(find(i)==find(0));//找到跟0号点在一个集合中得所有人
        printf("%d\n",res);
    }
    return 0;
}

3,多少张桌子

题目链接:https://www.acwing.com/problem/content/4288/

 一个并查集的模板题,只需要将给定的关系合并到一个集合中去,然后判断一下有多少个集合,就知道需要多少张桌子了

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1010;

int n,m;
int f[N];

int find(int x)//并查集找到集合操作,并将集合中的所有点进行路径压缩
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)f[i]=i;
        while(m--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            int fa=find(a),fb=find(b);//查询a和b所在的区间
            if(fa!=fb)//如果不在一个区间中就合并区间
                f[fa]=fb;
        }
        int ans=0;
        for(int i=1;i<=n;i++)
            if(f[i]==i)//如果集合的根节点是自己,说明要用一个新桌子
                ans++;
        printf("%d\n",ans);
    }
    return 0;
}

4,多少个答案是错误的

题目链接:https://www.acwing.com/problem/content/description/4289/

这题给出的条件是两个点之间的区间和,那么我们就知道了两个点的关系,给了一个区间和,我们可以看成是用前缀和求区间和的操作,例如给定l,r的区间和为s,等价于从1~r的区间和减去1~l-1的区间和的答案为s,因为我们可以用带权并查集维护每个点到根节点的距离,这里可以理解为到1号点的距离,如果两个点的关系已知,就可以加入到一个集合中,对于已经在一个集合中的点,我们就可以知道这两个点的关系,从而判断是否会与描述冲突

还有一个区间合并时如何维护权值,同样的,假设给定a,b这段区间的和为s,那么等于告诉了我们a-1和b这两个点的关系,合并是如果将a-1所在集合的根节点fa指向b所在集合的根节点,那么点fa到fb的距离可以通过a-1和b两个点的关系求得,即为d[a-1]+d[fa]+s=d[b],可以得到s=d[b]-d[a-1]-s.

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=2e5+10;

int n,m;
int f[N],d[N];//d[i]表示点i到其父节点的距离
int ans;

int find(int x)//找到x所在的集合,并更新该集合中所有点的权值
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x]+=d[f[x]];//更新集合中点的权值
        f[x]=root;//路径压缩,这一步一定要在上一步下面,否则修改权值会出错
    }
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)f[i]=i;
    while(m--)
    {
        int a,b,x;
        scanf("%d%d%d",&a,&b,&x);
        a--;//前缀和的思想,a~b的和为等于x,等价于sum(1~b)-sum(1~a-1)=x
        int fa=find(a),fb=find(b);
        if(fa!=fb)//如果不在一个集合中,就将两个点合并到一个集合
        {
            f[fa]=fb;//将a的根节点指向b的根节点
            d[fa]=d[b]-x-d[a];//更新a的根节点到b的根节点的权值
        }
        else//如果已经在一个集合中,就可以通过两个点的权值判断该描述是否会与前面的描述冲突
        {
            if(d[b]-d[a]!=x)//如果权值相减不等于x,说明有冲突
                ans++;
        }
    }
    printf("%d\n",ans);
    return 0;
}

5,食物链

题目链接:https://www.acwing.com/problem/content/242/

这题在算法提高课中解释的很清楚了,用带权并查集维护有三种集合具体可以查找 高级数据结构(算法提高课)

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=50010;

int n,m;
int f[N],d[N];
int ans;

int find(int x)//找到x所在的集合,并且更新x所在集合中所有点的边权
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x]+=d[f[x]];
        f[x]=root;
    }
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)f[i]=i;
    while(m--)
    {
        int op,x,y;
        scanf("%d%d%d",&op,&x,&y);
        if(x>n||y>n)ans++;
        else if(x==y&&op==2)ans++;
        else
        {
            int fx=find(x),fy=find(y);
            int t=0;//初始时默认x和y是同类,那么边权模3应该是相等的
            if(op==2)t=1;//如果是x吃y,那么x模3后的边权应该比y模3后的边权大1
            if(fx!=fy)//如果x和y不在一个集合中,就合并集合,并且更新边权
            {
                f[fx]=fy;//将x的根节点指向y的根节点
                d[fx]=d[y]-d[x]+t;//更新x的根节点到y的根节点的距离
            }
            else//如果x和y在一个集合中,就判断是否有矛盾
            {
                if(abs(d[x]-d[y]-t)%3)
                    ans++;
            }
        }
    }
    printf("%d",ans);
    return 0;
}

6,真正的骗子

题目链接:https://www.acwing.com/problem/content/261/

 这题有些复杂,需要用到带权并查集+01背包+DP回溯

同样的在算法进阶指南中给出了解释,高级数据结构(算法进阶指南)

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_map>
#include<vector>

using namespace std;

const int N=1010;

int n,p1,p2;//p1为天使数,p2为恶魔数
int a,b;
char str[5];
//f[i]表示点i所在的集合,d[i]表示点i的边权,dp[i][j]表示在前i个连通块中选择,能凑成j个天使的方案数
//num[i][0]表示第i个连通块中边权为0的数量,num[i][1]表示第i个连通块中边权为1的数量
int f[N],d[N],dp[N][N],num[N][2];

int find(int x)//找到x所在的集合,并且更新集合中所有点的边权且将集合中所有点路径压缩,指向根节点
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x]^=d[f[x]];
        f[x]=root;
    }
    return f[x];
}
int main()
{
    while(scanf("%d%d%d",&n,&p1,&p2),n||p1||p2)//多组测试数据
    {
        //初始化
        for(int i=1;i<=p1+p2;i++)f[i]=i,d[i]=0;
        memset(dp,0,sizeof dp);
        memset(num,0,sizeof num);
        
        //输入数据,合并集合,更新集合中点的边权
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d%s",&a,&b,str);
            int t=0;//初始时默认时yes,表示a和b的边权相同
            if(str[0]=='n')t=1;//如果是no,表示a和b的边权不同
            int fa=find(a),fb=find(b);//找到a的根节点和b的根节点
            f[fa]=fb;//合并集合
            d[fa]=d[a]^d[b]^t;//更新边权
        }
        
        int cnt=0;//记录连通块的数量
        unordered_map<int,int>mp;//用map给每个连通块编号
        for(int i=1;i<=p1+p2;i++)
        {
            int x=find(i);
            if(mp.count(x)==0)mp[x]=++cnt;//如果当前连通块之前没出现过,就编一个号
            num[mp[x]][d[i]]++;//记录当前连通块中不同边权的数量
        }
        
        //DP过程
        dp[0][0]=1;//边界初始化,从前0个连通块中选择,能凑出0个人的方案数为1
        for(int i=1;i<=cnt;i++)//遍历连通块的数量
        {
            for(int j=0;j<=p1;j++)//遍历天使的数量
            {
                if(j>=num[i][0])dp[i][j]+=dp[i-1][j-num[i][0]];//如果当前天使的数量能由第i个连通块中边权为0的人数凑成的话
                if(j>=num[i][1])dp[i][j]+=dp[i-1][j-num[i][1]];//如果当前天使的数量能由第i个连通块中边权为1的人数凑成的话
            }
        }

        //DP回溯,找到转移的过程
        //如果方案数不等于1,说明不满足题意,因为如果方案数大于1
        //那么至少会有一处不同,不能确定不同的那处的连通块中谁为天使,谁为恶魔
        if(dp[cnt][p1]!=1)puts("no");
        else
        {
            bool ans[N][2];//用一个ans记录答案
            memset(ans,false,sizeof ans);
            for(int i = cnt, j =p1; i >= 1; i --)//倒着查找转移的过程
            {
                if(dp[i - 1][j - num[i][0]] == 1)//说明由第i个连通块中边权为0的方案数转移过来的;
                {
                    ans[i][0] = true;
                    j -= num[i][0];
                }
                else if(dp[i - 1][j - num[i][1]] == 1)//说明由第i个连通块中边权为1的方案数转移过来的
                {
                    ans[i][1] = true;
                    j -= num[i][1];
                }
            }
            for(int i = 1; i <= p1+p2; i ++)//先找到第i个人所在的连通块和边权,看这个连通块的此边权是否时转移的点
                if(ans[mp[find(i)]][d[i]]) 
                    cout << i << endl;
            cout << "end" << endl;
        }
    }
    return 0;
}

7,超市

题目链接:https://www.acwing.com/problem/content/147/

其实这题用优先队列会更加好做,做法写在注释里了

代码如下:

#include<iostream>
#include<algorithm>
#include<queue>

using namespace std;

const int N=10010;

typedef pair<int,int>pii;

pii thing[N];

int main()
{
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=0;i<n;i++)
        {
            int v,s;
            scanf("%d%d",&v,&s);
            thing[i]={s,v};
        }
        sort(thing,thing+n);//先将商品按时间排好序
        priority_queue<int,vector<int>,greater<int>>heap;//堆里存储的是我们要卖出的商品的利润
        for(int i=0;i<n;i++)
        {
            int v=thing[i].second,s=thing[i].first;
            heap.push(v);
            if(s<heap.size())//如果这件商品的时间小于已经计划卖出的商品数,就删除堆顶的商品,也就是删除利润最低的商品
                heap.pop();
        }
        int res=0;
        while(heap.size())//计算堆里面所有的商品利润
        {
            res+=heap.top();
            heap.pop();
        }
        printf("%d\n",res);
    }

    return 0;
}

 8,奇偶游戏

题目链接:https://www.acwing.com/problem/content/241/

这题同样使用带权并查集维护两中集合的情况,可以用异或处理边权,在算法进阶指南写过了

 高级数据结构(算法进阶指南)

代码如下:

#include<iostream>
#include<algorithm>
#include<unordered_map>

using namespace std;

const int N=5010,M=N*2;

int n,m,cnt;//cnt用于给每个离散化后的点编号
int f[M],d[M];//d表示该节点与其父节点的奇偶性是否相同,0表示相同,1表示不同,f为并查集
unordered_map<int,int>mp;//哈希表用于离散化

int find(int x)//找到每个集合的根节点,并且将路径上的点都指向根节点,同时修改路径上的点的权值
{
    if(f[x]!=x)
    {
        int root=find(f[x]);
        d[x]^=d[f[x]];
        f[x]=root;
    }
    return f[x];
}
int get(int x)//离散化
{
    if(mp.count(x)==0)mp[x]=++cnt;//如果之前这个点没被离散化过,就开一个新的编号给它
    return mp[x];//返回这个点的编号
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=M;i++)f[i]=i;//并查集初始化
    int res=m;//初始时将答案设为m,这样如果全部都没错误的话m就是答案
    for(int i=1;i<=m;i++)
    {
        int a,b;
        char str[5];
        scanf("%d%d%s",&a,&b,str);
        a=get(a-1),b=get(b);//首先要离散化
        int fa=find(a),fb=find(b);//离散化后找到该点的根节点,也就是该点所在的集合
        int t=0;//t表示奇偶性,0表示偶数
        if(str[0]=='o')t=1;//1表示奇数
        if(fa==fb)//如果这两个点在一个集合中,就可以知道这两个点的关系
        {
            if((d[a]^d[b])!=t)//如果这两个点的奇偶性和给定的条件的奇偶性冲突了,就找到答案了
            {
                res=i-1;
                break;
            }
        }
        else//如果这两个点不在一个集合中,就将他们合并到一个集合中
        {
            f[fa]=fb;//将a的根节点指向b的根节点
            d[fa]=d[a]^d[b]^t;//根性a的根节点到b的根节点的距离
        }
    }
    printf("%d",res);//输出答案
    return 0;
}

9,导航噩梦

题目链接:https://www.acwing.com/problem/content/4290/

这题需要用带权并查集维护x轴和y轴方向上的边权,对于给定a,b,如果b在a的东边,我们就记录为x轴上的正边权,如果在西边,就记录x轴上的负边权,如果b在a的北边,就记录y轴上的正边权,如果b在a的南边,就记录y轴上的负边权。同时,我们将询问升序排序,这样就可以在合并集合时一起处理询问的问题,以及这个题可能会出现重复询问的情况,所以处理询问时要用一个while循环处理

代码如下:

#include<iostream>
#include<algorithm>
#include<cmath>

#define x first
#define y second

using namespace std;

const int N=40010;

typedef pair<int,int>pii;

int n,m,k;
int f[N],ans[N];//ans记录答案
pii d[N];//注意这题要维护两个边权,一个表示该节点到其父节点的x轴上的距离,另一个表示y轴上的距离

struct Node//存储给的农场的信息
{
    int a,b,len,op;//op为0说明是x轴,op为1说明是y轴
}node[N];
struct Query//存储给的询问的信息
{
    int a,b,num,id;//id记录是第几个询问,因为后面会排序
}query[N];

bool cmp(Query& x,Query& y)//按num来从小到大排序
{
    return x.num<y.num;
}
int find(int x)//找到x所在的集合,并且更新x所在集合中点的边权,并将其指向根节点
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x].x+=d[f[x]].x;//更新x到其父节点x轴上的距离
        d[x].y+=d[f[x]].y;//更新x到其父节点y轴上的距离
        f[x]=root;
    }
    return f[x];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)f[i]=i;

    for(int i=1;i<=m;i++)//出入农场的信息
    {
        int a,b,len,t;
        char op[2];
        scanf("%d%d%d%s",&a,&b,&len,op);

        //记录下来是x轴上的偏移量还是y轴上的偏移量,对应的距离取正还是取负
        if(op[0]=='E')node[i]={a,b,len,0};
        else if(op[0]=='W')node[i]={a,b,-len,0};
        else if(op[0]=='N')node[i]={a,b,len,1};
        else node[i]={a,b,-len,1};
    }
    scanf("%d",&k);
    for(int i=1;i<=k;i++)//输入询问,并且记录下来每个询问的编号
    {
        int a,b,num;
        scanf("%d%d%d",&a,&b,&num);
        query[i]={a,b,num,i};
    }
    sort(query+1,query+1+k,cmp);//按num来从小到大排序

    int idx=1;//用来遍历询问
    for(int i=1;i<=m;i++)
    {
        int a=node[i].a,b=node[i].b,len=node[i].len,t=node[i].op;
        int fa=find(a),fb=find(b);
        if(fa!=fb)//不在一个集合首先将其合并到一个集合中
        {
            f[fa]=fb;//将a的根节点指向b的根节点
            if(t==0)//说明a和b在x轴上,更新x轴上的距离与更新y轴上的距离的方式不同
            {
                d[fa].x=d[b].x-d[a].x+len;
                d[fa].y=d[b].y-d[a].y;
            }
            else//说明a和b在y轴上,同样的,要注意更新x轴上的距离与更新y轴上的方式不同
            {
                d[fa].x=d[b].x-d[a].x;
                d[fa].y=d[b].y-d[a].y+len;
            }
        }
        while(query[idx].num==i)因为可能有多个询问的num是i,所以这里要用一个while循环
        {
            a=query[idx].a,b=query[idx].b;
            int id=query[idx].id;
            fa=find(a),fb=find(b);
            if(fa!=fb)ans[id]=-1;//如果a和b不在一个集合中,就说明不能找到答案
            else ans[id]=abs(d[a].x-d[b].x)+abs(d[a].y-d[b].y);//如果在一个集合中,就可以得到答案
            idx++;
        }
    } 
    for(int i=1;i<=k;i++)
            printf("%d\n",ans[i]);
    return 0;
}

 10,研究虫子

题目链接:https://www.acwing.com/problem/content/4291/

这题用带权并查集维护两中集合,假设一种集合的边权到根节点的边权为1,另一种为0,因此只能是不同性交配,所以对于给定的信息,如果不知道这两个虫子的关系的话,我们就将其加入到一个集合中,但是边权要不同,如果两个虫子已经在一个集合中了,我们就可以知道这两个虫子的关系,如果这两个虫子的边权都为0或都为1,说明这两个虫子是同性

代码如下:

#include<iostream>
#include<algorithm>

using namespace std;

const int N=2010;

int n,m;
int f[N],d[N];//d[i]表示点i到其父节点的边权

int find(int x)//找到x所在的集合,并更新x所在集合中的点的边权
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x]^=d[f[x]];
        f[x]=root;
    }
    return f[x];
}
int main()
{
    int t;
    scanf("%d",&t);
    for(int i=1;i<=t;i++)
    {
        printf("Scenario #%d:\n",i);
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)f[i]=i,d[i]=0;//初始化
        
        bool flag=true;//如果为false说明发生了同性交配
        while(m--)
        {
            int a,b;
            scanf("%d%d",&a,&b);
            int fa=find(a),fb=find(b);
            if(fa!=fb)//不在一个集合就合并成一个集合
            {
                f[fa]=fb;//将a的根节点指向b的根节点
                d[fa]=d[a]^d[b]^1;//更新a的根节点到b的根节点的距离
            }
            else//在一个集合就判断是否冲突
            {
                if(d[a]^d[b]==0)
                    flag=false;
            }
        }
        if(!flag)
            printf("Suspicious bugs found!\n");
        else
            printf("No suspicious bugs found!\n");
        printf("\n");
    }
    return 0;
}

11,石头剪刀布

题目链接:https://www.acwing.com/problem/content/description/260/

这题算法进阶指南里讲了高级数据结构(算法进阶指南) ,维护点的边权与食物链相似

代码如下:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>

using namespace std;

const int N=510,M=2010;

int n,m,num;
int f[N],d[N],t[N];
int a[M],c[M];
char b[M];

int find(int x)//找到x所在集合的根节点,并更新集合中其他点的边权和其父节点,将其父节点指向根节点,即路径压缩
{
    if(x!=f[x])
    {
        int root=find(f[x]);
        d[x]+=d[f[x]];
        f[x]=root;//!!!!!注意,这里一定要写在最后,否则上一步的边权修改会出错
    }
    return f[x];
}
bool check(int j)//判断第j轮对局是否满足条件
{
    int x=a[j],y=c[j];
    int fx=find(x),fy=find(y);//分别找到x所在的集合和y所在的集合
    int op=0;//初始假定它们相等
    if(b[j]=='>')op=1;//如果x大于y,op为1
    else if(b[j]=='<')op=-1;//如果x小于y,op为-1
    
    //如果x和y之前已经在一个集合中,在集合中的关系与给定的关系出现了矛盾,就说明第j轮对局出现了矛盾
    //注意这里的判断条件,一定要这么写,可能会出现负数,但是不用取绝对值,因为我们只需要知道%3后是否等于0
    if(fx==fy&&(d[x]-d[y]-op)%3)return false;
    else
    {
        f[fx]=fy;//将x的根节点指向y的根节点
        d[fx]=d[y]-d[x]+op;//更新x的根节点到y的根节点的距离
    }
    return true;//说明第j轮符合条件
}
int main()
{
    while(cin>>n>>m)
    {
        for(int i=1;i<=m;i++)
            cin>>a[i]>>b[i]>>c[i];
            
        memset(t,0,sizeof t);//多组测试数据,每组要初始化
        int cnt=0;//用于记录可能是裁判的人数
        for(int i=0;i<n;i++)//枚举每个人作为裁判时
        {
            for(int i=0;i<=n;i++)f[i]=i,d[i]=0;//枚举每个人都要初始化
            bool is_umpire=true;//首先假定第i个人可以作为裁判
            for(int j=1;j<=m;j++)//枚举每一轮的对局
            {
                if(a[j]!=i&&c[j]!=i&&!check(j))//判断不包含裁判的对局是否会出现冲突,如果出现冲突说明第i个人不为裁判
                {
                    is_umpire=false;
                    t[i]=j;
                    break;
                }
            }
            if(is_umpire)
            {
                cnt++;
                num=i;
            }
        }
        if(!cnt)puts("Impossible");
        else if(cnt>1)puts("Can not determine");
        else
        {
            int res=0;
            for(int i=0;i<n;i++)
                if(i!=num)
                    res=max(res,t[i]);
            printf("Player %d can be determined to be the judge after %d lines\n",num,res);
        }
    }
    return 0;
}

12,连接问题

题目链接:https://www.acwing.com/problem/content/4292/

 这题我们只用一个朴素并查集就可以维护,维护时,每次合并区间我们都选择权值较大的点作为根节点,如果权值相等,就选择将编号较小的首领作为根节点,然后我们用一个pair记录下来所有的操作,如果时查询操作,pair的第二关键字就为-1,同时我们记录下来会摧毁的边,然后对于不会摧毁的边先合并到一个集合中,逆序处理存储的所有操作,如果是查询操作就查询目前的集合,目前的集合等价于正序枚举时前面操作摧毁给定边后的集合,用一个数组存下答案,最终再逆序输出答案,就得到的是正向遍历的答案

代码如下:

#include<iostream>
#include<algorithm>
#include<set>

using namespace std;

const int N = 10010, M = (N << 1);

typedef pair<int,int>pii;

int n, m, q, val[N], fa[N];
pii e[M],query[50010];
int stk[50010], top;  //栈

int find(int x){
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void merge(int x, int y){
    int fx = find(x), fy = find(y);
    if(fx != fy){
        if(val[fx] < val[fy]) swap(fx, fy); //将权值较大的作为首领
        else if(val[fx] == val[fy]) {
            if(fx > fy) swap(fx, fy);       //若权值相等,编号小的作为首领
        }
        fa[fy] = fx;
    }
}

int main(){

    bool flag = true;
    while(cin >> n){
        if(flag) flag = false;
        else puts("");     //输出格式

        set<pii> st;//存储会被摧毁的边
        top = 0;
        for(int i = 0; i < n; i ++ ) fa[i] = i, cin >> val[i];

        //存储所有的边
        cin >> m;
        for(int i = 1; i <= m; i ++ ){
            int x, y; cin >> x >> y;
            if(x > y) swap(x, y);
            e[i] = {x, y};
        }

        //存储询问
        cin >> q;
        for(int i = 1; i <= q; i ++ ){
            string op; cin >> op;
            int x, y;
            if(op == "query") cin >> x, query[i] = {x, -1};
            else {
                cin >> x >> y;
                if(x > y) swap(x, y);
                query[i] = {x, y}, st.insert({x, y});
            } 
        }

        //先建立所有不会被摧毁的边
        for(int i = 1; i <= m; i ++ )
            if(!st.count(e[i]))
                merge(e[i].first, e[i].second);

        //逆序处理询问
        for(int i = q; i; i -- )
            if(query[i].second == -1){
                int res = find(query[i].first);
                if(val[res] <= val[query[i].first]) stk[ ++ top] = -1;//不存在比当前大的
                else stk[ ++ top] = res;
            }else merge(query[i].first, query[i].second);  //合并

        //逆序输出答案
        while(top) printf("%d\n", stk[top -- ]);  
    }

    return 0;
}

13,小希的迷宫

题目链接:https://www.acwing.com/problem/content/description/4293/

这题只需要一个朴素并查集就可以做,但是有很多细节问题,首先要判断一个图是否是数,我们只需要看是否是n个点n-1条边,且只有一个根节点,同时没有自环,然后有一个特判,0个点0条边也是树

对于给定的两个点,存在一条边,我们就将其加入到一个集合中,如果对于给定的两个点已经在一个集合,说明有环,那么就不是一个树,最后要判断一下是否只存在一个根节点

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

const int N=1e5+10;

int f[N];

int find(int x)
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    int a,b;
    while(scanf("%d%d",&a,&b)!=EOF)
    {
        if(a==-1&&b==-1)
            break;
        if(a==0&&b==0)//这也是一个样例
            puts("Yes");
        else
        {
            vector<int>v;//用一个vector记录下来出现过的编号,后面用于求该树是否只有一个根节点
            for(int i=0;i<=N;i++)f[i]=i;//并查集初始化
            bool flag=true;//判断是否是树
            int fa=find(a),fb=find(b);
            f[fa]=fb;//先将连了边的两个点合并到一个集合中
            v.push_back(a),v.push_back(b);//加入到vector中
            while(scanf("%d%d",&a,&b),a||b)
            {
                if(!flag)continue;//这里要注意!!!!!这题卡常数严重,要加一个优化才能过,否则会MLE
                                //也就是说只要判断出来了该图不是树,那么久不用将后续的点加入到vecotr中了
                v.push_back(a),v.push_back(b);
                fa=find(a),fb=find(b);
                if(fa==fb)
                    flag=false;
                else
                    f[fa]=fb;
            }

            if(flag)//判断是否只有一个根节点
            {
                for(int i=0;i<v.size();i++)
                    if(find(v[i])!=find(v[0]))
                        flag=false;
            }
                    
            if(flag)
                puts("Yes");
            else
                puts("No");
        }
        
    }
    return 0;
}

14,这是一颗树吗

题目链接:https://www.acwing.com/problem/content/3412/

 这题跟上一题差不多,代码也基本一样

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>

using namespace std;

const int N=1e4+10;

int f[N],c[N];

int find(int x)
{
    if(x!=f[x])f[x]=find(f[x]);
    return f[x];
}
int main()
{
    int a,b;
    int t=0;
    while(scanf("%d%d",&a,&b)!=EOF)
    {
        memset(c,0,sizeof c);
        t++;
        if(a==-1&&b==-1)
            break;
        if(a==0&&b==0)//这也是一个样例,表示这课树只有一个根节点
            printf("Case %d is a tree.\n",t);
        else
        {
            vector<int>v;//用一个vector记录下来出现过的编号,后面用于求该树是否只有一个根节点
            for(int i=0;i<=N;i++)f[i]=i;//初始化
            
            bool flag=true;//判断是否是树
            int fa=find(a),fb=find(b);
            f[fa]=fb;//先将连了边的两个点合并到一个集合中
            v.push_back(a),v.push_back(b);//加入到vector中
            c[b]++;
            
            while(scanf("%d%d",&a,&b),a||b)
            {
                if(!flag)continue;//这里要注意!!!!!这题卡常数严重,要加一个优化才能过,否则会MLE
                                //也就是说只要判断出来了该图不是树,那么久不用将后续的点加入到vecotr中了
                c[b]++;
                if(c[b]>1)
                {
                    flag=false;
                    continue;
                }
                v.push_back(a),v.push_back(b);
                fa=find(a),fb=find(b);
                if(fa==fb)//如果a和b点已经在一个集合中,还要连边的话,说明有环那么就不是一颗树了
                    flag=false;
                else//否则合并集合
                    f[fa]=fb;
            }

            if(flag)//如果前面判断出来无环,我们还要再判断这棵树是否只有一个根
            {
                for(int i=0;i<v.size();i++)//只需要判断是否存在不一样的根节点即可
                    if(find(v[i])!=find(v[0]))
                        flag=false;
            }
                    
            if(flag)
                printf("Case %d is a tree.\n",t);
            else
                printf("Case %d is not a tree.\n",t);
        }
        
    }
    return 0;
}

并查集是一种常用的数据结构,用于管理一个不相交集合的数据。在并查集中,每个元素都有一个父节点指向它所属的集合的代表元素。通过查找和合并操作,可以判断两个元素是否属于同一个集合,并将它们合并到同一个集合中。 在解决某些问题时,可以使用并查集进行优化。例如,在区间查询中,可以通过优化并查集的查询过程,快速找到第一个符合条件的点。 对于拆边(destroy)操作,一般的并查集无法直接实现这个功能。但是可以通过一个巧妙的方法来解决。首先,记录下所有不会被拆除的边,然后按照逆序处理这些指令。遇到拆边操作时,将该边重新加入并查集中即可。 在实现并查集时,虽然它是一种树形结构,但只需要使用数组就可以实现。可以通过将每个元素的父节点记录在数组中,来表示元素之间的关系。通过路径压缩和按秩合并等优化策略,可以提高并查集的效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [「kuangbin带你飞」专题并查集专题题解](https://blog.youkuaiyun.com/weixin_51216553/article/details/121643742)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [并查集(详细解释+完整C语言代码)](https://blog.youkuaiyun.com/weixin_54186646/article/details/124477838)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值