题解:洛谷 AT_arc098_b Xor Sum 2

【题目来源】

洛谷:AT_arc098_b [ABC098D] Xor Sum 2 - 洛谷

【题目描述】

给你一串数 \(a\)

求出满足\(a_l+\cdots +a_r=a_l\operatorname{xor}\cdots\operatorname{xor}a_r,l\le r\)\((i,j)\) 的数量

\(1\le n\le 200000,\forall 1\le i\le n,0\le a_i<2^{20}(1048576)\)

【输入样例】

4
2 5 4 6

【输出样例】

5

【核心思想】

  1. 问题分析:给定一个长度为 \(n\) 的数组 \(a\),统计所有满足 \(\sum_{i=l}^{r} a_i = \bigoplus_{i=l}^{r} a_i\) 的连续子区间 \([l, r]\) 的个数。其中 \(\oplus\) 表示按位异或。由于加法存在进位,等式成立当且仅当区间内所有数的二进制表示中不存在重叠的 1 位,即区间内任意两个数的按位与为 0。等价于区间内所有数的按位或等于它们的按位异或(因为无进位)。利用这个性质,可以用双指针维护一个“合法”区间,统计所有子区间。

  2. 算法选择

    • 双指针 + 位计数:维护窗口 \([i, j]\),用 cnt[20] 记录每个二进制位上 1 出现的次数,cur_or 记录当前窗口所有数的按位或。扩展右指针时,检查新数 \(a[j+1]\) 与当前 cur_or 是否有公共位(cur_or & a[j+1] == 0),若满足则加入窗口,并更新 cntcur_or;否则无法扩展,此时以 \(i\) 为左端点的所有合法子区间个数为 \(j - i + 1\)(因为窗口内任意后缀都合法)。然后移动左指针,从窗口中移除 \(a[i]\),更新计数,继续寻找。
    • 复杂度:每个元素入队出队一次,每次更新 \(O(20)\),总时间复杂度 \(O(20n)\),空间 \(O(20)\)
  3. 关键步骤

    • 读入 \(n\) 和数组 \(a[1..n]\)
    • 初始化左指针 \(i=1\),右指针 \(j=0\)cur_or = 0,计数数组 cnt[20] = {0},答案 ans = 0(用 long long)。
    • 循环 \(i\) 从 1 到 \(n\)
      • 尝试扩展右指针:当 \(j+1 \le n\)(cur_or & a[j+1]) == 0 时,将 \(a[j+1]\) 加入窗口:更新 cnt 中各二进制位计数,若某位计数从 0 变为 1 则 cur_or |= (1<<bit)j++
      • 此时以 \(i\) 为左端点的合法区间数为 \(j - i + 1\),累加到 ans
      • 准备移动左指针:从窗口中移除 \(a[i]\):遍历其二进制位,若某位为 1,则 cnt[bit]--,若减为 0 则 cur_or &= ~(1<<bit)i++
    • 输出 ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot \log A)\),其中 \(A \le 2^{20}\),常数很小。
    • 空间复杂度:\(O(1)\)(只使用了固定大小的计数数组)。
  5. 双指针 + 位运算的核心思想

    • 等式转化:利用 \(sum \ge xor\),相等条件等价于区间内所有数在二进制位上互不重叠,即任意两数按位与为 0。
    • 窗口合法性:窗口内所有数的按位或 cur_or 与待加入数无公共位时,加入后仍合法,保证了窗口内所有子区间均合法。
    • 计数方式:固定左指针,向右扩展右指针直到不合法,则所有以当前左指针为起点的合法区间数就是当前窗口长度,体现了双指针的单调性。
    • 高效维护:通过位计数数组支持 \(O(1)\) 级别的加入和移除操作,避免了重新计算按位或。

【算法标签】

普及 #双指针

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int MAX_N = 200005;  // 定义数组最大长度
int n;                     // 数组长度
int a[MAX_N];              // 存储原始数组
int s1, s2;                // s1: 累加和, s2: 异或和
int ans;                   // 存储最终结果(满足条件的子数组个数)

signed main()
{
    // 输入数组长度
    cin >> n;

    // 输入数组元素
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    // 初始化双指针和累加器
    int i = 1, j = 0;  // i: 左指针, j: 右指针
    s1 = 0, s2 = 0;    // 初始化累加和与异或和

    // 滑动窗口法统计满足条件的子数组
    while (i <= n)
    {
        // 扩展右边界直到不满足条件
        while (j + 1 <= n && s1 + a[j + 1] == (s2 ^ a[j + 1]))
        {
            j++;            // 右指针右移
            s1 += a[j];     // 更新累加和
            s2 ^= a[j];     // 更新异或和
        }

        // 统计以i为左端点的满足条件的子数组个数
        ans += j - i + 1;

        // 移动左指针
        s1 -= a[i];         // 从累加和中移除a[i]
        s2 ^= a[i];         // 从异或和中移除a[i]
        i++;                // 左指针右移
    }

    // 输出结果
    cout << ans << endl;

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型
const int N = 200005;  // 定义数组最大长度
int n;                 // 数组长度
int a[N];              // 存储原始数组
int s1, s2;            // s1: 累加和, s2: 异或和
int ans;               // 存储最终结果(满足条件的子数组个数)

signed main()
{
    // 输入数组长度
    cin >> n;

    // 输入数组元素
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    // 初始化双指针和累加器
    int i = 1, j = 0;  // i: 左指针, j: 右指针
    s1 = 0, s2 = 0;    // 初始化累加和与异或和

    // 滑动窗口法统计满足条件的子数组
    for (; i <= n; i++)
    {
        // 扩展右边界直到不满足条件
        while (j + 1 <= n && s1 + a[j + 1] == (s2 ^ a[j + 1]))
        {
            j++;            // 右指针右移
            s1 += a[j];     // 更新累加和
            s2 ^= a[j];     // 更新异或和
        }

        // 统计以i为左端点的满足条件的子数组个数
        ans += j - i + 1;

        // 移动左指针
        s1 -= a[i];         // 从累加和中移除a[i]
        s2 ^= a[i];         // 从异或和中移除a[i]
    }

    // 输出结果
    cout << ans << endl;

    return 0;
}

【运行结果】

4
2 5 4 6
5
posted @ 2026-09-02 22:51  团爸讲算法  阅读(7)  评论(0)    收藏  举报