0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1955,日期见页头。两边不一致时信原站。
题目描述
考虑一个约束满足问题的简化版本:假设 x₁, x₂, x₃, ⋯ 代表程序中出现的变量,
给定 n 个形如 xᵢ = xⱼ 或 xᵢ ≠ xⱼ 的变量相等 / 不等的约束条件,
请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。
例如,一个问题中的约束条件为 x₁ = x₂, x₂ = x₃, x₃ = x₄, x₄ ≠ x₁,
这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
输入格式
输入的第一行包含一个正整数 t,表示需要判定的问题个数。注意这些问题之间是相互独立的。
对于每个问题:第一行包含一个正整数 n,表示该问题中需要被满足的约束条件个数。
接下来 n 行,每行包括三个整数 i, j, e。
若 e = 1,则该约束条件为 xᵢ = xⱼ;若 e = 0,则该约束条件为 xᵢ ≠ xⱼ。
输出格式
输出包括 t 行。第 k 行输出 YES 或 NO(字母全部大写),
YES 表示第 k 个问题判定为可以被满足,NO 表示不可被满足。
数据规模与约定
1 ≤ t ≤ 10;对于全部数据,1 ≤ n ≤ 10⁵,1 ≤ i, j ≤ 10⁹,e ∈ {0, 1}。
时限 2 秒,内存 512 MB。
输入输出样例
输入
2 2 1 2 1 1 2 0 2 1 2 1 2 1 1
输出
NO YES
第一个问题:x₁ = x₂ 和 x₁ ≠ x₂ 互相矛盾 ⇒ NO;
第二个问题:两个约束等价 ⇒ YES。
⚠ 两组官方样例里的「不等」全都排在最后 —— 记住这句话,第 ② 步要用它。
1★★ 关键的一步:先把所有「相等」并完,再回头验所有「不等」
// P1955 [NOI2015] 程序自动分析 —— ★ 这一版就能 AC//// ★★ 关键的一步是**顺序**:**先把所有 `xᵢ = xⱼ` 全部合并完,再回头验所有 `xᵢ ≠ xⱼ`。**// 理由一句话:并查集只能合并、不能拆 ——// 一条「不等」约束在**它出现的那一刻**可能还看不出矛盾,// 而后面来的「相等」会把两边并到一起。⇒ 边读边判就是错的(本页第 ② 步)。//// ⚠ 另一件事:`i, j ≤ 10⁹` 而 `n ≤ 10⁵` ⇒ **必须离散化**(本页第 ③ 步)。// ⚠ 多组数据,每组之间要清空(本页第 ④ 步)。#include <bits/stdc++.h>using namespace std;
static const int N = 200005; // 每组最多 2n 个不同的变量static int fa[N];
static int find(int x) { int r = x; while (fa[r] != r) r = fa[r]; while (fa[x] != r) { int nx = fa[x]; fa[x] = r; x = nx; } return r;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; if (!(cin >> t)) return 0; string out; while (t--) { int n; cin >> n; vector<array<int, 3>> c(n); vector<int> vals; vals.reserve(2 * n); for (int k = 0; k < n; k++) { int i, j, e; cin >> i >> j >> e; c[k] = {i, j, e}; vals.push_back(i); vals.push_back(j); } sort(vals.begin(), vals.end()); vals.erase(unique(vals.begin(), vals.end()), vals.end()); int cnt = (int)vals.size(); for (int i = 0; i < cnt; i++) fa[i] = i; // ★ 只清用到的那一段 auto id = [&](int v) { return (int)(lower_bound(vals.begin(), vals.end(), v) - vals.begin()); }; for (auto& q : c) if (q[2] == 1) fa[find(id(q[0]))] = find(id(q[1])); // ① 先并 bool ok = true; for (auto& q : c) if (q[2] == 0 && find(id(q[0])) == find(id(q[1]))) { ok = false; break; } // ② 再验 out += ok ? "YES\n" : "NO\n"; } cout << out; return 0;}点「运行 ▶」看结果
一条「不等」约束在它出现的那一刻可能还看不出矛盾 —— 把它两边连起来的那些「相等」还在后面没读到。 而并查集只能合并、不能拆 ⇒ 后来的合并没法回头把它判成矛盾。
⇒ 所以必须两趟:第一趟把 e = 1 全部合并,第二趟才逐条验 e = 0。
★ 这和隔壁 P1197 是同一句话的两个用法:
那道题靠把时间倒过来,这道题靠把约束分成两趟。
2★★★ 而「边读边判」那一版,两组官方样例一个都挡不住
// P1955 · 错法 ①:边读边判 —— 顺序错了就全错//// e = 1 ⇒ 立刻合并;e = 0 ⇒ **立刻**看两边是不是已经同根,是就判 NO。// ⚠ 问题在于:一条「不等」出现的那一刻,把它俩连起来的那些「相等」可能**还没读到**。// 而并查集只能合并、不能拆 ⇒ 后来的合并没法回头把它判成矛盾。// ★ 它**恒把 NO 判成 YES**(漏判),答案只会偏「宽松」。#include <bits/stdc++.h>using namespace std;
static const int N = 200005; // 每组最多 2n 个不同的变量static int fa[N];
static int find(int x) { int r = x; while (fa[r] != r) r = fa[r]; while (fa[x] != r) { int nx = fa[x]; fa[x] = r; x = nx; } return r;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; if (!(cin >> t)) return 0; string out; while (t--) { int n; cin >> n; vector<array<int, 3>> c(n); vector<int> vals; vals.reserve(2 * n); for (int k = 0; k < n; k++) { int i, j, e; cin >> i >> j >> e; c[k] = {i, j, e}; vals.push_back(i); vals.push_back(j); } sort(vals.begin(), vals.end()); vals.erase(unique(vals.begin(), vals.end()), vals.end()); int cnt = (int)vals.size(); for (int i = 0; i < cnt; i++) fa[i] = i; // ★ 只清用到的那一段 auto id = [&](int v) { return (int)(lower_bound(vals.begin(), vals.end(), v) - vals.begin()); }; bool ok = true; for (auto& q : c) { // ✗ 一趟走完,读到哪判到哪 if (q[2] == 1) fa[find(id(q[0]))] = find(id(q[1])); else if (find(id(q[0])) == find(id(q[1]))) { ok = false; break; } } out += ok ? "YES\n" : "NO\n"; } cout << out; return 0;}点「运行 ▶」看结果
| 官方样例 | 那一组的约束顺序 | 「边读边判」对不对 |
|---|---|---|
| 样例 1 · 问题一 | = 然后 ≠ |
★ 对(不等在最后) |
| 样例 1 · 问题二 | 两条都是 = |
★ 对(一条不等都没有) |
| 样例 2 · 问题一 | 三条都是 = |
★ 对 |
| 样例 2 · 问题二 | 三条 = 然后一条 ≠ |
★ 对(不等在最后) |
⇒ 四个问题全部「不等在最后」 —— 而那正好是这个 bug 的盲区: 不等排在最后时,读到它时相等已经全并完了,一趟和两趟是同一件事。
★ 专门造一档「不等全排在最后」来印证这件事:
| 300 轮 | 档 0 顺序打乱 | ★★ 档 1 不等全排最后 | 档 2 只有一组 | 档 3 变量号 10⁹ |
|---|---|---|---|---|
| 「边读边判」被抓 | 37 | ★ 0(能证) | 27 | ★ 0 |
⇒ 又一次「它过了样例的第三种原因:这组样例在结构上问不出这个问题」——
⚠ 而这一次连两组样例、四个问题都是同一个结构。
★ 档 3 那个 0 是另一回事:变量号铺到 10⁹ 之后约束几乎互不相干,
两版一起答 YES —— 那是在验零,不是它对。
3⚠ 离散化:题面的 10⁹ 和 n 的 10⁵ 差 5000 倍
| 题面变量号上限 | 10⁹ |
| 一组里用得到的变量最多 | 2n = 2 × 10⁵ |
| ⇒ 差 | ★ 5000 倍 |
⇒ 数组开不到 10⁹,必须离散化(排序 + 去重 + lower_bound)。
// P1955 · 错法 ②:拿取模当离散化//// `fa[i % 1000003]` —— 「反正 i ≤ 10⁹,模一个大质数就当下标好了」。// ⚠ 这是**哈希**,不是离散化:两个不同的变量模出同一个下标,就被当成了同一个变量。// ⇒ 又一次[第 49 章那条](/ch/49-hashing/):**哈希会错,而且它错得很安静**。// ★ 顺手写的生成器变量号只有几十个 ⇒ 这一档是**结构性的精确 0**(本页第 ③ 步)。#include <bits/stdc++.h>using namespace std;
static const int N = 200005; // 每组最多 2n 个不同的变量static int fa[N];
static int find(int x) { int r = x; while (fa[r] != r) r = fa[r]; while (fa[x] != r) { int nx = fa[x]; fa[x] = r; x = nx; } return r;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; if (!(cin >> t)) return 0; string out; while (t--) { int n; cin >> n; vector<array<int, 3>> c(n); vector<int> vals; vals.reserve(2 * n); for (int k = 0; k < n; k++) { int i, j, e; cin >> i >> j >> e; c[k] = {i, j, e}; vals.push_back(i); vals.push_back(j); } const int MOD = 1000003; // ✗ 拿取模当离散化 for (int v : vals) fa[v % MOD % N] = v % MOD % N; auto id = [&](int v) { return v % MOD % N; }; for (auto& q : c) if (q[2] == 1) fa[find(id(q[0]))] = find(id(q[1])); // ① 先并 bool ok = true; for (auto& q : c) if (q[2] == 0 && find(id(q[0])) == find(id(q[1]))) { ok = false; break; } // ② 再验 out += ok ? "YES\n" : "NO\n"; } cout << out; return 0;}点「运行 ▶」看结果
fa[i % 1000003] —— 「反正 i ≤ 10⁹,模一个大质数就当下标好了」。
⚠ 这是哈希,不是离散化:两个不同的变量模出同一个下标就被当成了同一个变量
(又一次第 49 章那条:哈希会错,而且错得很安静)。
| 300 轮 | 档 0 | 档 1 | 档 2 | 档 3 变量号 10⁹ | ★★★ 档 5 专门造碰撞 |
|---|---|---|---|---|---|
| 「取模当离散化」被抓 | ★ 0 | ★ 0 | ★ 0 | ★ 0 | ★ 297 |
★★ 四个档的 0 都不是结构性的,是概率低:一组里只有十几个变量,
从 10⁹ 里随机撞上同余的概率是百万分之几 —— 加多少轮都没用。
★ 而造它只要一行:变量号成对地取 v 和 v + 1000003。当场 297 / 300。
⇒ 又一次「抓不到时别加轮数,去想那条线在哪儿、然后照着它造」。
4⚠ 多组数据:题面第一句就写着「这些问题之间是相互独立的」
上一组并出来的关系留在数组里,而下一组离散化之后用的还是 0, 1, 2, … 这些下标
⇒ 上一组的合并直接污染下一组。
| 300 轮 | 档 0 | 档 1 | ★ 档 2 只有一组 | 档 3 | 档 5 |
|---|---|---|---|---|---|
t == 1 的轮数 |
96 | 96 | 300 | 96 | — |
| ⇒ 「没清空」漏掉的轮数 | ⚠ 216 | 208 | ★ 300 | 248 | 244 |
★ 档 2 那一列 300 ≡ 300 是能证的(只有一组,没有上一组可以污染); ⚠ 而别的档差得远 —— 上一组的合并不一定改变这一组的结论。 ★ 它恒把 YES 判成 NO(多出来的合并只会制造矛盾), 和第 ② 步那个「边读边判」(恒把 NO 判成 YES)方向正好相反。
正解里那一行是 for (int i = 0; i < cnt; i++) fa[i] = i; —— cnt 是这一组离散化后的变量个数。
★ 换成每组 memset 整个 2 × 10⁵ 的数组也不慢(t ≤ 10,一共才 2 × 10⁶);
⚠ 但如果数组是按 10⁹ 开的(那本来就不该),每组清一遍就是灾难。
⇒ 「多组数据要清空」和「清多大一段」是两个问题,后者由你选的存法决定。
5★ 对拍这一页
300 轮(t 随机 1n 随机 3 |
档 0 | ★★ 档 1 不等在最后 | ★ 档 2 只有一组 | 档 3 变量号 10⁹ | ★★★ 档 5 造碰撞 |
|---|---|---|---|---|---|
| 边读边判 | 37 | ★ 0 | 27 | ★ 0 | ★ 0 |
| 取模当离散化 | ★ 0 | ★ 0 | ★ 0 | ★ 0 | ★ 297 |
| 多组没清空 | 84 | 92 | ★ 0 | 52 | 56 |
★★★ 官方样例三个错法一个都没挡住 —— 三个原因各不相同: 「边读边判」是结构上问不出(四个问题的不等全在最后)、 「取模当离散化」是规模不够(变量号才 1、2)、 「多组没清空」是运气(那两组恰好互不干扰)。
6度量程序和生成器
7一页纸
| ★★ 关键的一步 | 先并完所有「相等」,再验所有「不等」 —— 还是那句「并查集只能合并不能拆」 |
| ★★★ 而样例问不出来 | 两组样例、四个问题的「不等」全都排在最后 ⇒ 「边读边判」全对 |
| ★ 专门造一档才看得见 | 「不等全排最后」那一档 0(能证),顺序打乱的档抓 37 |
| ⚠ 离散化 | 题面 10⁹ vs 用得到的 2n = 2×10⁵ —— 差 5000 倍 |
| ★★★ 拿取模当离散化 | 照题面随机四个档全是 0(概率低,不是结构性),★ 专门造碰撞当场 297 / 300 |
| ⚠ 多组没清空 | 恒把 YES 判成 NO;t == 1 那一档 300 ≡ 300 是能证的 0 |
| ★ 两个错法方向相反 | 「边读边判」恒把 NO 判成 YES,「没清空」恒把 YES 判成 NO |