题解:洛谷 P14359 异或和

【题目来源】

洛谷:P14359 [CSP-J 2025] 异或和 / xor(民间数据) - 洛谷

【题目描述】

小 R 有一个长度为 \(n\) 的非负整数序列 \(a_1, a_2, \dots, a_n\)。定义一个区间 \([l, r]\) (\(1 \leq l \leq r \leq n\))的权值为 \(a_l, a_{l+1}, \dots, a_r\) 的二进制按位异或和,即 \(a_l \oplus a_{l+1} \oplus \dots \oplus a_r\),其中 \(\oplus\) 表示二进制按位异或。

小 X 给了小 R 一个非负整数 \(k\)。小 X 希望小 R 选择序列中尽可能多的不相交的区间,使得每个区间的权值均为 \(k\)。两个区间 \([l_1, r_1], [l_2, r_2]\) 相交当且仅当两个区间同时包含至少一个相同的下标,即存在 \(1 \leq i \leq n\) 使得 \(l_1 \leq i \leq r_1\)\(l_2 \leq i \leq r_2\)

例如,对于序列 \([2, 1, 0, 3]\),若 \(k = 2\),则小 R 可以选择区间 \([1, 1]\) 和区间 \([2, 4]\),权值分别为 \(2\)\(1 \oplus 0 \oplus 3 = 2\);若 \(k = 3\),则小 R 可以选择区间 \([1, 2]\) 和区间 \([4, 4]\),权值分别为 \(1 \oplus 2 = 3\)\(3\)

你需要帮助小 R 求出他能选出的区间数量的最大值。

【输入】

输入的第一行包含两个非负整数 \(n, k\),分别表示小 R 的序列长度和小 X 给小 R 的非负整数。

输入的第二行包含 \(n\) 个非负整数 \(a_1, a_2, \dots, a_n\),表示小 R 的序列。

【输出】

输出一行一个非负整数,表示小 R 能选出的区间数量的最大值。

【输入样例】

4 2
2 1 0 3

【输出样例】

2

【核心思想】

  1. 问题分析:给定长度为 \(n\) 的非负整数序列 \(a_1, \dots, a_n\) 和目标值 \(k\),求最多能选出多少个互不相交的区间,使得每个区间的异或和等于 \(k\)。本质上是前缀异或和 + 贪心扫描问题:利用异或前缀和将区间异或转化为两点异或,贪心地在找到合法区间后立即分割,为后续区间创造空间。

  2. 算法选择

    • 前缀异或和\(sa[i] = a_1 \oplus a_2 \oplus \dots \oplus a_i\),则区间 \([l, r]\) 的异或和为 \(sa[r] \oplus sa[l-1]\)
    • 哈希表定位:用 map 记录每个前缀异或和最后一次出现的位置,快速查找满足 \(sa[r] \oplus sa[l-1] = k\)\(sa[l-1] = sa[r] \oplus k\) 的分割点
    • 贪心分割:一旦找到合法区间,立即计数并清空哈希表重新开始,保证区间互不相交且数量最大化
  3. 关键步骤

    • 读取数据:读入 \(n, k\) 和序列 \(a[1..n]\)
    • 计算前缀异或和\(sa[i] = sa[i-1] \oplus a[i]\)
    • 初始化mp[0] = 0(前缀和为 \(0\) 的位置在索引 \(0\)),last_r = 0ans = 0
    • 贪心扫描\(r = 1\)\(n\)):
      • \(t = sa[r] \oplus k\)(目标前缀异或和)
      • mp 中存在 \(t\)mp[t] >= last_r
        • 找到合法区间 \([mp[t]+1, r]\)ans++
        • last_r = r(更新最后一个区间的右端点)
        • mp.clear()mp[sa[r]] = r(清空后重新开始)
      • 否则:mp[sa[r]] = r(记录当前前缀和位置)
    • 输出 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \log n)\),每次 map 操作 \(O(\log n)\)
    • 空间复杂度:\(O(n)\),哈希表存储
  5. 贪心 + 前缀异或的核心思想

    • 异或前缀和转化\(a_l \oplus \dots \oplus a_r = sa[r] \oplus sa[l-1]\),将区间查询转化为两点查询
    • 贪心最优性:找到合法区间后立即分割,避免该区间与后续区间重叠,这是最大化数量的关键(区间越长越可能阻塞后续区间,但贪心取最早可行的分割点已足够)
    • 清空策略的必要性:清空哈希表保证后续区间严格在 last_r 之后开始,确保互不相交
    • \(mp[0] = 0\) 的初始化:处理从第 \(1\) 个元素开始的区间,\(sa[0] = 0\) 作为虚拟前缀
    • 适用于区间划分、异或性质、贪心最大化类问题

【算法标签】

普及 #贪心

【代码详解】

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

/*
 * 算法思路:
 * 贪心策略:如果当前区间满足要求,就直接单独成段,为后面有用的格子创建空间
 * 异或和优化:使用前缀异或和来避免重复计算
 * 核心原理:异或和的奇偶性由1的个数决定(奇数个1,异或和为1;偶数个1,异或和为0)
 * 查找技巧:利用异或的交换性,查找满足 sa[r] ^ sa[l-1] = k 的区间
 */

#define int long long  // 使用长整型防止溢出
const int N = 500005;  // 定义最大数组长度

int n, k;              // n:数组长度, k:目标异或值
int a[N];              // 原始数组
int sa[N];             // 前缀异或和数组,sa[i] = a[1]^a[2]^...^a[i]
int last_r;            // 记录上一个分段的右端点
int ans;               // 分段数量
map<int, int> mp;      // 哈希表,记录前缀异或和最后一次出现的位置

signed main()  // 使用signed代替int(因为定义了#define int long long)
{
    // 输入数组长度和目标异或值
    cin >> n >> k;

    // 计算前缀异或和
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        sa[i] = sa[i - 1] ^ a[i];  // 计算前缀异或和
    }

    // 初始化:前缀和为0的位置在索引0处
    mp[0] = 0;

    // 遍历数组,寻找最优分段
    for (int r = 1; r <= n; r++)
    {
        // 计算需要查找的前缀和:sa[r] ^ k = sa[l-1]
        int t = sa[r] ^ k;

        // 检查是否存在满足条件的分割点
        if (mp.count(t) && mp[t] >= last_r)
        {
            // 找到有效的分段点
            ans++;           // 分段数量加1
            last_r = r;      // 更新上一个分段的右端点

            // 清空哈希表,重新开始记录(贪心策略)
            mp.clear();
            // 记录当前前缀和的位置
            mp[sa[r]] = r;
        }
        else
        {
            // 未找到分段点,记录当前前缀和的位置
            mp[sa[r]] = r;
        }
    }

    // 输出最大分段数量
    cout << ans << endl;

    return 0;
}

【运行结果】

4 2
2 1 0 3
2

【算法真传】

从一道 CSP-J 真题出发:聊聊前缀异或和与贪心区间分割

posted @ 2026-08-17 09:45  团爸讲算法  阅读(25)  评论(0)    收藏  举报