#include<iostream>
#include<map>
#include <unordered_map>
using namespace std;
using ll = long long;
/*
大数据量场景(如 n=1e5、1e6,节点数极多):
unordered_map的平均 O (1) 操作效率会显著高于map的 O (log k);
map的 O (log k) 会随着节点数增加,耗时呈对数级增长,累积下来会带来明显的耗时增加,甚至可能超出题目时间限制导致超时;
本题语法上map可直接替换unordered_map,但时间复杂度会大大提升
本题的节点 ID 可能达到1e9量级(离散且范围极大,这正是使用unordered_map最主要原因),如果使用普通数组存储UF结构体:(vector也不适合这种离散ID,开销太大)
数组要求连续内存空间且预分配固定大小,1e9量级的数组根本无法分配(内存容量远远不足,会直接触发内存溢出错误);
而unordered_map是动态哈希表,只存储实际出现过的节点,不会为未出现的节点分配任何内存。
且每组测试结束后,unordered_map都可自动销毁
*/
// 保留原结构体,新增rel字段(记录与根的关系:0=朋友,1=敌人)
// 算法思想:扩展普通并查集,通过额外的关系字段维护节点间的对立/同类关系(种类并查集核心)
struct UF {
ll id; // 节点自身ID(保留原变量名)
UF* root; // 节点的父节点/根节点指针(保留原变量名)
ll rank; // 节点所在树的秩(用于按秩合并,优化时间复杂度,保留原变量名)
ll rel; // 新增核心字段:记录当前节点与它的根节点之间的关
// 0 = 当前节点与根节点是「朋友/同类」;1 = 当前节点与根节点是「敌人/异类」
};
// 带路径压缩的Root函数,同时更新rel(维护节点与根的关系,种类并查集的核心优化)
// 入参:unordered_map<ll, UF>& a - 存储离散节点的并查集(适配大数据范围,避免数组溢出)
// 入参:ll x - 要查找根节点的目标节点ID
// 返回值:UF* - 目标节点x的最终根节点指针
UF* Root(unordered_map<ll, UF>& a, ll x) {
// 递归终止条件:当前节点的根节点是它自身(说明找到最终根节点)
if (a[x].root != &a[x]) {
// 步骤1:保存当前节点的旧根节点(路径压缩前的父节点),用于后续更新关系
UF* old_root = a[x].root;
// 步骤2:递归查找旧根节点的最终根节点,并完成路径压缩(普通并查集路径压缩逻辑)
// 递归后,old_root->root 会指向最终根节点,同时old_root->rel会更新为old_root到最终根的关系
a[x].root = Root(a, old_root->id);
// 步骤3:核心!路径压缩时,更新当前节点x到最终根节点的关系(关系传递性)
// 逻辑推导:x -> 最终根 的关系 = x -> old_root 的关系 ^ old_root -> 最终根 的关系
// 异或^适用原因:二值关系(0/1)的传递,满足「朋友的朋友是朋友,朋友的敌人是敌人,敌人的敌人是朋友」
a[x].rel ^= old_root->rel;
}
// 返回最终根节点,此时x已直接指向最终根,且x->rel已更新为x到最终根的正确关系
return a[x].root;
}
int main() {
int T;
cin >> T;
// 多组测试用例,每组用例独立处理(避免不同用例间的节点状态干扰)
while (T--) {
ll n;
cin >> n;
// 保留原变量名a,改为unordered_map适配1e9量级的离散节点(解决全局数组内存不足问题)
// 键:节点ID(ll类型),值:对应的UF结构体(存储节点的并查集状态和关系)
unordered_map<ll, UF> a;
bool ok = true; // 标记当前测试用例是否存在矛盾关系(初始化为无矛盾)
// 处理当前测试用例的n条关系指令
for (ll i = 0; i < n; i++) {
ll ai, bi, c;
cin >> ai >> bi >> c;
// 指令含义:ai 和 bi 的关系为 c(c=1:朋友/同类;c=0:敌人/异类)
// 步骤1:初始化节点ai(若未在当前并查集中存在)
if (a.find(ai) == a.end()) {
// 初始化规则:
// 1. 节点ID为自身ai
// 2. 初始根节点指向自身(普通并查集初始化规则)
// 3. 初始秩为1(单个节点构成的树,高度/规模为1)
// 4. 初始关系rel=0(自身与自身必然是朋友/同类,无对立关系)
a[ai] = { ai, &a[ai], 1, 0 };
}
// 步骤2:初始化节点bi(若未在当前并查集中存在),规则同ai
if (a.find(bi) == a.end()) {
a[bi] = { bi, &a[bi], 1, 0 };
}
// 步骤3:查找ai和bi的最终根节点(同时完成路径压缩和关系更新)
UF* ra = Root(a, ai); // ra:ai的最终根节点
UF* rb = Root(a, bi); // rb:bi的最终根节点
// 情况1:ai和bi已经在同一个集合中(根节点相同),需要检查关系是否矛盾
if (ra == rb) {
// 核心逻辑:计算ai和bi的实际关系,与输入的c对比,判断是否矛盾
// 1. 实际关系推导:ai 和 bi 的关系 = (ai到根的关系) ^ (bi到根的关系)
// 2. 期望关系:输入c=1(朋友)对应期望关系为0,c=0(敌人)对应期望关系为1
// 因此期望关系可表示为 1 - c(完成c到实际关系值的转换)
ll expected_rel = 1 - c; // 转换输入c为对应的二值关系期望
ll actual_rel = a[ai].rel ^ a[bi].rel; // 计算ai和bi的实际关系
// 若实际关系与期望关系不一致,说明存在矛盾,标记为false
if (actual_rel != expected_rel) {
ok = false;
}
}
// 情况2:ai和bi不在同一个集合中(根节点不同),需要合并两个集合并维护关系
else {
// 步骤1:按秩合并(普通并查集优化,降低树的高度,保证近似O(1)的操作复杂度)
// 逻辑:将秩小的树合并到秩大的树的根节点下,减少后续路径压缩的开销
if (ra->rank < rb->rank) {
swap(ra, rb); // 交换根节点,确保ra始终是秩更大的根节点
swap(ai, bi); // 同步交换节点,保证后续关系计算的一致性
}
// 步骤2:合并两个集合(将rb的根节点指向ra,完成树的合并)
rb->root = ra;
// 步骤3:核心!计算合并后rb(原小树的根)到ra(原大树的根)的关系(rel)
// 逻辑推导:要保证ai和bi的关系符合输入c,需满足:
// rb->rel = (ai到ra的关系) ^ (bi到rb的关系) ^ (ai和bi的期望关系)
// 其中 ai和bi的期望关系为 1 - c,最终化简为如下公式
rb->rel = a[ai].rel ^ a[bi].rel ^ (1 - c);
// 步骤4:若两个树的秩相等,合并后大树的秩+1(维护秩的正确性,用于后续按秩合并)
if (ra->rank == rb->rank) {
ra->rank++;
}
}
}
// 输出当前测试用例的结果:无矛盾输出YES,有矛盾输出NO
cout << (ok ? "YES" : "NO") << endl;
}
return 0;
}