团伙(group)(信息学奥赛一本通- P1385)

【题目描述】

在某城市里住着n个人,任何两个认识的人不是朋友就是敌人,而且满足:

1、我朋友的朋友是我的朋友;

2、我敌人的敌人是我的朋友;

所有是朋友的人组成一个团伙。告诉你关于这n个人的m条信息,即某两个人是朋友,或者某两个人是敌人,请你编写一个程序,计算出这个城市最多可能有多少个团伙?

【输入】

第1行为n和m,1<n<1000,1<=m<=100 000;

以下m行,每行为p x y,p的值为0或1,p为0时,表示x和y是朋友,p为1时,表示x和y是敌人。

【输出】

一个整数,表示这n个人最多可能有几个团伙。

【输入样例】

复制代码
6 4
1 1 4
0 3 5
0 4 6
1 1 2

【输出样例】

复制代码
3

1. 题目背景

在并查集的标准应用中,我们通常处理的是"朋友的朋友是朋友"这种具有传递性的关系。但在本题中,引入了第二种关系------"敌人的敌人是朋友"。

题目核心规则

  1. 朋友的朋友是朋友 ->标准并查集 Union 操作。

  2. 敌人的敌人是朋友->这是解题的关键难点。

2. 解题思路

难点:如何处理"敌人"关系?

我们可以开一个并查集数组 fri[] 来维护朋友关系(即团伙)。但是,当输入告诉我们X和Y是敌人时,我们不能直接把他们合并,也不能简单地忽略。

根据规则:"敌人的敌人是朋友"。

这意味着:

  • 如果X有一个敌人z,现在Y也是X的敌人,那么Y和z就应该属于同一个团伙(朋友)。

  • 同理,如果Y有一个敌人z,现在X也是Y的敌人,那么X和z就应该属于同一个团伙。

算法策略

我们需要两个数组:

  1. fri[i]:并查集数组,记录i所在团伙的老大。

  2. ene[i]:记录i的第一个敌人是谁。

处理逻辑

  • 如果是朋友 (p=0) :直接 uni(x, y)

  • 如果是敌人 (p=1)

    1. 检查x是否已经有记录在案的敌人ene[x]

      • 如果没有,记录 ene[x] = y

      • 如果有,根据"敌人的敌人是朋友",将y和x的旧敌人ene[x] 合并:union(y, ene[x])

    2. 同理检查y,如果y有旧敌人ene[y],则将x和ene[y] 合并。

3. 完整代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int n,m;
int fri[1010];//记录每个人朋友关系中的老大
//记录每个人的敌人编号,只需记录一个即可,其他敌人都会成为第一个敌人的朋友
int ene[1010];

//查询朋友关系中的老大,并进行路径压缩
int find(int x){
    //如果已经是朋友关系中的老大(根结点)就返回自己
    if(fri[x]==x) return x;
    //否则就递归找到朋友祖先,并把朋友祖先赋值给沿途所有节点
    return fri[x]=find(fri[x]);
}

void uni(int x,int y){
    //如果x和y朋友祖先相同,即已经是朋友,就不需要操作
    int frx=find(x);//x的朋友祖先
    int fry=find(y);//y的朋友祖先
    //否则就让x的朋友中的老大变成y朋友中老大的老大
    if(frx!=fry){
        fri[fry]=frx;
    }
}

int main(){
    cin>>n>>m;
    //初始化每个人的朋友老大是自己
    for(int i=1;i<=n;i++) fri[i]=i;
    for(int i=1;i<=m;i++){
        int p,x,y;
        cin>>p>>x>>y;
        //如果是朋友,先判断他两是否已经是朋友(有同一个朋友老大)
        //是就不需要操作
        //不是就合并到一个集合
        if(p==0){
            uni(x,y);
        } 
        //如果是敌人
        else{
            if(ene[x]==0){//如果x还没有敌人 就记录
                ene[x]=y;
            }
            else{//x已经有敌人了
                //敌人的敌人是朋友 所以y和x已经记录的敌人就是朋友
                uni(ene[x],y);
            }
            //关系是双向的
            if(ene[y]==0){//如果y还没有敌人 就记录
                ene[y]=x;
            }
            else{
                //敌人的敌人是朋友 所以x和y已经记录的敌人就是朋友
                uni(ene[y],x);
            }
        }
    }
    //因为最后一次操作可能是uni操作,所以fri数组中存的可能不是最终祖先
    //因为合并时只改了根节点 所以统计前必须对所有人做一次find,
    //(或者在统计时判断 fri[i]==i就不需要对所有数进行一次find操作)
    //即遍历一次所有人进行find操作,这样才能确保每个人的朋友
    //关系中的老大都是祖先老大(对所有人都进行了一次路径压缩)
    for(int i=1;i<=n;i++){
        find(i);
    }
    sort(fri+1,fri+n+1);//对fri数组从小到大排序 让相同朋友老大聚集在一起
    int cnt=1;//至少有一个团伙(即一个朋友祖先)
    //找出所有不同祖先
    for(int i=1;i<n;i++){
        if(fri[i]!=fri[i+1])
            cnt++;
    }
    cout<<cnt;
    return 0;
}

4. 易错点

  1. 敌人的处理:

    有些同学会疑惑,为什么只需要记录一个敌人?如果有多个敌人怎么办?

    其实,根据并查集的逻辑,如果A的敌人是B,后来A的敌人又是C。代码中 uni(ene[A], C) 会把B和C合并。这意味着,A的所有敌人最终都会被合并到同一个"反派团伙"里。所以只需要记录一个ene[x]作为连接点即可。

  2. 最终统计:

    我代码中使用sort统计的方法有有一个前提:必须先执行一遍 find(i)。

    因为uni操作只修改了根节点的父级,子节点的fri值可能还没更新。如果不做find直接排序,统计结果会偏大。

    另一种常用的统计方法是:(这样就不需要对所有数进行一次find操作)

    cpp 复制代码
    int cnt = 0;
    for(int i=1; i<=n; i++) {
        if(fri[i]==i) cnt++; //只数"大哥"的数量
    }

5. 总结

这道题是并查集思维拓展的经典案例。它教会了我们:并查集不仅可以维护"集合内的关系",还可以通过辅助数组(如本题的 ene[])来推导"集合间的关系"。掌握了这个思想,对于后续学习"带权并查集"或"种类并查集"(如食物链问题)非常有帮助。

相关推荐
Ka1Yan2 小时前
[链表] - 代码随想录 160. 相交链表
算法·leetcode·链表
学嵌入式的小杨同学2 小时前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
数据结构·c++·算法·unity·游戏引擎·代理模式
rgeshfgreh2 小时前
顺序表实战:构建到销毁全解析
算法
十八岁讨厌编程2 小时前
【算法训练营Day32】图论专题
算法·深度优先·图论
小欣加油3 小时前
leetcode 174 地下城游戏
c++·算法·leetcode·职场和发展·动态规划
sali-tec3 小时前
C# 基于OpenCv的视觉工作流-章11-高斯滤波
图像处理·人工智能·opencv·算法·计算机视觉
不知名XL3 小时前
day23 贪心算法 part01
算法·贪心算法
wuqingshun3141594 小时前
蓝桥杯 缺页异常2【算法赛】
算法·职场和发展·蓝桥杯
Mh_ithrha4 小时前
题目:小鱼比可爱(java)
java·开发语言·算法