246 区间最大公约数(线段树)

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;
    }
}
我们可以使用线段树来解决这个问题。对于每个区间,我们都可以预处理出其内部所有数的最大公约数,然后在询问时,查询覆盖该询问区间的所有区间最大公约数并取最大值即可。 具体地,我们可以将二维区间 $(i,j)$ 分别看作 $i$ 和 $j$ 两个维度,建立一颗二维线段树。对于每个节点 $(x,y)$,它表示区间为 $[l_x,r_x]\times[l_y,r_y]$,其中 $l_x,r_x,l_y,r_y$ 分别表示该节点在 $x$ 和 $y$ 维度上的左右边界。我们可以在每个节点上维护一个值 $g_{x,y}$,表示区间 $[l_x,r_x]\times[l_y,r_y]$ 内部所有数的最大公约数。 对于每个节点 $(x,y)$,我们可以通过递归地计算其左右儿子节点的 $g$ 值来求出该节点的 $g$ 值。具体地,我们可以将节点 $(x,y)$ 表示区间分成四个子区间,分别为 $[l_x,\lfloor\frac{l_x+r_x}{2}\rfloor]\times[l_y,\lfloor\frac{l_y+r_y}{2}\rfloor]$、$[\lfloor\frac{l_x+r_x}{2}\rfloor+1,r_x]\times[l_y,\lfloor\frac{l_y+r_y}{2}\rfloor]$、$[l_x,\lfloor\frac{l_x+r_x}{2}\rfloor]\times[\lfloor\frac{l_y+r_y}{2}\rfloor+1,r_y]$ 和 $[\lfloor\frac{l_x+r_x}{2}\rfloor+1,r_x]\times[\lfloor\frac{l_y+r_y}{2}\rfloor+1,r_y]$。然后我们可以递归地计算出这四个子区间的 $g$ 值,然后将它们合并起来得到该节点的 $g$ 值。合并方法为取四个子区间的 $g$ 值的最大公约数。 查询时,我们从根节点开始,递归地查找覆盖询问区间的节点,并将这些节点的 $g$ 值取最大值。具体地,对于当前节点 $(x,y)$,如果它表示区间与询问区间不相交,则直接返回 1。否则,如果它表示区间完全包含询问区间,则返回该节点的 $g$ 值。否则,我们将询问区间分成四个子区间,并递归地查询每个子区间,然后将它们的 $g$ 值取最大公约数作为当前节点的 $g$ 值返回。 时间复杂度为 $O((n+m)\log^2(n+m))$,其中 $n$ 和 $m$ 分别为二维区间的行数和列数。空间复杂度为 $O((n+m)\log^2(n+m))$。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值