把 3 个期望按 30 个二进制位拆开,每位的 xor/and/or 只需统计 0/1 连续段长度与前缀异或的取值次数,再用 2S-T 换算成有序对口径。
OJ: roj
题目 ID: 3094
难度:提高+/省选-
标签:位运算前缀异或数学计数贡献法python
创建: 2026-10-01 16:43
更新: 2026-10-07 13:50
形式化题目
给定长度为
要求分别计算三个数学期望
三条运算都是逐位独立的:结果的第
正解
思路
题面的两个量级决定了整条路线:
朴素做法的瓶颈在区间个数。 最直接的做法是枚举所有有序对
for l in 1..N:
for r in 1..N:
扫描 a[l..r] 求 xor / and / or即使边扩右端点边维护、让每个区间做到
第一步:按位拆分。 xor、and、or 都是逐位定义的运算,第
由于按位展开后求和可以交换次序,
其中
第二步:一位上的三种统计。 固定一位,设抽出的 0/1 串是
| 运算 | 区间结果为 1 的条件 | |
|---|---|---|
| and | 区间内全是 1,即区间完整落在某个 1 段内 | |
| or | 区间内至少一个 1;用全集减掉"全是 0" | |
| xor | 区间内有奇数个 1,等价于 |
最后一行里的
关键在于
- 遇到长度
的 1 段:段内 逐格交替,前 格翻到对面,后 格落回原值,出段时按 的奇偶决定新的前缀值; - 遇到长度
的 0 段: 一个都不变, 格全部计入当前值一侧。
这样一位上的全部统计只需遍历段表,段数最多
第三步:换回有序对口径。 上面统计的是
其中
用样例
| 子段 |
xor | and | or | |
|---|---|---|---|---|
| 4 | 4 | 4 | ||
| 1 | 4 | 5 | ||
| 1 | 4 | 5 | ||
| 5 | 5 | 5 | ||
| 总和 | 11 | 17 | 19 | |
| 除以 |
2.750 | 4.250 | 4.750 |
再看按位拆分。低三位以外的位全是 0,逐位加权正好还原上面的总和:
| 位 |
位权 |
该位 0/1 串 | and 贡献 |
or 贡献 | xor 贡献 | ||||
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 1 |
1 | 2 | 2 | 1 | 1 | 3 | 3 |
| 1 | 2 | 0 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 4 | 1 1 |
3 | 3 | 2 | 2 | 4 | 4 | 2 |
| 加权求和 |
第 2 位是全 1 段,
整体路线如下:
N 个数 a_1..a_N
│ 写成定长 30 位二进制,首尾相接
▼
30 × N 的 0/1 矩阵 ──按列切片──▶ 第 k 位的 0/1 串
│
├─ 连续段表 ─▶ S_and / S_or / c0·c1
│
└─ 2S - T ─▶ 该位贡献 × 2^k
▲ │
└──────────────── 30 列加权求和 ◀──────────────────────────┘
│
▼
总和 / N^2 ─▶ 三个期望(保留 3 位小数)推导中的量落到代码里是这样的:定长二进制矩阵是 packed,按列切片是 packed[k::WIDTH];连续段表是 runs;all_one 和 total - all_zero;seen[0] * seen[1](seen 在遍历 runs 时按段累计前缀异或 singles;有序对修正与位权加权收在 one_bit_values 的返回值与 solve() 的求和式里。
代码
Python 版:
#!/usr/bin/env python3
# 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-10-01 16:43
# update_at: 2026-10-01 17:00
import re
import sys
WIDTH = 30 # 10^9 < 2^30,定长二进制串才能按列切片取出每一位
def one_bit_values(column: str) -> tuple[int, int, int]:
"""一位上的三份加权分子:column 是该位按原下标顺序拼成的 0/1 串。
xor / and / or 的每份答案都是「所有区间的该位值(0 或 1)之和」,
而三种运算的取值都只由同值连续段的长度决定,所以先压缩成段长表再统计。
"""
runs = [(int(part[0]), len(part)) for part in re.findall("0+|1+", column)]
ones = [length for value, length in runs if value]
zeros = [length for value, length in runs if not value]
all_one = sum(length * (length + 1) // 2 for length in ones) # 全是 1 的区间数 = and 为 1 的区间数
all_zero = sum(length * (length + 1) // 2 for length in zeros) # 全是 0 的区间数
total = len(column) * (len(column) + 1) // 2 # 区间总数
# 前缀异或 P_i 只在扫过 1 时翻转:1 段内部逐格交替,0 段内部保持不变。
# P 取到 0 的次数乘取到 1 的次数,就是 xor 为 1 的区间数。
parity, seen = 0, [1, 0] # P_0 = 0,先计入偶侧
for value, length in runs:
if value:
seen[parity] += length // 2 # 段内交替,后 floor(L/2) 个落回原奇偶
seen[parity ^ 1] += (length + 1) // 2 # 前 ceil(L/2) 个翻到对面
parity ^= length & 1 # 段的奇偶长度决定出段时的前缀值
else:
seen[parity] += length # 0 段不改变前缀值
# 长度大于 1 的区间有 (l, r) 两种取法,单点区间只有一种,故加权数为 2c - s
singles = sum(ones) # 该位上 1 的个数 = 单点区间里 op 结果为 1 的个数
return tuple(2 * count - singles for count in (seen[0] * seen[1], all_one, total - all_zero))
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
signal = [next(data) for _ in range(n)]
# 全部数拼成 WIDTH 位定长串:按列切片就一次拿到所有位的 0/1 序列
packed = ''.join(f'{value:0{WIDTH}b}' for value in signal)
parts = [one_bit_values(packed[k::WIDTH]) for k in range(WIDTH)]
# 按位独立:第 k 列的位权是 2^(WIDTH-1-k)(最高位在最左),30 列加权相加即三份答案
totals = [sum(part[i] << (WIDTH - 1 - k) for k, part in enumerate(parts)) for i in range(3)]
# 分母是 (l, r) 的有序取法总数 N^2,与上面的加权计数口径一致
print(' '.join(f'{total / n ** 2:.3f}' for total in totals))
if __name__ == "__main__":
solve()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-10-06 18:35
* update_at: 2026-10-06 18:35
*/
#include <cstdio>
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXN = 100005; // N 最大 1e5
const int WIDTH = 30; // a_i < 2^30,逐位拆成 30 列
ll n;
ll a[MAXN]; // 题目给出的信号,下标从 1 开始
ll run_value[MAXN]; // 某一列压缩后的连续段取值(0/1)
ll run_length[MAXN]; // 对应连续段的长度
// 计算第 k 位上的三种「有序区间计数」(未乘位权):
// 把该位抽成 0/1 串并压缩成同值连续段,由段长得到 and / or 的区间数;
// 再由前缀异或取值次数得到 xor 的区间数,最后用 2S-T 换成有序对口径。
void count_one_bit(int k, ll &cnt_xor, ll &cnt_and, ll &cnt_or) {
int seg = 0; // 连续段个数
int i = 1;
while (i <= n) {
ll value = (a[i] >> k) & 1;
int j = i;
while (j <= n && ((a[j] >> k) & 1) == value) {
j++;
}
seg++;
run_value[seg] = value;
run_length[seg] = j - i;
i = j;
}
ll all_one = 0; // 完整落在 1 段内的区间数,即 and 为 1 的无序区间数
ll all_zero = 0; // 完整落在 0 段内的区间数
ll ones = 0; // 该位上 1 的个数,等于单点区间里结果为 1 的个数
for (int t = 1; t <= seg; t++) {
ll len = run_length[t];
if (run_value[t] == 1) {
all_one += len * (len + 1) / 2;
ones += len;
} else {
all_zero += len * (len + 1) / 2;
}
}
ll total = n * (n + 1) / 2; // l <= r 的无序区间总数
// 前缀异或 P_i 只在扫过 1 时翻转:1 段内逐格交替,0 段内保持不变。
ll seen[2] = {1, 0}; // P_0 = 0,先计入取值 0 的一侧
int parity = 0;
for (int t = 1; t <= seg; t++) {
ll len = run_length[t];
if (run_value[t] == 1) {
seen[parity] += len / 2;
seen[parity ^ 1] += (len + 1) / 2;
parity ^= len & 1;
} else {
seen[parity] += len;
}
}
ll un_xor = seen[0] * seen[1]; // 区间异或为 1 的无序区间数
ll un_and = all_one;
ll un_or = total - all_zero; // 全集减掉全 0 区间,即含 1 的区间数
// 长度大于 1 的区间在有序对里出现两次,单点区间只出现一次
cnt_xor = 2 * un_xor - ones;
cnt_and = 2 * un_and - ones;
cnt_or = 2 * un_or - ones;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
long double sum_xor = 0.0L;
long double sum_and = 0.0L;
long double sum_or = 0.0L;
long double bit_value = 1.0L; // 当前位的位权 2^k
for (int k = 0; k < WIDTH; k++) {
ll cnt_xor, cnt_and, cnt_or;
count_one_bit(k, cnt_xor, cnt_and, cnt_or);
sum_xor += bit_value * cnt_xor;
sum_and += bit_value * cnt_and;
sum_or += bit_value * cnt_or;
bit_value *= 2.0L;
}
long double denom = n;
denom = denom * n; // 分母是有序对总数 N^2
printf("%.3Lf %.3Lf %.3Lf\n", sum_xor / denom, sum_and / denom, sum_or / denom);
return 0;
}复杂度
- 时间:打包是
,按列切片 30 次 ,每列求连续段与统计也是 ,总计 ,即 ( 为值域上界)。 时约 次基本操作,远快于朴素做法的 。 - 空间:
packed占个字符,加上读入的整数列表,以及每列临时产生的切片与段表,总量仍是 ;实测 时峰值约 33MB。
总结
- 按位拆分是位运算期望题的通用起手式:xor / and / or 逐位独立,把 30 位拆成 30 个独立的 0/1 计数问题,值域的对数只有 30,代价极小。
- 一位上的三种统计各自有明确的结构刻画:and 数"全是 1 的区间"、or 数"含 1 的区间"(用全集减去全 0 段)、xor 数"前缀异或取值不同的下标对"。前两者只需连续段长度,后者只需每段对
取值的分摊次数。 - 统计的是无序区间时,务必用
换回题目口径,分母是 。