📌  相关文章
📜  在给定数组上交替加减的范围查询(1)

📅  最后修改于: 2023-12-03 15:08:05.194000             🧑  作者: Mango

在给定数组上交替加减的范围查询

本篇介绍如何在给定的数组上进行交替加减的范围查询。常见的数组范围查询通常是使用前缀和或线段树。但是如果需要在数组上进行交替加减操作,我们就需要使用一个 trick 来使得前缀和和线段树适应这种情况。

基础

在介绍交替加减的范围查询之前,我们先简单介绍一下前缀和和线段树。

前缀和

前缀和是一种常见的数组计算方法。它用一个额外的数组 $sum$ 来记录原数组 $arr$ 中前 $i$ 个元素的和,即 $sum[i] = \sum_{j=0}^i arr[j]$。然后对于询问 $[l,r]$ 的区间和,我们可以使用 $sum[r] - sum[l-1]$ 得到。这种方法需要 $O(n)$ 的空间,但是只需要 $O(1)$ 的时间就可以回答一个询问。如果有多个询问,我们也可以预处理出所有询问的答案,然后一次性回答它们,复杂度为 $O(n+q)$,其中 $q$ 为询问个数。

int n, q;
int arr[MAXN];
int sum[MAXN];

cin >> n;
for (int i = 1; i <= n; i++) {
    cin >> arr[i];
    sum[i] = sum[i-1] + arr[i];
}

cin >> q;
while (q--) {
    int l, r;
    cin >> l >> r;
    cout << sum[r] - sum[l-1] << endl;
}
线段树

线段树是一种常见的数据结构,用于高效地支持一些区间操作。线段树是一棵二叉树,其中每个节点代表数组的一个子区间,并且每个节点上记录了对应区间的信息。线段树常用于维护区间最值、区间和、区间 gcd/lcm 等。

线段树的构建过程通常使用递归的方式:对于节点 $p$,先递归构建它的左子节点 $p_{\text{left}}$ 和右子节点 $p_{\text{right}}$,然后合并子节点的信息得到节点 $p$ 的信息。具体而言,对于区间 $[l,r]$,我们用节点 $p$ 来表示并维护这个区间,$p$ 的左儿子维护区间 $[l,(l+r)/2]$,右儿子维护区间 $[(l+r)/2+1,r]$,节点 $p$ 的信息可以由上述两个子区间的信息计算得到。

int n, q;
int arr[MAXN];
int sum[MAXN << 2];

void build(int p, int l, int r) {
    if (l == r) {
        sum[p] = arr[l];
    } else {
        int mid = (l + r) >> 1;
        build(p*2, l, mid);
        build(p*2+1, mid+1, r);
        sum[p] = sum[p*2] + sum[p*2+1];
    }
}

int query(int p, int l, int r, int ql, int qr) {
    if (l > qr || r < ql) {
        return 0;
    } else if (ql <= l && r <= qr) {
        return sum[p];
    } else {
        int mid = (l + r) >> 1;
        return query(p*2, l, mid, ql, qr) + query(p*2+1, mid+1, r, ql, qr);
    }
}

cin >> n;
for (int i = 1; i <= n; i++) {
    cin >> arr[i];
}
build(1, 1, n);

cin >> q;
while (q--) {
    int l, r;
    cin >> l >> r;
    cout << query(1, 1, n, l, r) << endl;
}
交替加减的范围查询

如果需要在数组上交替进行加减操作,我们需要对前缀和或线段树的实现进行修改。由于交替加减的次数是奇偶相间的,我们可以使用额外的一维数组 $op$ 来记录一个位置加减的次数。$op[i]$ 为偶数表示不需要将位置 $i$ 更新,为奇数表示需要取反位置 $i$ 的值。例如,当 $op[i]=1$ 时,原本加上去的 $a_i$ 实际上会变成 $-a_i$。

以前缀和为例,我们可以使用以下的修改方法:

int n, q;
int arr[MAXN];
int sum[MAXN];
int op[MAXN];

cin >> n;
for (int i = 1; i <= n; i++) {
    cin >> arr[i];
    sum[i] = sum[i-1] + (op[i] % 2 == 0 ? arr[i] : -arr[i]);
}

cin >> q;
while (q--) {
    int l, r;
    cin >> l >> r;
    int ans = sum[r] - sum[l-1] + (op[l-1] % 2 == 1 ? arr[l-1] : -arr[l-1]);
    cout << ans << endl;
}

对于线段树,我们需要在每个节点上记录当前节点表示区间内的加减次数 $op$。当我们使用递归方式构建线段树时,递归函数会接受一个参数 $flag$,表示当前区间内的加减次数是偶数还是奇数。根据参数 $flag$,我们决定当前区间内的信息是否需要取反。

int n, q;
int arr[MAXN];
int sum[MAXN << 2];
int op[MAXN << 2];

void build(int p, int l, int r, int flag) {
    if (l == r) {
        sum[p] = (op[p] % 2 == 0 ? arr[l] : -arr[l]);
    } else {
        int mid = (l + r) >> 1;
        build(p*2, l, mid, flag + op[p]);
        build(p*2+1, mid+1, r, flag + op[p]);
        sum[p] = sum[p*2] + sum[p*2+1];
    }
}

void modify(int p, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr) {
        op[p]++;
    } else {
        int mid = (l + r) >> 1;
        if (ql <= mid) modify(p*2, l, mid, ql, qr);
        if (qr > mid) modify(p*2+1, mid+1, r, ql, qr);
        sum[p] = (op[p] % 2 == 0 ? sum[p*2] + sum[p*2+1] : -(sum[p*2] + sum[p*2+1]));
    }
}

int query(int p, int l, int r, int ql, int qr, int flag) {
    if (l > qr || r < ql) {
        return 0;
    } else if (ql <= l && r <= qr) {
        return (flag + op[p]) % 2 == 0 ? sum[p] : -sum[p];
    } else {
        int mid = (l + r) >> 1;
        int ans = query(p*2, l, mid, ql, qr, flag + op[p]);
        ans += query(p*2+1, mid+1, r, ql, qr, flag + op[p]);
        return (flag + op[p]) % 2 == 0 ? ans : -ans;
    }
}

cin >> n;
for (int i = 1; i <= n; i++) {
    cin >> arr[i];
}
build(1, 1, n, 0);

cin >> q;
while (q--) {
    int t, l, r;
    cin >> t >> l >> r;
    if (t == 0) {
        modify(1, 1, n, l, r);
    } else {
        int ans = query(1, 1, n, l, r, 0);
        cout << ans << endl;
    }
}
总结

本篇文章介绍了如何在给定的数组上进行交替加减的范围查询。我们通过使用前缀和或线段树,并在其中使用额外的数组 $op$ 来记录每个位置的加减次数,使得这两种算法都能适应交替加减的情况。虽然这种技巧在实现上稍有复杂,但是对于需要交替加减操作的问题,这种方法仍然是解决问题的有效手段。