P14359 [CSP-J 2025] 异或和

P14359 CSP-J 2025 异或和

来源:洛谷题库

文章目录

题目

数据范围

知识点

贪心、前缀和

题意

给定长度为 n 的非负整数序列,选出尽可能多的‌不相交‌区间,使得每个区间的异或和等于 k。

思路

题目倒是容易理解,求出异或和为k的区间,选出尽可能多的不相交区间。

问题1:求出异或和为k的区间

问题2:选出尽可能多的不相交区间。

  • 解决问题1:要找出所有异或和等于 k 的区间,可以直接暴力枚举:
    用三层循环,外层循环固定区间的右端点 r,中层循环固定区间的左端点 l,内层循环把从 l 到 r 的所有数字挨个做异或运算。并检查是否等于 k,将符合条件的区间存储在结构体中。这种方法需要三重循环------显然时间复杂度会过高 只能拿到60分左右
  • 解决问题2:已知可选的区间,要在区间1,n择尽可能多的不相交区间,经典的"活动安排"问题,贪心策略是:‌优先选择结束位置靠前的区间‌。
csharp 复制代码
ps:为什么按右端点从小到大选是最优的?
就像你去选兴趣课,优先选最早下课的课,这样后面能排下更多的课。选结束位置最早的区间,能给后面的区间留出最多的空间,自然就能选出最多的不重叠区间。

如果只是按照课程长度排序,课程较短的时间可能开始时间较晚,前面的空余时间无法选课造成时间浪费
如果按照开始时间排序,开始时间小但是课程长度长也会导致将结果不是最优

未优化的55分代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5*1e5+5;
// 定义区间结构体
struct node {
    int l, r;
}nodes[N];
int a[N];
bool cmp(node a,node b){
	return a.r< b.r; 
}
int main() {
    // 优化IO
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    // 【暴力枚举】找出所有异或和为 k 的区间
    // 这一步在 N=5e5 时会导致 O(N^2) 复杂度 只能拿部分分 
    int t=0,id=0;
    for (int r = 1; r <= n; ++r) {
        for (int l = 1; l <= r; ++l) {
        	 t=0;
        	 for (int i= l; i <= r; ++i) {
        	 	t ^=a[i];
	            if (t == k) {
	                nodes[id++]={l, r};
	            }
			}
        }
    }
    // 按右端点排序
    sort(nodes+1, nodes+n+1,cmp);
    int ans = 0;
    int last_end = 0; // 上一个选中区间的右端点
    // 贪心选择
    for (int i=0;i<id;i++) {
        // 如果当前区间的左端点 > 上一个选中区间的右端点,则不重叠
        if (nodes[i].l > last_end) {
            ans++;
            last_end = nodes[i].r;
        }
    }
    cout << ans << endl;
    return 0;
}

优化AC版

一、如何快速求异或和:

三重循环显然超时(一般测评机1s限制,时间复杂度限制在10^8左右),看到任意区间求和我们会想到前缀和,那这个题目能否用前缀和的思路,知道异或公式很好办,不知道的同学就只能考场上自行推一下,看看能不能模仿前缀和形成前缀异或

(自行推导/知识点)

异或运算 ⊕ \oplus ⊕ 有三个性质,它们和加法几乎一模一样:

性质 公式
交换律 a ⊕ b = b ⊕ a a \oplus b = b \oplus a a⊕b=b⊕a
结合律 ( a ⊕ b ) ⊕ c = a ⊕ ( b ⊕ c ) (a \oplus b) \oplus c = a \oplus (b \oplus c) (a⊕b)⊕c=a⊕(b⊕c)
推导 a ⊕ b = c a \oplus b = c a⊕b=c, a ⊕ c = b a\oplus c = b a⊕c=b , b ⊕ c = a b\oplus c = a b⊕c=a

我们定义:

s 0 = 0 , s i = a 1 ⊕ a 2 ⊕ ⋯ ⊕ a i s0 = 0, \qquad si = a_1 \oplus a_2 \oplus \cdots \oplus a_i s0=0,si=a1⊕a2⊕⋯⊕ai

也就是"从头一直异或到第 i i i 个"的结果。

现在把 s r sr sr 展开,它其实是"前半段 + 我们想要的区间":

$ s r = a 1 ⊕ ⋯ ⊕ a l − 1 ⏟ = s l − 1 ⊕ a l ⊕ ⋯ ⊕ a r ⏟ 区间 l , r 的异或和 sr = \underbrace{a_1 \oplus \cdots \oplus a_{l-1}}{=\ sl-1} \oplus \underbrace{a_l \oplus \cdots \oplus a_r}{区间\ l,\\ r\ 的异或和} sr== sl−1 a1⊕⋯⊕al−1⊕区间 l, r 的异或和 al⊕⋯⊕ar

即:

s r = s l − 1 ⊕ s l , r sr = sl-1 \oplus sl, r sr=sl−1⊕sl,r

现在根据推导:

s r ⊕ s l − 1 = s l , r sr \oplus sl-1 =sl, r sr⊕sl−1=sl,r

所以得到结论:

