利用异或消去所有成对长度,并用 fread 流式读入满足千万数据和 8 MB 内存限制。
OJ: luogu
题目 ID: P1469
难度:普及-
标签:位运算异或输入优化
日期: 2026-07-16 19:20
题意
给出奇数根筷子的长度。除了一个长度出现奇数次,其余长度都出现偶数次,求落单筷子的长度。
数据范围的两个重点是:8 MB。程序既不能保存全部筷子,也要控制输入开销。
思路
朴素统计为什么不合适
最直接的方法是用映射记录每个长度出现次数的奇偶性:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:59
* update_at: 2026-07-19 11:59
*/
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据朴素解,统计每个长度出现次数的奇偶性。
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
map<int, int> parity;
for (int i = 1; i <= n; i++) {
int length;
cin >> length;
parity[length] ^= 1;
}
for (map<int, int>::iterator it = parity.begin(); it != parity.end(); it++) {
if (it->second == 1) {
cout << it->first << '\n';
break;
}
}
return 0;
}这个做法适合小数据验证,但 map 的每个节点都要保存键、值、指针和管理信息。长度种类很多时,它无法满足 8 MB 空间限制。
用异或让所有成对长度消失
异或满足:
异或还满足交换律和结合律,所以输入顺序不影响结果。把所有长度异或起来,每个出现偶数次的长度都会两两抵消,最终只剩出现奇数次的落单长度。
例如样例中的异或结果可以重新分组为:
整个算法只需要一个答案变量,不需要知道某个长度此前出现过多少次。
为什么还要手写快速读入
本题要读取一千多万个整数。即使算法是
C++ 正解使用 fread 每次读入一个固定大小的字节块,再在块内解析十进制整数。解析器只保存:
- 一个
64 KB输入缓冲区; - 当前缓冲区位置和有效长度;
- 当前整数与异或答案。
因此总内存不随 main.py 虽然也尝试分块读取,但仍要在 Python 层逐字节循环和反复 yield 一千多万个整数;同时 Python 运行时本身也不适合 8 MB 的严格内存限制,所以改用 C++ 流式解析。
正确性说明
设落单长度为
长度
快速读入只改变整数从输入字节中被取出的方式,不改变整数顺序和数值;主循环恰好读取并异或
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-19 11:59
* update_at: 2026-07-19 11:59
*/
#include <cstdio>
const int BUFFER_SIZE = 1 << 16;
char input_buffer[BUFFER_SIZE];
int buffer_position;
int buffer_length;
bool read_integer(int &value) {
char current;
do {
if (buffer_position == buffer_length) {
buffer_length = fread(input_buffer, 1, BUFFER_SIZE, stdin);
buffer_position = 0;
if (buffer_length == 0) return false;
}
current = input_buffer[buffer_position++];
} while (current < '0' || current > '9');
value = 0;
while (current >= '0' && current <= '9') {
value = value * 10 + current - '0';
if (buffer_position == buffer_length) {
buffer_length = fread(input_buffer, 1, BUFFER_SIZE, stdin);
buffer_position = 0;
if (buffer_length == 0) return true;
}
current = input_buffer[buffer_position++];
}
return true;
}
int main() {
int n;
read_integer(n);
int answer = 0;
for (int i = 1; i <= n; i++) {
int length;
read_integer(length);
answer ^= length;
}
printf("%d\n", answer);
return 0;
}复杂度
- 每根筷子只参与一次异或,时间复杂度为
;从输入字节角度看,每个字节也只扫描一次。 - 除固定的
64 KB输入缓冲区外,只使用常数个整数,额外空间复杂度为。 brute.cpp使用映射,时间复杂度为,空间复杂度为 ,仅用于小数据验证。
本机用 0.05s 完成,采样峰值 RSS 为 3712 KB;具体时间随机器变化,但内存规模稳定低于题目限制。
总结
本题的算法核心是“偶数次出现可以用异或消去”,工程核心则是“不能保存数据,也不能让输入解析成为瓶颈”。一个异或变量配合固定大小的 fread 缓冲区,能够同时满足千万数据和 8 MB 空间限制。