P10734 [NOISG 2019 Prelim] Experimental Charges 解题报告
P10734 [NOISG 2019 Prelim] Experimental Charges 解题报告
大家好!今天我们来分析一道非常巧妙的题目:P10734 - Experimental Charges。这道题的核心是处理粒子间的关系,看起来有点复杂,但只要掌握一个关键技巧,就能迎刃而解。这个技巧就是带扩展域的并查集,也常被称为“种类并查集”。
1. 题目解读与分析
首先,我们来弄清楚题目到底在说什么。
- 背景:有 \(N\) 个粒子,每个粒子要么带正电,要么带负电。
- 规则:同种电荷相互排斥(Repel),不同电荷相互吸引(Attract)。
- 操作:
R a b
:告诉你粒子 \(a\) 和 \(b\) 相互排斥。这意味着它俩带同种电荷。A a b
:告诉你粒子 \(a\) 和 \(b\) 相互吸引。这意味着它俩带不同种电荷。Q a b
:问你根据目前已知的信息,粒子 \(a\) 和 \(b\) 是吸引还是排斥,或者无法确定。
关键点:我们只关心粒子间的相对关系,而不需要知道它们具体带的是什么电荷。例如,如果知道 R 1 2
和 A 2 3
,我们就能推断出:1和2电荷相同,2和3电荷不同,所以1和3的电荷也不同。
这种“朋友的朋友是朋友”、“敌人的朋友是敌人”的关系传递,非常适合用并查集来维护。并查集最擅长的就是处理“是否在同一个集合”的问题。
2. 核心思路:从“敌人”到“我敌人的朋友”
标准的并查集只能处理“朋友”关系(即在同一个集合)。如果 R a b
,我们可以简单地把 a
和 b
合并(union(a, b)
),表示它们是“同类”。
但问题来了,A a b
(吸引)代表的是“不同类”的关系,这怎么表示呢?我们不能说“a
不在 b
的集合里”,因为这提供不了任何信息。
这里就是本题最精妙的地方:扩展域思想,我们可以把它通俗地理解为“给每个粒子创建一个反物质伙伴”。
- 建立“反粒子”:对于每个粒子
i
(编号从 1 到 N),我们都想象存在一个它的“反粒子”,记作i'
。这个i'
代表和i
电荷完全相反的粒子。 - 开辟新空间:在程序实现中,我们可以用编号
1
到N
代表原始粒子,用编号N+1
到2N
来代表它们的“反粒子”。也就是说,粒子i
的“反粒子”i'
就用i+N
来表示。 - 重新定义关系:现在我们有了两倍的元素,所有的关系都可以转化为“同类”关系:
-
排斥
R a b
:意味着a
和b
是同类。同时,它们的“反粒子”a'
和b'
也是同类。- 操作:合并
a
和b
,同时合并a'
和b'
。 - 代码实现:
union(a, b)
并且union(a+N, b+N)
。
- 操作:合并
-
吸引
A a b
:意味着a
和b
是异类。这等价于说,a
和b
的“反粒子”b'
是同类。同理,b
和a
的“反粒子”a'
也是同类。- 操作:合并
a
和b'
,同时合并b
和a'
。 - 代码实现:
union(a, b+N)
并且union(b, a+N)
。
- 操作:合并
-
通过这种方式,我们巧妙地将“吸引”(不同类)的关系,转化为了在更大范围内的“排斥”(同类)关系。
3. 查询操作的判断
当我们收到一个查询 Q a b
时,如何判断它们的关系呢?
-
判断是否排斥 (Repel):如果根据已知信息,能确定
a
和b
是同类,那么它们就相互排斥。- 条件:
a
和b
在同一个集合里。 - 代码检查:
find(a) == find(b)
。 - (注:由于我们的合并操作是成对的,如果
find(a) == find(b)
成立,那么find(a+N) == find(b+N)
也必然成立,所以检查一个即可。)
- 条件:
-
判断是否吸引 (Attract):如果根据已知信息,能确定
a
和b
是异类,那么它们就相互吸引。- 条件:
a
和b
的“反粒子”b'
在同一个集合里。 - 代码检查:
find(a) == find(b+N)
。 - (同样,如果这个条件成立,
find(b) == find(a+N)
也必然成立。)
- 条件:
-
判断是否不确定 (?):如果上述两种情况都不满足,说明在当前信息下,我们无法推断出
a
和b
的确切关系。
4. 代码解析
现在我们来看题解提供的C++代码,它完美地实现了上述思路。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+10; // 数组大小要开到 2N,这里直接开了 2*10^5+10,足够了
int n,m,f[N]; // f[] 是并查集的父节点数组
// 并查集的 find 函数,带路径压缩优化
int gf(int x){return x==f[x]?f[x]:f[x]=gf(f[x]);}
// 并查集的 union 函数
void link(int x,int y){
int u=gf(x),v=gf(y); // 找到 x 和 y 的根节点
if (u != v) f[u]=v; // 如果不在一个集合,就合并
}
int main(){
// 初始化并查集,每个元素的父亲都是自己
for(int i=1;i<N;++i)f[i]=i;
cin>>n>>m; // 读入粒子数 N 和操作数 Q
while(m--){
char op;int a,b;
cin>>op>>a>>b;
if(op=='A'){ // 吸引操作
// a 和 b 的反粒子 b+n 是同类
link(a,b+n);
// a 的反粒子 a+n 和 b 是同类
link(a+n,b);
}
else if(op=='R'){ // 排斥操作
// a 和 b 是同类
link(a,b);
// a 和 b 的反粒子也是同类
link(a+n,b+n);
}
else{ // 查询操作
// 检查 a 和 b 是否是同类
if(gf(a)==gf(b)||gf(a+n)==gf(b+n)) cout<<'R';
// 检查 a 和 b 是否是异类 (a 和 b+n 是同类)
else if(gf(a+n)==gf(b)||gf(a)==gf(b+n)) cout<<'A';
// 否则,关系不确定
else cout<<'?';
cout<<"\n";
}
}
return 0;
}
代码要点:
gf(x)
就是find(x)
,用于查找元素x
所在集合的根。link(x,y)
就是union(x,y)
,用于合并x
和y
所在的集合。- 粒子
i
的“反粒子”在代码中用i+n
表示。 - 整个逻辑与我们前面分析的思路完全一致。
5. 总结
本题是一道典型的“扩展域并查集”入门题。它的核心思想在于:当我们需要处理多种(通常是对立的)关系时,可以通过创建“虚拟节点”(如本题的“反粒子”)来扩展并查集的功能,将所有关系都统一为“同类”关系进行处理。
这个技巧非常强大,还能解决类似“A和B是敌人”、“C和D是朋友”这类更复杂的关系推理问题。希望这份报告能帮助你理解并掌握这个技巧!