GCD 之旅

GCD 之旅

图层差分(gcd从小到大),逐层合并

问题重述

给定 \(n\) 个城市,每个城市有魔法值 \(a_i\)。任意两城市 \(u,v\) 之间有一条双向道路,边权为 \(\gcd(a_u, a_v)\)。
对于查询 \((x,y)\),定义一条路径的 MIN 为路径上所有边权的最小值。要求从 \(x\) 到 \(y\) 的所有路径中,MIN 的最大值。
若 \(x=y\),答案为 \(a_x\)。

核心转化:最大瓶颈路径与最大生成树

在无向图中,两点之间所有路径的“最小边权的最大值”等于该图最大生成树上这两点间路径的最小边权。
因此,问题转化为:在完全图上构建最大生成树,然后查询树上路径最小值。
但完全图有 \(O(n^2)\) 条边,不能显式建图,需要利用边权的特殊性质。

利用 gcd 性质批量处理边

边权为 \(\gcd(a_u, a_v)\)。对于整数 \(d\),若 \(d \mid a_u\) 且 \(d \mid a_v\),则 \(\gcd(a_u, a_v) \ge d\)。
因此,所有“边权至少为 \(d\)”的边,连接了所有魔法值能被 \(d\) 整除的城市。
我们可以从大到小枚举 \(d\)(从 \(\max a_i\) 到 \(1\)),对于每个 \(d\),找出所有魔法值是 \(d\) 的倍数的城市,将它们所在的连通块合并。
这等价于按边权从大到小加入所有边权 \(\ge d\) 的边,模拟 Kruskal 算法构建最大生成树。

离线并查集处理查询

对于查询 \((x,y)\),我们希望找到最大的 \(d\),使得存在一条从 \(x\) 到 \(y\) 的路径,路径上每条边的 \(\gcd\) 都 \(\ge d\)。
当我们从大到小枚举 \(d\) 并合并连通块时,如果某一步 \(x\) 和 \(y\) 第一次被合并到同一个连通块,那么当前的 \(d\) 就是答案。
为了高效回答所有查询,采用离线并查集:

  1. 初始时每个城市自成一个连通块,根为自己。
  2. 每个查询 \((x,y)\) 拆成两个“半查询”:
    • 在 \(x\) 当前所在连通块的根上挂一条记录:(查询编号, y)
    • 在 \(y\) 当前所在连通块的根上挂一条记录:(查询编号, x)
  3. 当两个连通块合并时,设根分别为 \(r_u\) 和 \(r_v\),按连通块大小启发式合并,只遍历较小连通块上的半查询列表:
    • 对于每条记录 (id, other),若 ans[id] 已有值,跳过。
    • 检查 find(other) 是否等于合并后的大连通块根。若是,说明查询两端首次连通,ans[id] = d(当前枚举的 \(d\))。
    • 否则,将该半查询转移到较大连通块的挂载列表中,继续等待。
  4. 合并完成后,清空较小连通块的列表,并更新并查集父子关系与大小。

算法步骤

  1. 读入数据,记录每个城市的魔法值 \(a_i\),并按值建桶:buk[v] 存储所有 \(a_i = v\) 的城市编号。
  2. 初始化并查集,每个城市独立,大小为 \(1\)。初始化查询挂载列表 qur 为空的 vector。
  3. 处理查询:
    • 若 \(x = y\),直接 ans[id] = a_x。
    • 否则,在 qur[x] 中加入 (y, id),在 qur[y] 中加入 (x, id)。
  4. 从大到小枚举 \(d\)(从 \(\max a_i\) 到 \(1\)):
    • 用一个时间戳 timer 和标记数组 mark 对根去重。
    • 遍历 \(d\) 的所有倍数 \(m = d, 2d, 3d, \dots\),对于每个 \(m\),取出 buk[m] 中的城市,找到其并查集根 \(r\)。若 mark[r] != timer,则标记并加入临时列表 roots。
    • 若 roots 大小 \(\le 1\),跳过。
    • 取 roots[0] 作为代表,依次与 roots[1..] 调用 unite(rep, roots[i], d)。
  5. 输出所有 ans[id]。

并查集合并函数 unite(x, y, d) 细节

  • 找到 \(x,y\) 的根 \(rx, ry\)。若相同则返回。
  • 按连通块大小启发式合并:若 \(sz[rx] > sz[ry]\),交换 \(rx, ry\),确保 \(rx\) 是较小根。
  • 遍历 qur[rx] 中的每个半查询 (u, id):
    • 若 ans[id] 非零,跳过。
    • 令 ru = find(u)。
    • 若 ru == ry,则 ans[id] = d。
    • 否则,将 (u, id) 追加到 qur[ry] 中。
  • 清空 qur[rx]。
  • 令 pa[rx] = ry,sz[ry] += sz[rx]。

