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 2A 2 3,我们就能推断出:1和2电荷相同,2和3电荷不同,所以1和3的电荷也不同。

这种“朋友的朋友是朋友”、“敌人的朋友是敌人”的关系传递,非常适合用并查集来维护。并查集最擅长的就是处理“是否在同一个集合”的问题。

2. 核心思路:从“敌人”到“我敌人的朋友”

标准的并查集只能处理“朋友”关系(即在同一个集合)。如果 R a b,我们可以简单地把 ab 合并(union(a, b)),表示它们是“同类”。

但问题来了,A a b(吸引)代表的是“不同类”的关系,这怎么表示呢?我们不能说“a 不在 b 的集合里”,因为这提供不了任何信息。

这里就是本题最精妙的地方:扩展域思想,我们可以把它通俗地理解为“给每个粒子创建一个反物质伙伴”

  1. 建立“反粒子”:对于每个粒子 i(编号从 1 到 N),我们都想象存在一个它的“反粒子”,记作 i'。这个 i' 代表和 i 电荷完全相反的粒子。
  2. 开辟新空间:在程序实现中,我们可以用编号 1N 代表原始粒子,用编号 N+12N 来代表它们的“反粒子”。也就是说,粒子 i 的“反粒子” i' 就用 i+N 来表示。
  3. 重新定义关系:现在我们有了两倍的元素,所有的关系都可以转化为“同类”关系:
    • 排斥 R a b:意味着 ab 是同类。同时,它们的“反粒子” a'b' 也是同类。

      • 操作:合并 ab,同时合并 a'b'
      • 代码实现union(a, b) 并且 union(a+N, b+N)
    • 吸引 A a b:意味着 ab 是异类。这等价于说,ab 的“反粒子” b' 是同类。同理,ba 的“反粒子” a' 也是同类。

      • 操作:合并 ab',同时合并 ba'
      • 代码实现union(a, b+N) 并且 union(b, a+N)

通过这种方式,我们巧妙地将“吸引”(不同类)的关系,转化为了在更大范围内的“排斥”(同类)关系。

3. 查询操作的判断

当我们收到一个查询 Q a b 时,如何判断它们的关系呢?

  1. 判断是否排斥 (Repel):如果根据已知信息,能确定 ab 是同类,那么它们就相互排斥。

    • 条件ab 在同一个集合里。
    • 代码检查find(a) == find(b)
    • (注:由于我们的合并操作是成对的,如果 find(a) == find(b) 成立,那么 find(a+N) == find(b+N) 也必然成立,所以检查一个即可。)
  2. 判断是否吸引 (Attract):如果根据已知信息,能确定 ab 是异类,那么它们就相互吸引。

    • 条件ab 的“反粒子” b' 在同一个集合里。
    • 代码检查find(a) == find(b+N)
    • (同样,如果这个条件成立,find(b) == find(a+N) 也必然成立。)
  3. 判断是否不确定 (?):如果上述两种情况都不满足,说明在当前信息下,我们无法推断出 ab 的确切关系。

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),用于合并 xy 所在的集合。
  • 粒子 i 的“反粒子”在代码中用 i+n 表示。
  • 整个逻辑与我们前面分析的思路完全一致。

5. 总结

本题是一道典型的“扩展域并查集”入门题。它的核心思想在于:当我们需要处理多种(通常是对立的)关系时,可以通过创建“虚拟节点”(如本题的“反粒子”)来扩展并查集的功能,将所有关系都统一为“同类”关系进行处理。

这个技巧非常强大,还能解决类似“A和B是敌人”、“C和D是朋友”这类更复杂的关系推理问题。希望这份报告能帮助你理解并掌握这个技巧!

posted @ 2025-07-16 10:48  surprise_ying  阅读(11)  评论(0)    收藏  举报