题解:AcWing 246 区间最大公约数

【题目来源】

AcWing:246. 区间最大公约数 - AcWing题库

【题目描述】

给定一个长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:

  1. C l r d,表示把 \(A[l],A[l+1],…,A[r]\) 都加上 \(d\)
  2. Q l r,表示询问 \(A[l],A[l+1],…,A[r]\) 的最大公约数(\(GCD\))。

对于每个询问,输出一个整数表示答案。

【输入】

第一行两个整数 \(N,M\)

第二行 \(N\) 个整数 \(A[i]\)

接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。

【输出】

对于每个询问,输出一个整数表示答案。

每个答案占一行。

【输入样例】

5 5
1 3 5 7 9
Q 1 5
C 1 5 1
Q 1 5
C 3 3 6
Q 2 4

【输出样例】

1
2
4

【核心思想】

  1. 问题分析:给定长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,支持区间 \([l, r]\)\(d\) 和查询区间 \([l, r]\) 的最大公约数。关键在于如何在区间修改的同时高效维护区间 GCD。

  2. 算法选择

    • 差分数组(Difference Array):将区间加法转化为差分数组的单点修改,实现 \(O(1)\) 区间标记
    • 线段树(Segment Tree):维护差分数组的区间 GCD,支持单点修改和区间 GCD 查询
    • 树状数组(Fenwick Tree):维护差分数组的前缀和,用于快速查询 \(A[l]\) 的值
    • 数学性质:利用 \(\gcd(a_1, a_2, ..., a_n) = \gcd(a_1, a_2-a_1, a_3-a_2, ..., a_n-a_{n-1})\) 的性质,将区间 GCD 转化为差分数组的 GCD
  3. 关键步骤

    • 初始化:读取 \(N\)(数组长度)、\(M\)(指令数)、\(A[1..N]\)(初始数组)
    • 构建差分数组\(b[i] = A[i] - A[i-1]\),其中 \(b[1] = A[1]\)
    • 树状数组初始化:将差分数组 \(b[i]\) 加入树状数组,支持前缀和查询得到 \(A[i]\)
    • 线段树建立:维护差分数组 \(b\) 的区间 GCD
    • 处理查询指令 Q l r
      • 通过树状数组前缀和查询 \(A[l] = \sum_{i=1}^{l} b[i]\)
      • 通过线段树查询 \(\gcd(b[l+1], b[l+2], ..., b[r])\)
      • 答案为 \(\gcd(A[l], \gcd(b[l+1..r]))\),即 \(\gcd(A[l..r])\)
      • \(l = r\),直接输出 \(|A[l]|\)
    • 处理修改指令 C l r d
      • 树状数组单点修改:add(l, d)add(r+1, -d)
      • 线段树单点更新:update(l, d)update(r+1, -d)(若 \(r+1 \leq n\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O((N + M) \log N)\),线段树和树状数组操作均为 \(O(\log N)\)
    • 空间复杂度:\(O(N)\),线段树(\(4N\))+ 树状数组(\(N\))+ 原数组
  5. 线段树维护 GCD 的核心思想

    • 差分降维:利用 \(\gcd(A[l..r]) = \gcd(A[l], \gcd(b[l+1..r]))\) 的数学性质,将区间修改下的区间 GCD 查询转化为差分数组的区间 GCD 查询
    • 区间修改转单点修改:对原数组区间 \([l, r]\)\(d\),等价于对差分数组 \(b[l] += d\)\(b[r+1] -= d\),仅影响两个端点
    • 线段树维护 GCD:父节点的 GCD 等于左右子节点 GCD 的 GCD,即 \(\gcd(l.v, r.v)\)
    • 树状数组维护前缀和:通过前缀和还原原数组的值 \(A[l]\),用于与差分 GCD 合并得到最终答案
    • 适用于区间加减与区间 GCD 查询类问题

【算法标签】

线段树

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止数值溢出
const int N = 500005;           // 最大数组长度
int n, m;                       // n: 数组长度, m: 指令数
int w[N];                       // 原数组
int tr1[N];                     // 树状数组(维护差分数组的前缀和)
struct Node
{
    int l, r, v;                // 区间左右端点、区间差分数组的GCD值
} tr[N * 4];                    // 线段树数组(4倍空间)

// 树状数组:lowbit运算,提取x的最低位1对应的2次幂数
int lowbit(int x)
{
    return x & -x;
}

// 树状数组:单点修改(向后修),在位置x加上c
void add(int x, int c)
{
    for (int i = x; i <= n; i += lowbit(i)) tr1[i] += c;
}

// 树状数组:前缀查询(向前查),查询[1,x]的和
int sum(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr1[i];
    return res;
}

// 辗转相除法求GCD
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

// 线段树:向上更新,父节点的v = 左右子节点v的GCD
void pushup(int u)
{
    auto &root = tr[u], &l = tr[u << 1], &r = tr[u << 1 | 1];
    tr[u].v = gcd(l.v, r.v);
}

// 线段树:建立,维护差分数组b[i] = w[i] - w[i-1]的GCD
void build(int u, int l, int r)
{
    if (l == r)                 // 叶子节点
        tr[u] = {l, r, w[r] - w[r - 1]};                    // 差分数组的值
    else
    {
        tr[u] = {l, r};         // 初始化当前节点的区间范围
        int mid = l + r >> 1;   // 取中点(等价于 (l+r)/2)
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 递归建立左右子树
        pushup(u);              // 向上更新当前节点
    }
}

// 线段树:区间查询GCD
int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r)                       // 当前节点区间完全包含在查询区间内
        return tr[u].v;
    int mid = tr[u].l + tr[u].r >> 1;                       // 取中点
    int v = 0;
    if (l <= mid) v = query(u << 1, l, r);                  // 左子树有贡献
    if (r > mid) v = gcd(v, query(u << 1 | 1, l, r));       // 右子树有贡献,与左子树结果取GCD
    return v;
}

