\(AcWing\) \(246\). 区间最大公约数
一、题目描述
给定一个长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:
C l r d
,表示把 \(A[l],A[l+1],…,A[r]\) 都加上 \(d\)Q l r
,表示询问 \(A[l],A[l+1],…,A[r]\) 的最大公约数(\(GCD\))。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 \(N,M\)。
第二行 \(N\) 个整数 A[i] 。
接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
二、解题思路
1、差分数组
因为涉及到 区间修改 的问题,直观上需要 \(pushdown\) 操作,即 用父节点信息来更新子节点信息,但是 \(pushdown\) 操作很复杂并且容易写错(懒标记啥的,费劲~),所以 使用差分数组的技巧, 转区间修改为单点修改:
设原数组为\(\large a[i]\),对应的差分数组 \(\large b[i]\):\(\large b_i=a_i−a_{i−1}\),(这里认为\(a[0]=0\))
那么线段树维护这个\(\large b\)数组就可得到 单点修改从而改变整个区间 的效果。
2、更相减损术
那么根据 更相减损术
则有:\(\large gcd(a,b)=gcd(a,b−a)\)
动动脑筋推广一下得:【动完脑筋还是没想明白,那就直接背结论吧~】
我们想要求的就是:\(\large (A_l,A_{l+1},A_{l+2}…A_r)\)这个区间的 最大公约数
根据上面的 更相减损数理论,就是在求下面区间的 最大公约数:
稍微转化一下,得到:
这个东西要一分两半来看:
-
① \(A[l]\):因为我们维护的是一个差分数组的线段树,所以可以转化为差分的写法:
\[\large A[l]=sum(b[1],b[2],...,b[l]) \] -
② 后面的那一坨
\[\large (b[l+1],b[l+2],b[l+3],…,b[r]) \]就是区间\(l+1 \sim r\)的最大\(gcd\)值,这个东西在线段树的节点上以结构体形式保存着呢,可以直接
Node right=query(1,l+1,r)
查询出来,\(right.d\)就是最大公约数 -
③ 最后两者打一下擂台:
求\(\large (A_l,A_{l+1},A_{l+2}…A_r)\)这个区间的最大公约数,就是
res=abs(gcd(left.sum,right.d))
3、负数的最大公约数
注意\(gcd\)操作是没有负数的,所以需要进行取反。
三、实现代码
#include <bits/stdc++.h>
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, 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 == tr[u].r) { //找到叶子节点
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() {
//加快读入
ios::sync_with_stdio(false), cin.tie(0);
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') {
Node left = query(1, 1, l);
Node right({0, 0, 0, 0});
if (l < r) right = query(1, l + 1, r); //如果区间内只有一个点l=r,则直接返回a[l]即可,即left.sum, right为空
printf("%lld\n", abs(gcd(left.sum, right.d)));
} else {
cin >> d;
modify(1, l, d), modify(1, r + 1, -d);
}
}
return 0;
}