题目链接

区间取反与单点求值

题目描述

给定一个长度为 的 01 字符串,我们需要实现一个数据结构来支持以下两种操作:

  1. 区间取反:将区间 内的所有字符进行取反操作(0 变为 11 变为 0)。
  2. 单点查询:查询下标为 的字符的值。

输入:

  • 第一行包含两个整数 ,分别表示 01 串的长度和操作的次数。
  • 第二行是一个长度为 的 01 字符串。
  • 接下来 行,每行描述一个操作。格式为 1 l r2 x

输出:

  • 对于每个查询操作,输出一行,表示对应下标的字符值。

解题思路

本题要求我们维护一个可以进行区间修改和单点查询的数据结构。对于这类问题,一个非常有效的方法是使用树状数组结合差分思想。

核心思路如下: 一个数被取反奇数次,结果等于取反一次;被取反偶数次,结果等于没有取反。这个性质正好对应了异或(XOR)运算的性质:。因此,我们可以将取反操作看作是与 1 进行异或。

  1. 差分数组

    我们定义一个差分数组 ,用它来记录每个点相比前一个点变化的次数。当我们对区间 进行取反时,只有两个点的状态会发生变化:

    • 点开始被取反,它与 点的关系改变了。
    • 点不再被取反,它与 点的关系也改变了。

    所以,一次区间 的取反操作,等价于在差分数组上进行两次单点修改:

  2. 树状数组维护差分前缀和

    一个点 的最终值,取决于它的初始值以及它被取反的总次数。而点 被取反的总次数,恰好是差分数组 的前缀和 (这里的求和是异或意义下的)。

    为了快速求出差分数组的前缀异或和,我们可以使用树状数组来维护。

综上,整个流程如下:

  • 初始化:读入原始 01 串。创建一个大小为 的树状数组,所有元素初始化为
  • 区间取反 l r:在树状数组上执行两次单点更新:add(l, 1)add(r + 1, 1)
  • 单点查询 x:查询树状数组中 的前缀异或和 query(x),这个结果就是 被取反的总次数。用原始值 a[x] 与这个次数进行异或,即得到最终答案。

代码

#include <iostream>
#include <vector>
#include <string>

using namespace std;

int n;
vector<int> tr;
vector<int> a;

int lowbit(int x) {
    return x & -x;
}

void add(int x, int k) {
    for (int i = x; i <= n; i += lowbit(i)) {
        tr[i] ^= k;
    }
}

int query(int x) {
    int res = 0;
    for (int i = x; i > 0; i -= lowbit(i)) {
        res ^= tr[i];
    }
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(NULL);

    int m;
    cin >> n >> m;
    
    string s;
    cin >> s;
    
    a.resize(n + 1);
    tr.resize(n + 1, 0);
    
    for (int i = 0; i < n; ++i) {
        a[i + 1] = s[i] - '0';
    }
    
    while (m--) {
        int op;
        cin >> op;
        if (op == 1) {
            int l, r;
            cin >> l >> r;
            add(l, 1);
            if (r + 1 <= n) {
                add(r + 1, 1);
            }
        } else {
            int x;
            cin >> x;
            cout << (a[x] ^ query(x)) << "\n";
        }
    }
    
    return 0;
}
import java.util.Scanner;

public class Main {
    static int n;
    static int[] tr;
    static int[] a;

    static int lowbit(int x) {
        return x & -x;
    }

    static void add(int x, int k) {
        for (int i = x; i <= n; i += lowbit(i)) {
            tr[i] ^= k;
        }
    }

    static int query(int x) {
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i)) {
            res ^= tr[i];
        }
        return res;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        int m = sc.nextInt();
        String s = sc.next();

        a = new int[n + 1];
        tr = new int[n + 1];

        for (int i = 0; i < n; i++) {
            a[i + 1] = s.charAt(i) - '0';
        }

        while (m-- > 0) {
            int op = sc.nextInt();
            if (op == 1) {
                int l = sc.nextInt();
                int r = sc.nextInt();
                add(l, 1);
                if (r + 1 <= n) {
                    add(r + 1, 1);
                }
            } else {
                int x = sc.nextInt();
                System.out.println(a[x] ^ query(x));
            }
        }
    }
}
import sys

def lowbit(x):
    return x & -x

def add(x, k):
    while x <= n:
        tr[x] ^= k
        x += lowbit(x)

def query(x):
    res = 0
    while x > 0:
        res ^= tr[x]
        x -= lowbit(x)
    return res

# 读取输入
lines = sys.stdin.readlines()
n, m = map(int, lines[0].split())
s = lines[1].strip()
a = [0] * (n + 1)
tr = [0] * (n + 1)

for i in range(n):
    a[i + 1] = int(s[i])

for i in range(m):
    parts = list(map(int, lines[i + 2].split()))
    op = parts[0]
    
    if op == 1:
        l, r = parts[1], parts[2]
        add(l, 1)
        if r + 1 <= n:
            add(r + 1, 1)
    else:
        x = parts[1]
        result = a[x] ^ query(x)
        print(result)

算法及复杂度

  • 算法:树状数组 + 差分
  • 时间复杂度:共有 次操作,每次操作(修改或查询)在树状数组上的时间复杂度均为 。因此,总时间复杂度为 ,其中 为初始化字符串的开销。
  • 空间复杂度:需要一个数组存储原始 01 串,以及一个树状数组,所以空间复杂度为