// 线段树:单点修改,在差分数组位置pos加上d
void update(int u, int pos, int d)
{
    if (tr[u].l == tr[u].r)     // 叶子节点
    {
        tr[u].v += d;           // 差分数组的值增加d
        return;
    }
    int mid = tr[u].l + tr[u].r >> 1;                       // 取中点
    if (pos <= mid) update(u << 1, pos, d);                 // 在左子树
    else update(u << 1 | 1, pos, d);                        // 在右子树
    pushup(u);                  // 修改后向上更新
}

signed main()                   // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n >> m;              // 读入数组长度和指令数
    for (int i = 1; i <= n; i++)
        cin >> w[i];            // 读入原数组

    // 初始化树状数组:将差分数组 b[i] = w[i] - w[i-1] 加入树状数组
    for (int i = 1; i <= n; i++)
        add(i, w[i] - w[i - 1]);
    build(1, 1, n);         // 建立线段树,维护差分数组,区间为[1, n]

    char op;
    int l, r, d;
    while (m--)                 // 依次处理每条指令
    {
        cin >> op;
        if (op == 'Q')          // 查询指令
        {
            cin >> l >> r;
            int left = sum(l);                          // 树状数组查询A[l]的值(差分数组前缀和)
            int right = query(1, l + 1, r);             // 线段树查询差分数组[l+1, r]的GCD
            if (l == r)                                 // 区间长度为1
                cout << abs(left) << endl;              // 直接输出A[l]的绝对值
            else
                // GCD(A[l..r]) = GCD(A[l], GCD(b[l+1], b[l+2], ..., b[r]))
                // 其中 b[i] = A[i] - A[i-1] 为差分数组
                cout << abs(gcd(left, right)) << endl;
        }
        else                    // 修改指令
        {
            cin >> l >> r >> d;
            // 树状数组:区间加d,转化为差分数组的两个单点修改
            add(l, d);
            add(r + 1, -d);
            // 线段树:同步更新差分数组的两个端点
            update(1, l, d);
            if (r + 1 <= n)                         // 注意线段树建到了n
                update(1, r + 1, -d);
        }
    }
    return 0;
}

【运行结果】

5 5
1 3 5 7 9
Q 1 5
1
C 1 5 1
Q 1 5
2
C 3 3 6
Q 2 4
4
posted @ 2026-06-23 20:07  团爸讲算法  阅读(7)  评论(0)    收藏  举报