区间 l , r 的异或和 = s r ⊕ s l − 1 \boxed{区间l, r的异或和 = sr \oplus sl-1} 区间l,r的异或和=sr⊕sl−1

对比前缀和:异或版的"减法"就是异或自己

区间值公式 靠什么"抵消"多出来的部分
前缀和 s u m r − s u m l − 1 sumr - suml-1 sumr−suml−1 用减法把前半段减掉
前缀异或 s r ⊕ s l − 1 sr \oplus sl-1 sr⊕sl−1 用异或 把前半段消掉(因为 x ⊕ x = 0 x \oplus x = 0 x⊕x=0)
落到题目上:条件只有一种写法

题目要求区间异或和等于 k k k:

s r ⊕ s l − 1 = k sr \oplus sl-1 = k sr⊕sl−1=k

这是整道题的核心钥匙 :一个"区间异或和为 k k k"的问题,被翻译成了**"两个前缀值彼此相差(异或) k k k"**的问题。

用样例验证一下

序列 [2, 1, 0, 3], k = 2 k = 2 k=2,先算出所有前缀异或:

i i i 0 1 2 3 4
a i a_i ai --- 2 1 0 3
s i si si 0 2 3 3 0
  • 区间 1 , 1 1, 1 1,1: s 1 ⊕ s 0 = 2 ⊕ 0 = 2 = k s1 \oplus s0 = 2 \oplus 0 = 2 = k s1⊕s0=2⊕0=2=k ✅
  • 区间 2 , 4 2, 4 2,4: s 4 ⊕ s 1 = 0 ⊕ 2 = 2 = k s4 \oplus s1 = 0 \oplus 2 = 2 = k s4⊕s1=0⊕2=2=k ✅

二、如何快速求出权值为k的区间

根据公式能快速求出怕l,r异或和,但是需要双重循环遍历区间求其异或和,能不能如何快速找出l所在的位置?
即:遍历到r,l在哪能够是的sl,r=k .

答案:"映射数组"

根据推导,把 s l − 1 sl-1 sl−1 能够通过当前的sr单独解出来,而sl-1之前也求过

s l − 1 = s r ⊕ k \boxed{sl-1 = sr \oplus k} sl−1=sr⊕k

  1. 转化之后,题目变成了什么

s l − 1 = s r ⊕ k sl-1 = sr \oplus k sl−1=sr⊕k。我们换个视角 ------ 枚举右端点 r r r:

对每一个 r r r,我需要的那个"左端点前一格" j = l − 1 j = l-1 j=l−1,它的前缀值必须等于 s r ⊕ k sr \oplus k sr⊕k。

换句话说:在当前 r r r 之前,有没有哪个位置 j j j,它的 s j sj sj 刚好等于" s r ⊕ k sr \oplus k sr⊕k"?

于是问题从"找区间"变成了一个非常单纯的查询:

给我一个数 v = s r ⊕ k v = sr \oplus k v=sr⊕k,告诉我它以前出现过吗?出现在哪个下标?

  1. 朴素做法卡在哪里

最笨的办法:每读到一个 r r r,就回头把 s 0 ... r − 1 s0 \\dots r-1 s0...r−1 扫一遍找 v v v。这是 O ( n ) O(n) O(n) 的查询,外层还有 n n n 个 r r r,总复杂度 O ( n 2 ) O(n^2) O(n2) ------ n = 5 × 10 5 n = 5 \times 10^5 n=5×105 时照样超时。

换个思路,不能挨个遍历,只是想找到 s r ⊕ k sr \oplus k sr⊕k这个值是否在s0,r中出现过,因此想到映射数组

  1. 破局思想:让"值"当数组下标

我们要的极其简单:给一个数值,立刻知道它在之前的哪些位置出现过。

怎么才能"立刻"知道?------ 如果这个数值本身就是数组下标,那就一步到位了。

想到映射数组,应该先看看数组范围:

题目保证 a i < 2 20 a_i < 2^{20} ai<220,所以它们的异或和也一定落在 [ 0 , 2 20 ) [0,\ 2^{20}) [0, 220) 这个范围内 ------ 值域有限,而且不大(约 10 6 10^6 106 个)。

值域有限 ⇒ 我们可以直接开一个长度 2 20 2^{20} 220 的数组,用"前缀异或值"直接当它的下标,数组里存这个值出现的位置:

p o s v = 前缀异或值为 v 的位置 posv = 前缀异或值为\ v\ 的位置 posv=前缀异或值为 v 的位置

这就是一个"值 → 位置"的映射表(也叫桶、直接寻址表)。原本"扫一遍找值"的 O ( n ) O(n) O(n) 查询,变成了"查 p o s v posv posv"的一次数组访问 O ( 1 ) O(1) O(1)。

  1. 映射表怎么用(跟着代码走一遍)

初始化:pos 全部置 − 1 -1 −1(表示没出现过),但有个容易被忽略的起点 ------

p o s 0 = 0 pos0 = 0 pos0=0

因为 s 0 = 0 s0 = 0 s0=0,它确实"在下标 0 处出现过"。忘了这一步,所有以第一个元素开头的区间都会漏掉。 s i = s i ⊕ 0 si = si \oplus 0 si=si⊕0

