【题目描述】
在某城市里住着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. 题目背景
在并查集的标准应用中,我们通常处理的是"朋友的朋友是朋友"这种具有传递性的关系。但在本题中,引入了第二种关系------"敌人的敌人是朋友"。
题目核心规则:
-
朋友的朋友是朋友 ->标准并查集 U
nion操作。 -
敌人的敌人是朋友->这是解题的关键难点。
2. 解题思路
难点:如何处理"敌人"关系?
我们可以开一个并查集数组 fri[] 来维护朋友关系(即团伙)。但是,当输入告诉我们X和Y是敌人时,我们不能直接把他们合并,也不能简单地忽略。
根据规则:"敌人的敌人是朋友"。
这意味着:
-
如果X有一个敌人z,现在Y也是X的敌人,那么Y和z就应该属于同一个团伙(朋友)。
-
同理,如果Y有一个敌人z,现在X也是Y的敌人,那么X和z就应该属于同一个团伙。
算法策略
我们需要两个数组:
-
fri[i]:并查集数组,记录i所在团伙的老大。 -
ene[i]:记录i的第一个敌人是谁。
处理逻辑:
-
如果是朋友 (p=0) :直接
uni(x, y)。 -
如果是敌人 (p=1):
-
检查x是否已经有记录在案的敌人
ene[x]?-
如果没有,记录
ene[x] = y。 -
如果有,根据"敌人的敌人是朋友",将y和x的旧敌人
ene[x]合并:union(y, ene[x])。
-
-
同理检查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. 易错点
-
敌人的处理:
有些同学会疑惑,为什么只需要记录一个敌人?如果有多个敌人怎么办?
其实,根据并查集的逻辑,如果A的敌人是B,后来A的敌人又是C。代码中 uni(ene[A], C) 会把B和C合并。这意味着,A的所有敌人最终都会被合并到同一个"反派团伙"里。所以只需要记录一个ene[x]作为连接点即可。
-
最终统计:
我代码中使用sort统计的方法有有一个前提:必须先执行一遍 find(i)。
因为uni操作只修改了根节点的父级,子节点的fri值可能还没更新。如果不做find直接排序,统计结果会偏大。
另一种常用的统计方法是:(这样就不需要对所有数进行一次find操作)
cppint cnt = 0; for(int i=1; i<=n; i++) { if(fri[i]==i) cnt++; //只数"大哥"的数量 }
5. 总结
这道题是并查集思维拓展的经典案例。它教会了我们:并查集不仅可以维护"集合内的关系",还可以通过辅助数组(如本题的 ene[])来推导"集合间的关系"。掌握了这个思想,对于后续学习"带权并查集"或"种类并查集"(如食物链问题)非常有帮助。