Funny Game
CF 的题还是一如既往的好玩!
题目要求我们执行 次操作。对于第 次操作(),可以选择一对 满足 ,在点 与点 之间建边。执行所有操作后,所得到的图应为一颗树。
方便起见,将 转化为 。从感觉上看,靠前的操作要靠后的操作容易得多,因此我们选择从后往前反着算。
第一次操作时,。此时 最多只有 种可能的结果,而一共有 个相互独立的点。根据鸽巢原理,能找到至少一对 之间可以建边。连边后,两个点缩成了一个连通块。图上还有 个连通块。
第二次操作时,。此时 最多只有 种可能的结果。从刚才产生的连通块中任选一点,就相当于有 个相互独立的点。根据鸽巢原理,仍可以从这些点中找到至少一对 之间可以建边。连边后,图上还有 个连通块。
……
第 次操作时,。此时 最多只有 种可能的结果。从目前的 个连通块中每个都任取一点,就相当于有 个相互独立的点。根据鸽巢原理,仍可以从这些点中找到至少一对 之间可以建边。连边后,图上还有 个连通块。
由此,我们用数学归纳法证明了一定可以构造出这样的树来,因此先输出 YES
。
具体的实现上,我使用了并查集。初始时所有节点均自成集合。在倒序枚举操作时,将“从每个连通块中任取一点”实现为取该集合在并查集中的根节点即可。拿一个 map 搞一下,如果为并查集树的根节点就以余数为键点编号为值扔到 set 里,同时判重。
#include <bits/extc++.h>
using namespace std;
namespace pbds = __gnu_pbds;
using ui = unsigned int;
using uli = unsigned long long int;
using li = long long int;
struct DSU {
vector<size_t> fa;
DSU(size_t n) : fa(n) { iota(fa.begin(), fa.end(), size_t(0)); }
size_t find(size_t x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
bool check(size_t x, size_t y) { return find(x) == find(y); }
void merge(size_t x, size_t y) { fa[find(x)] = find(y); }
};
int main(void) {
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
size_t T;
cin >> T;
while (T--) {
size_t n;
cin >> n;
vector<ui> a(n);
for (ui &i : a)
cin >> i;
DSU ds(n);
cout << "YES\n";
vector<pair<size_t, size_t>> ans;
for (ui x = n - 1; x >= 1; --x) {
map<ui, size_t> d;
for (size_t i = 0; i < n; ++i)
if (ds.find(i) == i)
if (d.count(a[i] % x)) {
ans.emplace_back(d[a[i] % x], i);
ds.merge(d[a[i] % x], i);
goto next;
} else
d[a[i] % x] = i;
throw;
next:;
}
for_each(ans.rbegin(), ans.rend(), [](auto const &x) { cout << x.first + 1 << ' ' << x.second + 1 << '\n'; });
}
return 0;
}