Relearn through Review
Relearn through Review
ST表
题目大意
给定一个长度为 (n) 的整数序列 (a) 和一个非负整数 (k)。允许进行至多一次操作:选择一段连续子数组 ([l, r]),将该子数组内的所有元素加上 (k)。求操作后,整个序列的最大公约数(GCD)最大是多少。
核心思路拆解
1. 问题转化:将序列分为三段
假设我们选择的加 (k) 的区间是 ([l, r]),那么修改后的整个序列可以看作由三部分组成:
- 左段(不变):(a_1, a_2, \dots, a_{l-1})
- 中段(加 (k)):(a_l+k, a_{l+1}+k, \dots, a_r+k)
- 右段(不变):(a_{r+1}, a_{r+2}, \dots, a_n)
整个序列的最大公因数,就是这三段各自最大公因数的再求 GCD。
利用 GCD 的经典性质 (\gcd(x, y) = \gcd(x, y-x)),我们可以将中段的 GCD 转化为其端点和差分数组的 GCD:
[
\gcd(a_l+k, \dots, a_r+k) = \gcd(a_r+k, |a_{l+1}-a_l|, \dots, |a_r-a_{r-1}|)
]
因此,对于任意选定的区间 ([l, r]),最终整个序列的 GCD 为:
[
\gcd(\underbrace{\text{pre}(l-1)}{\text{前缀}}, \underbrace{\text{diff}(l, r)}{\text{差分 GCD}}, \underbrace{\text{suf}(r+1)}{\text{后缀}}, \underbrace{a_r+k}{\text{中段右端点}})
]
2. 为什么用“差分”?
在算法竞赛中,“区间加法”和“区间 GCD”往往很难直接维护。
观察中段的差分项 (|b_{i+1} - b_i|):
[
|b_{i+1} - b_i| = |(a_{i+1} + k) - (a_i + k)| = |a_{i+1} - a_i|
]
奇迹发生:因为区间内每个数都加了 (k),所以它们相邻的差值,完全不受 (k) 的影响!
因此,中段的 GCD 就变成了 (\gcd(a_r+k, \text{区间差分 GCD})),问题成功转化为经典模型。
3. 如何高效选择 (l, r)?(核心剪枝)
直接枚举 (l) 和 (r) 有 (O(n^2)) 种情况,必然超时。我们需要利用数论性质大幅剪枝。
如果固定右端点 (r),那么后缀 GCD ((\text{suf}(r+1))) 和 (a_r+k) 就都是定值了。此时,随着左端点 (l) 的增大:
- 前缀 GCD((\text{pre}(l-1))):会呈阶梯状变小。由于每次变小至少折半,所以最多只有 (O(\log X)) 种不同的取值((X \le 10^{18}),最多约 60 种)。
- 差分 GCD((\text{diff}(l, r))):当 (l) 增大时,区间 ([l, r]) 变短,涉及的差分元素变少,差分 GCD 只可能变大(且变大后的值必定是原值的倍数)。
贪心结论:
假设在某一段区间内,前缀 GCD 的值固定为 (P)。因为 (l) 变大时,差分 GCD 会变大,而其他两项都不变。所以,在这个使得前缀 GCD 不变的区间里,(l) 取得越大越好(直接取最右侧的 (l))。
跳跃寻找 (l):
由于前缀 GCD 数组是单调不增的,我们可以利用二分查找,直接跳过所有相等的 (\text{pre}) 值,找到下一个使得 GCD 发生变化的 (l)。
算法流程
- 预处理:
- 计算前缀 GCD 数组:
pre[i] = gcd(pre[i-1], a[i])。 - 计算后缀 GCD 数组:
suf[i] = gcd(suf[i+1], a[i])。 - 构造差分数组
d[i] = abs(a[i] - a[i-1]),并使用 ST表 维护差分数组的区间 GCD 查询,做到 (O(1)) 查询。
- 计算前缀 GCD 数组:
- 外层枚举 (r)((1 \le r \le n)):
- 固定 (r),开始跳跃枚举 (l)(初始
l = r)。
- 固定 (r),开始跳跃枚举 (l)(初始
- 内层跳跃枚举 (l):
- 计算当前候选的答案:(G = \gcd(\text{pre}[l-1], \text{diff}(l, r), \text{suf}[r+1], a[r]+k)),更新全局最大值。
- 利用二分查找在 (\text{pre}) 数组中,找到最靠左的与当前 (\text{pre}[l-1]) 相等的值的位置,把 (l) 移动到该位置,开启下一轮跳跃。
复杂度分析
- 预处理前缀/后缀及构建 ST 表:(O(n \log n))
- 外层枚举 (r):(O(n))
- 内层跳跃枚举 (l):最多 (O(\log X)) 次,每次包含二分 (O(\log n)) 和 ST 表查询 (O(1))。
- 总时间复杂度:(O(n \log X \log n))。在 (n=3 \times 10^5) 时,运行效率极高,稳过。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct SparseTable {
int n;
vector<int> lg;
vector<vector<ll>> st;
SparseTable(const vector<ll>& d) {
n = d.size() - 1;
if(n <= 0) return;
lg.assign(n + 1, 0);
for(int i = 2; i <= n; i ++) lg[i] = lg[i / 2] + 1;
int K = lg[n] + 1;
st.assign(n + 1, vector<ll>(K, 0));
for(int i = 1; i <= n; i ++) st[i][0] = d[i];
for(int j = 1; j < K; j ++) {
for(int i = 1; i + (1 << j) - 1 <= n; i ++) {
st[i][j] = gcd(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
ll query(int L, int R) {
if(L > R || n == 0) return 0;
int j = lg[R - L + 1];
return gcd(st[L][j], st[R - (1 << j) + 1][j]);
}
};
void solve() {
int n; ll k; cin >> n >> k;
vector<ll> a(n + 1, 0);
for(int i = 1; i <= n; i ++) cin >> a[i];
vector<ll> pre(n + 1, 0), suf(n + 2, 0);
for(int i = 1; i <= n; i ++) pre[i] = gcd(pre[i - 1], a[i]);
for(int i = n; i >= 1; i --) suf[i] = gcd(suf[i + 1], a[i]);
ll ans = pre[n];
vector<ll> d(n + 2, 0);
for(int i = 2; i <= n; i ++) d[i] = abs(a[i] - a[i - 1]);
SparseTable st(d);
for(int r = 1; r <= n; r ++) {
int l = r;
while(l >= 1) {
ll p = pre[l - 1], s = suf[r + 1];
ll di = 0;
if(l < r) di = st.query(l + 1, r);
ll ri = a[r] + k;
ll res = gcd(gcd(p, s), gcd(di, ri));
ans = max(res, ans);
int L = 1, R = l - 1, id = l - 1;
while(L <= R) {
int mid = (L + R) / 2;
if(pre[mid] <= p) {
R = mid - 1;
id = mid;
}
else L = mid + 1;
}
l = id;
}
}
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号