1. 问题描述:
给定一个长度为 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 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
N ≤ 500000,M ≤ 100000,
1 ≤ A[i] ≤ 10 ^ 18,
|d| ≤ 10 ^ 18
输入样例:
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
2. 思路分析:
分析题目可以知道这是一道关于区间动态修改与区间查询的题目,所以我们需要使用线段树来解决(线段树适用于区间动态修改与查询的相关问题)。首先第一个操作是整个区间都加上一个数字,因为是区间修改所以我们很容易想到使用带有懒标记的线段树来求解,但是带有懒标记的线段树写起来会比较复杂(多了一个pushdpown操作),我们需要遵循的一个原则是能够使用不带有懒标记的线段树求解那么尽量使用不带有懒标记的线段树求解,对于区间修改的问题我们一般是考虑带有懒标记的线段树进行求解,但是这道题目比较特殊,我们可以看到第二个操作是查询一个区间的最大公约数,其实这里涉及到求解一个区间的最大公约数的式子,有了这个式子之后我们就可以使用不带有懒标记的线段树进行求解(下面式子中的小括号表示求解这些这些数字的最大公约数):
其实这个式子很好证明,证明等号的方法一般证明两个方向即可,一个是大于等于,另外一个是小于等于,我们在做题的时候记住这个结论即可。一个区间加上一个数字我们可以使用差分的思想,区间[l,r]加上一个数v对应的操作是在l这个位置加上v,并且在r + 1的位置减去v即可,这样利用差分的思想和求解最大公约数的式子就可以转换为不带有懒标记的线段树进行求解。然后我们需要考虑线段树节点需要存储哪些信息,可以发现区间的左右端点以及区间的最大公约数是需要存储的,并且线段树中维护的是差分的信息,并且需要维护两个信息,第一个需要维护前缀和,当长度为1的区间线段树节点维护序列中相邻两个数字的差,第二个需要维护区间差分的最大公约数,当长度为1的时候最大公约数也为相邻两个数字的差,这样我们通过维护差分的信息就可以使用下面的式子求解出区间[l,r]的最大公约数:
这个式子其实是对应最上面求解最大公约数的式子,线段树节点维护的前缀和信息用来求解一个区间相邻两个数字的差的和,当我们求解区间[1,l]的前缀和的时候计算结果就为al,也即a1 + a2 - a1 + a3 - a2 + ...al - al-1 = al,并且a2 - a1,a3 - a2,a4 - a3...的最大公约数使用线段树节点也很好维护(通过pushup操作很容易更新),这样我们就可以通过上面的式子计算出区间[l,r]的最大公约数。因为使用到了差分的思想,所以我们在修改的时候为单点修改,区间[l,r]加上v的时候我们可以将位置为l数字加上v,位置为r + 1的数字减去v即可,结合线段树动态维护前缀和和差分信息对应的最大公约数即可。
3. 代码如下:
c++:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 500010;
int n, m;
LL w[N];
struct Node
{
int l, r;
LL sum, d;
}tr[N * 4];
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];
tr[u] = {l, r, b, b};
}
else
{
tr[u].l = l, tr[u].r = r;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void modify(int u, int x, LL v)
{
if (tr[u].l == x && tr[u].r == x)
{
LL b = tr[u].sum + v;
tr[u] = {x, x, b, b};
}
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
}
Node query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) return tr[u];
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (r <= mid) return query(u << 1, l, r);
else if (l > mid) return query(u << 1 | 1, l, r);
else
{
auto left = query(u << 1, l, r);
auto right = query(u << 1 | 1, l, r);
Node res;
pushup(res, left, right);
return res;
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%lld", &w[i]);
build(1, 1, n);
int l, r;
LL d;
char op[2];
while (m -- )
{
scanf("%s%d%d", 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
{
scanf("%lld", &d);
modify(1, l, d);
if (r + 1 <= n) modify(1, r + 1, -d);
}
}
return 0;
}
java:(发现提交上去出现运行时错误,但是在本地的编译器是可以通过的)
import java.util.Scanner;
public class Main {
static int []w;
static Tree []tree;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
w = new int[n + 1];
tree = new Tree[n * 4];
for (int i = 0; i < 4 * n; ++i){
tree[i] = new Tree();
}
for (int i = 1; i <= n; ++i){
w[i] = sc.nextInt();
}
// nextline方法接收输入数字之后的回车符
sc.nextLine();
build(1, 1, n);
// s为接收输入的控制字符是Q还是C
for (int i = 0; i < m; ++i){
String s = sc.next();
if (s.equals("Q")){
int a = sc.nextInt();
int b = sc.nextInt();
// 接收回车符
sc.nextLine();
Tree left = query(1, 1, a);
Tree right = query(1, a + 1, b);
System.out.println(Math.abs(gcd(left.sum, right.d)));
}else {
// 更新操作, 差分思想所以只需要更新l与r + 1位置的值即可, l的位置加上v, r + 1的位置减去v即可
// 这样在更新的时候维护的差分信息也会更新
int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();
// 接收回车符
sc.nextLine();
modify(1, a, c);
if (b + 1 <= n) modify(1, b + 1, -c);
}
}
}
public static void build(int u, int l, int r){
tree[u].l = l;
tree[u].r = r;
if (l == r){
// 叶子节点维护相邻两个数字的差的差分信息
int b = w[r] - w[r - 1];
tree[u].sum = tree[u].d = b;
return;
}
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
// 创建线段树节点的时候需要将子节点的信息传递到父节点信息
pushup(u);
}
// pushup操作, 将子节点的信息传递到父节点信息上
private static void pushup(int u) {
pushup(tree[u], tree[u << 1], tree[u << 1 | 1]);
}
// pushup方法将子节点的信息更新到父节点, 需要更新区间的前缀和和最大公约数
private static void pushup(Tree u, Tree l, Tree r) {
u.sum = l.sum + r.sum;
u.d = gcd(l.d, r.d);
}
// 递归求解最大公约数
private static int gcd(int a, int b) {
return b != 0 ? gcd(b, a % b) : a;
}
// 将x位置的值加上v
public static void modify(int u, int x, int v){
// 找到了更新的位置
if (tree[u].l == x && tree[u].r == x){
// 只有一个数字的时候前缀和和最大公约数都为b
int b = tree[u].sum + v;
tree[u].sum = b;
tree[u].d = b;
return;
}
int mid = tree[u].l + tree[u].r >> 1;
if (x <= mid) modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
// 查询操作, 方法的返回值为Tree类型这样可以通过节点信息来进行更新
public static Tree query(int u, int l, int r){
// 当前节点的区间包含于查询的区间直接返回
if (tree[u].l >= l && tree[u].r <= r) return tree[u];
int mid = tree[u].l + tree[u].r >> 1;
// 查询的区间在当前节点对应区间的左子区间
if (r <= mid) return query(u << 1, l, r);
// 查询的区间在当前节点对应区间的右子区间
else if (l > mid) return query(u << 1 | 1, l, r);
# 横跨mid对应的区间
else {
Tree res = new Tree();
Tree left = query(u << 1, l, r);
Tree right = query(u << 1 | 1, l, r);
pushup(res, left, right);
return res;
}
}
public static class Tree{
private int l, r;
// sum为差分的前缀和([1:l]的sum表示a[l]), d当前区间差分信息的最大公约数, a1 - 0, a2 - a1, a3 - a2...的最大公约数
private int sum, d;
}
}