题解:AcWing 237 程序自动分析
【题目来源】
AcWing:237. 程序自动分析 - AcWing题库
【题目描述】
在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
考虑一个约束满足问题的简化版本:假设 \(x_1,x_2,x_3,…\) 代表程序中出现的变量,给定 \(n\) 个形如 \(x_i=x_j\) 或 \(x_i≠x_j\) 的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。
例如,一个问题中的约束条件为:\(x_1=x_2\),\(x_2=x_3\),\(x_3=x_4\),\(x_1≠x_4\),这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
现在给出一些约束满足问题,请分别对它们进行判定。
【输入】
输入文件的第 \(1\) 行包含 \(1\) 个正整数 \(t\),表示需要判定的问题个数,注意这些问题之间是相互独立的。
对于每个问题,包含若干行:
第 \(1\) 行包含 \(1\) 个正整数 \(n\),表示该问题中需要被满足的约束条件个数。
接下来 \(n\) 行,每行包括 \(3\) 个整数 \(i,j,e\),描述 \(1\) 个相等/不等的约束条件,相邻整数之间用单个空格隔开。若 \(e=1\),则该约束条件为 \(x_i=x_j\);若 \(e=0\),则该约束条件为 \(x_i≠x_j\)。
【输出】
输出文件包括 \(t\) 行。
输出文件的第 \(k\) 行输出一个字符串 YES 或者 NO,YES 表示输入中的第 \(k\) 个问题判定为可以被满足,NO 表示不可被满足。
【输入样例】
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1
【输出样例】
NO
YES
【核心思想】
-
问题分析:给定 \(t\) 组测试数据,每组包含 \(n\) 个变量之间的相等(\(x_i = x_j\))或不等(\(x_i \neq x_j\))约束条件,判断是否存在一种赋值方案使得所有约束同时满足。这是一个并查集问题,关键在于利用相等关系的传递性:若 \(x_i = x_j\) 且 \(x_j = x_k\),则 \(x_i = x_k\)。
-
算法选择:
- 并查集(Disjoint Set Union, DSU):维护变量的等价类,支持 \(O(\alpha(n))\) 的合并与查询
- 离散化(Hash/Map):变量编号可能很大(达到 \(10^9\)),需要映射为连续的编号
- 两遍扫描策略:先合并所有相等关系,再检查不等关系是否冲突
-
关键步骤:
- 离散化:使用
unordered_map将原始变量值 \(x, y\) 映射为 \(1 \sim n\) 的连续编号 - 初始化并查集:
p[i] = i,每个变量初始独立 - 第一遍:处理相等约束(\(e = 1\)):
- 对于每个相等约束 \((x_i, x_j)\):
pa = find(x_i),pb = find(x_j)p[pa] = pb:将两个等价类合并
- 对于每个相等约束 \((x_i, x_j)\):
- 第二遍:检查不等约束(\(e = 0\)):
- 对于每个不等约束 \((x_i, x_j)\):
pa = find(x_i),pb = find(x_j)- 若
pa == pb,说明 \(x_i\) 和 \(x_j\) 已在同一等价类,与不等约束矛盾,输出NO
- 对于每个不等约束 \((x_i, x_j)\):
- 结果:若所有不等约束均不矛盾,输出
YES
- 离散化:使用
-
时间/空间复杂度:
- 时间复杂度:\(O(m \cdot \alpha(n))\),其中 \(m\) 为约束条数,\(\alpha\) 为阿克曼函数反函数,近似常数。离散化 \(O(m)\),并查集操作 \(O(m \cdot \alpha(n))\)
- 空间复杂度:\(O(n)\),并查集父节点数组和离散化映射表
-
并查集的核心思想:
- 等价类维护:将相等的变量归入同一集合,利用路径压缩优化查询效率
- 传递闭包:相等关系具有传递性,并查集天然支持这种闭包计算
- 冲突检测策略:必须先处理所有相等关系(建立等价类),再检查不等关系。若顺序颠倒,可能导致错误判断
- 离散化必要性:当变量范围极大而实际出现次数较少时,通过哈希映射压缩空间
- 适用于变量分组、连通性判断、等价关系推理类问题
【算法标签】
并查集
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 200005; // 最大节点数(离散化后最多 n 个节点)
int n, m, T; // n: 离散化后的节点计数, m: 约束条件个数, T: 测试用例数
int p[N]; // 并查集父节点数组
unordered_map<int, int> S; // 离散化映射:原始变量值 -> 连续编号
struct Query
{
int x, y, e; // x,y: 离散化后的变量编号, e: 约束类型(1相等,0不等)
} query[N]; // 存储所有约束条件
// 离散化:将原始变量值映射为连续编号(从1开始)
int get(int x)
{
if (S.count(x) == 0) S[x] = ++n; // 若该值未出现过,分配新编号(n 从0递增)
return S[x]; // 返回该变量对应的编号
}
// 并查集查找(路径压缩)
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]); // 递归查找根节点并压缩路径
return p[x]; // 返回根节点
}
int main()
{
cin >> T; // 读入测试用例数
while (T--) // 遍历每个测试用例
{
n = 0; // 重置节点计数器(用于离散化)
S.clear(); // 清空离散化映射
cin >> m; // 读入约束条件个数
for (int i = 1; i <= m; i++)
{
int x, y, e;
cin >> x >> y >> e; // 读入原始变量值和约束类型
query[i] = {get(x), get(y), e}; // 离散化后存入查询数组
}
for (int i = 1; i <= n; i++) p[i] = i; // 初始化并查集:每个节点自成一个集合
// 第一遍:处理所有 "相等" 约束(e=1),合并对应集合
for (int i = 1; i <= m; i++)
{
if (query[i].e == 1)
{
int pa = find(query[i].x), pb = find(query[i].y);
p[pa] = pb; // 将 x 所在集合的根指向 y 所在集合的根
}
}
// 第二遍:检查所有 "不等" 约束(e=0)是否产生矛盾
bool has_conflict = false;
for (int i = 1; i <= m; i++)
{
if (query[i].e == 0)
{
int pa = find(query[i].x), pb = find(query[i].y);
if (pa == pb) // 若原本应不等的两个变量已在同一集合
{
has_conflict = true; // 发现矛盾,约束不可同时满足
break;
}
}
}
if (has_conflict) cout << "NO" << endl; // 存在矛盾,不可满足
else cout << "YES" << endl; // 无矛盾,可以满足
}
return 0;
}
【运行结果】
2
2
1 2 1
1 2 0
NO
2
1 2 1
2 1 1
YES
浙公网安备 33010602011771号