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;
}
posted @ 2024-07-21 23:44  MrPython  阅读(6)  评论(0)    收藏  举报  来源