AcWing 246. 区间最大公约数


题目传送门

一级标题提醒:此题细节很多

1. \(gcd(a,b)=gcd(a,b?a)\)

\(lyd\)的书里提到的这个性质叫更相减损术,可以推广到多个数的情况。

更相减损术其实是欧几里得算法的一个特例。即\(gcd(a?nb,b)=gcd(a,b)\)
\((a,b,c)=((a,b),(b,c))=((a,b?a),(b,c?b))=(a,b?a,b,c?b)\)
由于\((b?a,b)=(a,b?a)\)所以\((a,b,c)=(a,b?a,c?b)\)
有了这个式子说明可以通过维护序列的差分来达到求\(gcd\)同样的效果
比如求\((a,b,c)\)只需要知道现在\(a\)的值,然后知道\((b?a,c?b)\)\(gcd\),再求一个公约数就行了。

  • 差分就可以把区间加减变成单点加减。可以用没有\(lazy\)的线段树来做。

  • 再维护一个差分,做成树状数组或者线段树,用来维护每个数的值。

2. \(gcd(a,b)=gcd(a,?b)\)
在数值加减的过程中可能会产生负数,而约定\(gcd\)是没有负数的,所以需要用这个式子来搞定负数。
具体来说,就是在每次查询或者更新的时候,如果遇到了负数,就把它取反。
注意:只能对结果取反而不能直接把线段树的负数叶子节点取反。因为直接把叶子取反会对今后的加减操作造成影响。

差分操作
差分操作是\(a[x]+d,a[y+1]-d;\)这个时候就可能出现\(y+1\)越界的情况。需要及时特判掉。

实现代码:

#include 

using namespace std;
typedef long long LL;
const int N = 500010;

int n, m;
LL w[N];

struct Node {
    int l, r;
    LL sum; //区间总和
    LL d;   //区间内的最大公约数
} tr[N << 2];

//求最大公约数
LL gcd(LL a, LL b) {
    return b ? gcd(b, a % b) : a;
}

//函数重载
void pushup(Node &u, Node &l, Node &r) {
    u.sum = l.sum + r.sum; //更新父节点的区间和
    u.d = gcd(l.d, r.d);   //计算区间的最大公约数
}
void pushup(int u) {
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

//构建
void build(int u, int l, int r) {
    if (l == r) {
        LL b = w[r] - w[r - 1]; //更相减损数,所以按原数组差分构建,yxc大佬很良心修改了试题,添加了1e18的数据范围说明
        tr[u] = {l, r, b, b};   //当是叶子节点时,区间和就是自己,区间最大公约数也是自己
        return;
    }
    tr[u].l = l, tr[u].r = r; //不加这句就和yxc一样的下场~
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
    //子节点变更需要更新父节点需要更新父节点的总和、最大公约数
    pushup(u);
}
//以u为根的子树中,修改位置为x的节点,值为+v
void modify(int u, int x, LL v) {
    if (tr[u].l == x && tr[u].r == x) { //找到叶子节点
        LL b = tr[u].sum + v;           //+v
        tr[u].sum = tr[u].d = b;        //修改区间和与最大公约数
        return;
    }
    int mid = tr[u].l + tr[u].r >> 1;
    if (x <= mid)                 //在左侧
        modify(u << 1, x, v);     //让左儿子处理
    else                          //在右侧
        modify(u << 1 | 1, x, v); //让右儿子处理
    // u的子节点数据变更,需要从u开始向上更新父节点信息
    pushup(u);
}
//查询
Node query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    int mid = tr[u].l + tr[u].r >> 1;
    if (r <= mid) return query(u << 1, l, r);
    if (l > mid) return query(u << 1 | 1, l, r);
    Node left = query(u << 1, l, r);
    Node right = query(u << 1 | 1, l, r);
    Node res;
    pushup(res, left, right);
    return res;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> w[i];
    //因为差分的r+1可能越界,这里在建立线段树时就多创建一个位置就OK
    build(1, 1, n + 1);

    int l, r;
    LL d;
    char op;
    while (m--) {
        cin >> op >> l >> r;
        if (op == 'Q') {
            auto left = query(1, 1, l);
            Node right({0, 0, 0, 0});
            if (l + 1 <= r) right = query(1, l + 1, r);
            printf("%lld\n", abs(gcd(left.sum, right.d)));
        } else {
            cin >> d;
            modify(1, l, d), modify(1, r + 1, -d);
        }
    }
    return 0;
}