Relearn through Review

Relearn through Review

ST表

题目大意

给定一个长度为 (n) 的整数序列 (a) 和一个非负整数 (k)。允许进行至多一次操作:选择一段连续子数组 ([l, r]),将该子数组内的所有元素加上 (k)。求操作后,整个序列的最大公约数(GCD)最大是多少。

核心思路拆解

1. 问题转化:将序列分为三段

假设我们选择的加 (k) 的区间是 ([l, r]),那么修改后的整个序列可以看作由三部分组成:

  1. 左段(不变):(a_1, a_2, \dots, a_{l-1})
  2. 中段(加 (k)):(a_l+k, a_{l+1}+k, \dots, a_r+k)
  3. 右段(不变):(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)。

算法流程

  1. 预处理:
    • 计算前缀 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)) 查询。
  2. 外层枚举 (r)((1 \le r \le n)):
    • 固定 (r),开始跳跃枚举 (l)(初始 l = r)。
  3. 内层跳跃枚举 (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;
}
posted @ 2026-11-03 21:45  thenrify  阅读(3)  评论(0)    收藏  举报