复杂度分析

  • 枚举倍数:\(\sum_{d=1}^{MAXV} \frac{MAXV}{d} = O(MAXV \log MAXV)\),其中 \(MAXV = 10^6\),约 \(1.4 \times 10^7\) 次。
  • 访问城市:每个城市会被其所有约数对应的 \(d\) 访问一次,总次数为 \(\sum_{i=1}^n \tau(a_i)\)。平均约数个数约几十,最坏(高合成数)约 \(240\),当 \(n=10^6\) 时最坏约 \(2.4 \times 10^8\),实际远小于此。
  • 并查集合并与查询转移:每个半查询最多从较小连通块转移到较大连通块 \(O(\log n)\) 次,总共有 \(2q\) 个半查询,因此总转移次数 \(O(q \log n)\)。每次转移均摊 \(O(1)\)。路径压缩后 find 接近 \(O(1)\)。
  • 总复杂度:\(O(MAXV \log MAXV + \sum \tau(a_i) + (n + q) \log n)\),空间 \(O(n + q + MAXV)\)。

实现要点与优化

  • 使用 vector<vector<int>> buk 按值存储城市编号,避免遍历所有城市。
  • 使用时间戳 timer 和 mark 数组对并查集根去重,避免每次清空大数组。
  • 查询挂载列表使用 vector<vector<pair<int,int>>>,合并时只移动较小列表,并用 ans[id] 标记已解决查询。
  • 输入输出使用 ios::sync_with_stdio(false); cin.tie(0); 加速。
  • 若担心最坏情况,可预先只保留非空的桶,但通常不需要。

正确性证明概要

  • 按 \(d\) 从大到小枚举并合并所有 \(a_i\) 是 \(d\) 的倍数的城市,等价于按边权从大到小加入所有边权 \(\ge d\) 的边。这与 Kruskal 算法构建最大生成树的过程一致。
  • 对于任意查询 \((x,y)\),当 \(x\) 和 \(y\) 首次被合并到同一连通块时,当前枚举的 \(d\) 就是使得 \(x\) 与 \(y\) 连通的最大边权阈值,即最大瓶颈路径的 MIN 值。
  • 离线并查集保证每个查询只被解决一次,且答案被正确记录。

特殊情形

  • \(x = y\):旅行者未离开城市,MIN 为 \(a_x\),直接输出。
  • 所有 \(a_i\) 互质时,最大 MIN 为 \(1\),算法会枚举到 \(d=1\) 时合并所有城市,查询得到答案 \(1\)。
  • 若 \(n=1\),只有自查询,直接输出 \(a_1\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 10;
int ans[N];

struct dsu {
    int n; vector<int> pa, sz;
    vector<vector<pair<int, int>>> qur;
    dsu(int _n) {init(_n);}
    void init(int _n) {
        n = _n;
        pa.assign(n + 1, 0); sz.assign(n + 1, 1);
        qur.assign(n + 1, vector<pair<int, int>>());
        iota(pa.begin(), pa.end(), 0);
    }
    int findu(int x) {
        while(pa[x] != x) {
            pa[x] = pa[pa[x]];
            x = pa[x];
        }
        return x;
    }
    void unite(int x, int y, int d) {
        int rx = findu(x), ry = findu(y);
        if(rx == ry) return;
        if(sz[rx] > sz[ry]) swap(rx, ry);

        for(auto [u, id] : qur[rx]) {
            if(ans[id]) continue;
            int ru = findu(u);
            if(ru == ry) ans[id] = d;
            else qur[ry].push_back({u, id});
        }
        pa[rx] = ry;
        sz[ry] += sz[rx];
        qur[rx].clear();
    }
};

void solve() {
    int n, q; cin >> n >> q;
    vector<int> a(n + 1);
    int mx = 0;
    for(int i = 1; i <= n; i ++) {
        cin >> a[i];
        mx = max(a[i], mx);
    }
    vector<vector<int>> buk(mx + 1);
    for(int i = 1; i <= n; i ++) buk[a[i]].push_back(i);

    dsu dsu(n);
    for(int i = 1; i <= q; i ++) {
        int x, y; cin >> x >> y;
        if(x == y) ans[i] = a[x];
        else {
            dsu.qur[x].push_back({y, i});
            dsu.qur[y].push_back({x, i});
        }
    }

    vector<int> mark(n + 1, 0);
    int timer = 0;
    for(int d = mx; d >= 1; d --) {
        ++ timer;
        vector<int> root;
        for(int i = d; i <= mx; i += d) {
            for(int city : buk[i]) {
                int r = dsu.findu(city);
                if(mark[r] != timer) {
                    mark[r] = timer;
                    root.push_back(r);
                }
            }
        }
        if(root.size() <= 1) continue;
        for(int i = 1; i < root.size(); i ++) dsu.unite(root[i - 1], root[i], d);
    }
    for(int i = 1; i <= q; i ++) cout << ans[i] << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    int _ = 1; //cin >> _;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-11 21:09  thenrify  阅读(11)  评论(0)    收藏  举报