然后从左到右扫,每步做三件事:

  1. 更新前缀异或: s i = s i − 1 ⊕ a i si = si-1 \oplus a_i si=si−1⊕ai;
  2. 算目标值 target = s[i] ^ k,查 pos[target];
  3. 无论结果如何,把 pos[s[i]] = i(登记这一次的出现位置)。

用样例 [2, 1, 0, 3]、 k = 2 k = 2 k=2 走一遍:

i i i a i a_i ai s i si si target = s i ⊕ k = si \oplus k =si⊕k pos[target] 判断 结果 登记后
1 2 2 0 0 0 ≥ l a s t _ e n d ( 0 ) 0 \ge last\_end(0) 0≥last_end(0) ✅ 选中 1 , 1 1,1 1,1,ans=1, l a s t _ e n d = 1 last\_end=1 last_end=1 pos[2]=1
2 1 3 1 −1 不存在 ❌ 跳过 pos[3]=2
3 0 3 1 −1 不存在 ❌ 跳过 pos[3]=3
4 3 0 2 1 1 ≥ l a s t _ e n d ( 1 ) 1 \ge last\_end(1) 1≥last_end(1) ✅ 选中 2 , 4 2,4 2,4,ans=2, l a s t _ e n d = 4 last\_end=4 last_end=4 pos[0]=4

答案 2 2 2,和样例一致。

其中 j ≥ l a s t _ e n d j \ge last\_end j≥last_end 就是在保证不重叠:区间是 j + 1 , i j+1,\\ i j+1, i,只要 j ≥ l a s t _ e n d j \ge last\_end j≥last_end,左端点 j + 1 j+1 j+1 就严格大于上一个区间的右端点。

  1. 为什么只存"最后一次"出现的位置就够了

这是本题最妙的一环。我们要的是 j ≥ l a s t _ e n d j \ge last\_end j≥last_end。 p o s v posv posv 存的是最大的 那个 j j j:

  • 若最大 的 j j j 都 ≥ l a s t _ e n d \ge last\_end ≥last_end,那它就是一个合法选择 ✅;
  • 若最大 的 j j j 都 < l a s t _ e n d < last\_end <last_end,那比它更早的 j j j 只会更小,全部 也会 < l a s t _ e n d < last\_end <last_end,区间有重合,一个都用不上 ❌。

所以"最新的位置"既是最有可能合格的那个,也是最省事的那个 ------ 一个值只需要记一个位置,映射表因此简单又正确。

(每次都覆盖成最新位置 pos[s[i]] = i,就是在做这件事。)

  1. 复杂度
  • 时间 :每个 i i i 只做 O ( 1 ) O(1) O(1) 次的异或、查询、赋值 → O ( n ) O(n) O(n);
  • 空间 :pos 数组 2 20 2^{20} 220 个 int,约 4 MB;

n = 5 × 10 5 n = 5 \times 10^5 n=5×105 轻松通过。

优化后的AC代码

csharp 复制代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2 << 20;  // 异或值范围 < 2^20
int pos[MAXN];             // pos[v]: 前缀异或值v出现的位置(如果有多个则为出现的最后一个)
int n, k;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    memset(pos, -1, sizeof(pos));
    pos[0] = 0;  // s[0] = 0,出现在下标0处
    cin >> n >> k;
    int cur_xor = 0;   // 当前前缀异或值
    int last_end = 0;  // 上一个选中区间的右端点
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        int a;
        cin >> a;
        cur_xor ^= a;
        int target = cur_xor ^ k;
        // 如果找到匹配的前缀,且新区间不与已选区间重叠
        if (pos[target] >= last_end) {
            ans++;
            last_end = i;  // 更新最后选中的右端点
        }
        pos[cur_xor] = i;  // 记录当前前缀异或值的最新位置
    }
    cout << ans << endl;
    return 0;
}
相关推荐
茉莉玫瑰花茶1 小时前
GO [ 数据格式 ]
服务器·网络·算法·golang
小poop1 小时前
高级排序算法与性能对比——归并、非比较排序及总结
数据结构·算法·排序算法
aqiu1111111 小时前
CodeTop按顺序刷题0003:LeetCode 206. 反转链表(迭代法图文详解与动态演示)
算法·leetcode·链表
果霸大叔1 小时前
AI网关第8篇:AI 代理与负载均衡--多模型实例如何调度?
人工智能·算法·自然语言处理·负载均衡·ai-native
不会就选b1 小时前
算法日常・每日刷题--<动态规划>29
数据结构·算法·leetcode
垆边人似月.2 小时前
最少的元素个数使和为 target
数据结构·c++·算法
炽橙超真云2 小时前
2D 工程图纸如何自动转成可编辑 3D 模型?拆解读图与建体两条技术路径(DXF/DWG/PDF 到 STEP)
算法·3d·工业智能·工业ai
夕除2 小时前
redis--dict rehash
算法·哈希算法
云上先途12 小时前
Agent 轨迹数据一般包含哪些内容,加工的核心目标是什么?
人工智能·算法·机器学习