[yLOI2019] 棠梨煎雪
每个串压成 0/1 两个位掩码,线段树按位或合并区间约束,统计兼容二进制串数量。
OJ: luogu
题目 ID: P5522
难度:提高
标签:线段树位运算状态压缩区间合并字符串
日期: 2026-07-16 23:59
形式化题目
有 0、1 或 ?,
- 询问区间
:求有多少个 0/1 串 ,满足区间内每个字符串都能把 ?替换成 0 或 1 后变成; - 把某一个字符串整体替换成新串。
最后把所有询问答案异或起来输出。
本质上:每个带 ? 的串是一个「位置约束的集合」——位置要么被固定为 0、要么被固定为 1、要么自由。询问就是求区间内这些约束集合的交集大小。
思路
先看一个直接按题面定义的朴素解:
/**
* 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-08-12 23:00
* update_at: 2026-08-12 22:12
*/
// brute.cpp:小数据暴力解,使用 01 序列递归枚举候选串 S,用来理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 105;
const int MAX_LEN = 35;
int n, m, q;
char str[MAXN][MAX_LEN]; // str[i]:第 i 年信的内容(0 / 1 / ?)
int choose[MAX_LEN]; // choose[j]:候选串 S 第 j 位的取值,0 或 1
// 检查当前完整候选串 choose[0..n-1] 是否兼容 [l, r] 内的所有信。
// 对每个位置:信里是 '0' 则 S 该位必须为 0,是 '1' 则必须为 1,是 '?' 则无限制。
bool check(int l, int r) {
for (int i = l; i <= r; i++) {
for (int j = 0; j < n; j++) {
if (str[i][j] != '?' && str[i][j] - '0' != choose[j])
return false;
}
}
return true;
}
// dfs(dep):这一层枚举候选串第 dep 位取 0 还是 1。
// 生成完整 n 位候选串后,在叶子节点统一检查并统计答案。
void dfs(int dep, int l, int r, int& cnt) {
if (dep == n) {
if (check(l, r))
cnt++;
return;
}
for (int b = 0; b <= 1; b++) {
choose[dep] = b;
dfs(dep + 1, l, r, cnt);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
for (int i = 1; i <= m; i++)
cin >> str[i];
int ans_xor = 0; // 所有查询答案的异或和
while (q--) {
int opt;
cin >> opt;
if (opt == 1) {
// 修改:直接覆盖第 pos 年的信
int pos;
cin >> pos >> str[pos];
} else {
// 查询:枚举全部 2^n 个候选串 S,统计与 [l, r] 内所有信兼容的个数
int l, r, cnt = 0;
cin >> l >> r;
dfs(0, l, r, cnt);
ans_xor ^= cnt;
}
}
cout << ans_xor << '\n';
return 0;
}这个暴力把每次询问看成一串 01 选择:choose[j] = 0/1 表示候选串
瓶颈在于逐候选串、逐信、逐位地检查。优化思路是把一封信的约束压进两个位掩码(每个位置一个 bit,
zero:出现0的位置集合;one:出现1的位置集合;?不贡献任何位。
编码动作本身很简单:约定字符串第 1 << i 把对应位置 1:
int z = 0, o = 0;
for (int i = 0; i < n; i++) {
if (t[i] == '0') z |= (1 << i); // 第 i 位固定为 0
else if (t[i] == '1') o |= (1 << i); // 第 i 位固定为 1
// '?' 不设置任何位
}以 010 为例:第 0、2 个字符是 0,把 bit 0 与 bit 2 置 1,得 1,得
关键观察是区间合并就是按位或:
答案公式:若
「按位或合并」满足结合律、单位元为 0(空集合),且操作形态是单点修改 + 区间查询——这正是线段树的标准形态。每个节点存 (zero, one) 两个掩码,push_up 时分别 OR;一次询问只访问
以样例的三封信为例(位置 1…3),掩码编码与合并过程如下:
第一张表展示每封信编码出的两个掩码:
| 串 | zero(固定为 0 的位置) | one(固定为 1 的位置) |
|---|---|---|
| 010 | {1, 3} | {2} |
| 0?0 | {1, 3} | ∅ |
| 1?0 | {3} | {1} |
| 0??(修改后的 s3) | {1} | ∅ |
第二张表展示 4 次询问的合并结果:
| 询问 | 合并 Z | 合并 O | Z∩O | 自由位 | 答案 |
|---|---|---|---|---|---|
| [1,2] | {1, 3} | {2} | ∅ | 0 | 1 |
| [2,3] | {1, 3} | {1} | {1} | — | 0 |
| 修改后 [2,3] | {1, 3} | ∅ | ∅ | 1 | 2 |
| 修改后 [1,3] | {1, 3} | {2} | ∅ | 0 | 1 |
观察要点:第二次询问中位置 1 被 s2 固定为 0、又被 s3 固定为 1,Z ∩ O ≠ ∅ 直接判 0;把 s3 改成 0?? 后冲突消除,位置 2 成为唯一的自由位,答案变成
下面这张图展示一棵规模为 8 的线段树,节点上的数字是它代表的区间:
任意询问区间都能拆成 [2,2]、[3,4]、[5,6]、[7,7] 四个整段节点,各花
代码
/**
* 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-08-12 23:00
* update_at: 2026-08-15 22:30
*/
// main.cpp:P5522 正式主解。线段树按位或合并区间的 0/1 约束,单点修改、区间查询。
#include <bits/stdc++.h>
using namespace std;
// 按位或合并区间约束的线段树(单点修改、区间查询)
struct SegmentTreeBitOr {
// 线段树节点:zero / one 为区间内所有串的约束掩码
struct Node {
int zero = 0; // 区间内被固定为 0 的位置集合(位掩码)
int one = 0; // 区间内被固定为 1 的位置集合(位掩码)
// 合并两个孩子:两个掩码分别按位或,合并结果与顺序无关
Node operator+(const Node &other) const {
return Node{zero | other.zero, one | other.one};
}
};
// 左儿子 / 右儿子的节点编号
static int lson(int p) { return p << 1; }
static int rson(int p) { return p << 1 | 1; }
// 区间 [l, r] 的中点
static int mid(int l, int r) { return (l + r) >> 1; }
int n = 0; // 区间大小
vector<Node> tree; // 线段树数组
SegmentTreeBitOr(int n = 0) {
init(n);
}
void init(int size) {
n = size;
tree.assign(n * 4 + 5, Node{});
}
// 上推:用两个孩子合并出当前节点
void push_up(int p) {
tree[p] = tree[lson(p)] + tree[rson(p)];
}
// 把字符串 t 编码成两个位掩码:第 i 个字符对应二进制第 i 位(从 0 开始)。
// '0' 把第 i 位置进 zero,'1' 把第 i 位置进 one,'?' 不设置任何位。
static Node encode(const string &t) {
int z = 0, o = 0;
for (int i = 0; i < (int)t.size(); i++) {
if (t[i] == '0') z |= (1 << i);
else if (t[i] == '1') o |= (1 << i);
}
return Node{z, o};
}
// 用字符串数组 s 建树(下标从 1 开始)
void build(const vector<string> &s, int l, int r, int p = 1) {
if (l == r) {
tree[p] = encode(s[l]);
return;
}
int m = mid(l, r);
build(s, l, m, lson(p));
build(s, m + 1, r, rson(p));
push_up(p);
}
// 单点修改:把位置 pos 的字符串整体替换为 t
void modify(int pos, const string &t, int l, int r, int p = 1) {
if (l == r) {
tree[p] = encode(t);
return;
}
int m = mid(l, r);
if (pos <= m) modify(pos, t, l, m, lson(p));
else modify(pos, t, m + 1, r, rson(p));
push_up(p);
}
// 区间查询:返回 [ql, qr] 内所有约束 OR 合并后的结果
Node query(int ql, int qr, int l, int r, int p = 1) {
if (ql <= l && r <= qr) return tree[p];
int m = mid(l, r);
Node answer;
if (ql <= m) answer = answer + query(ql, qr, l, m, lson(p));
if (qr > m) answer = answer + query(ql, qr, m + 1, r, rson(p));
return answer;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, q;
cin >> n >> m >> q;
vector<string> s(m + 1);
for (int i = 1; i <= m; i++) {
cin >> s[i];
}
SegmentTreeBitOr seg(m);
seg.build(s, 1, m);
int ans_xor = 0; // 所有查询答案的异或和
while (q--) {
int opt;
cin >> opt;
if (opt == 1) {
int pos;
string t;
cin >> pos >> t;
seg.modify(pos, t, 1, m);
} else {
int l, r;
cin >> l >> r;
auto res = seg.query(l, r, 1, m);
if ((res.zero & res.one) == 0) {
// 无冲突:没有被任何串固定的位置都可自由选 0/1,答案 = 2^自由位个数
int free_cnt = n - __builtin_popcount(res.zero | res.one);
ans_xor ^= (1 << free_cnt);
}
// 若 zero & one != 0,某一位同时被固定为 0 和 1,答案为 0,异或 0 不变
}
}
cout << ans_xor << '\n';
return 0;
}复杂度
- 时间:建树
;单次修改或询问 ;总复杂度 。 - 空间:线段树四倍数组
,另有 的原串数组( 视为常数),共 。
总结
这道题把「带 ? 的字符串区间约束」压缩成「两个位掩码 + 按位或合并」:交集语义对应 OR 合并,冲突判定与 push_up / build / 单点改 / 区间查模板结构,本解即由该模板(segtree-point-add-range-sum)把「加合并」改成「OR 合并」而来。
图示解析
这张 ASCII 图展示整道题的解题路线:从暴力枚举出发,到位掩码 + 线段树解法:
朴素暴力(brute.cpp)
枚举全部 2^n 个候选串 S(choose[j] = 第 j 位取 0 或 1)
叶子节点逐信逐位检查兼容性 O(2^n * n * 区间长) 每次询问
|
| 瓶颈:候选串指数级、区间逐串线性级,都不行
v
关键观察
一封信的约束 = 两个位置集合:zero(固定为 0)、one(固定为 1)
区间内所有信同时满足 ⇔ 约束取并集 ⇔ 掩码按位或
z∩o ≠ ∅(某位同时被固定为 0 和 1)→ 答案 0
否则答案 = 2^(n - |z∪o|)(自由位各两种取法)
|
v
线段树(main.cpp)
节点存区间 (zero, one) 两个掩码
push_up:左右儿子分别按位或合并
修改:叶子重新编码,路径上 push_up 上推
查询:区间拆成 O(log m) 个整段节点,OR 进累计变量
|
v
复杂度 O((m + q) log m),空间 O(m)观察要点:图中三条主线分别对应「暴力慢在哪里」「观察到的合并规则」「正式解如何利用这个规则」。合并规则是这道题的灵魂——交集大小的语义落到位掩码上就是 OR;冲突判定与