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\) 就是答案。
为了高效回答所有查询,采用离线并查集:
- 初始时每个城市自成一个连通块,根为自己。
- 每个查询 \((x,y)\) 拆成两个“半查询”:
- 在 \(x\) 当前所在连通块的根上挂一条记录:
(查询编号, y) - 在 \(y\) 当前所在连通块的根上挂一条记录:
(查询编号, x)
- 在 \(x\) 当前所在连通块的根上挂一条记录:
- 当两个连通块合并时,设根分别为 \(r_u\) 和 \(r_v\),按连通块大小启发式合并,只遍历较小连通块上的半查询列表:
- 对于每条记录
(id, other),若ans[id]已有值,跳过。 - 检查
find(other)是否等于合并后的大连通块根。若是,说明查询两端首次连通,ans[id] = d(当前枚举的 \(d\))。 - 否则,将该半查询转移到较大连通块的挂载列表中,继续等待。
- 对于每条记录
- 合并完成后,清空较小连通块的列表,并更新并查集父子关系与大小。
算法步骤
- 读入数据,记录每个城市的魔法值 \(a_i\),并按值建桶:
buk[v]存储所有 \(a_i = v\) 的城市编号。 - 初始化并查集,每个城市独立,大小为 \(1\)。初始化查询挂载列表
qur为空的vector。 - 处理查询:
- 若 \(x = y\),直接
ans[id] = a_x。 - 否则,在
qur[x]中加入(y, id),在qur[y]中加入(x, id)。
- 若 \(x = y\),直接
- 从大到小枚举 \(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)。
- 用一个时间戳
- 输出所有
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;
}

浙公网安备 33010602011771号