P10500 Rainbow 的信号
P10500 Rainbow 的信号
一道期望的题目,适合对位运算比较熟悉的人尝试。
题目大意
给定一个 \(n\) 个元素的自然数序列 \(A\),每次以相同概率从 \(\{1, 2, \dots, n\}\) 中选出 \(l\) 和 \(r\)。若 \(l > r\) 则互相交换,由 \(a_l, a_{l + 1}, \dots, a_r\) 构成新序列 \(P\)。现在分别求 \(P\) 的异或、与、或三种位运算结果的期望。
对于 \(100\%\) 的数据,满足 \(1 \le n \le 10^5\),\(0 \le a_i \le 10^9\)。
解法
题目解法参考自李煜东的《算法竞赛进阶指南》。
关于期望与概率
先简单解释一下期望(详见 OiWiki - 数学期望):
期望简单说就是概率 \(\times\) 权重。
不过现在是序列的话,权重这个词应该改为序列的数,然后乘上它被选上的概率。
举个例子:有序列 \(\{4, 3, 1, 4, 9, 7\}\),每个数被选中的概率相等,求选一个数的期望。
因为每个数概率相等,选中一个数的概率就是 \(\frac{1}{6}\)。根据公式可得期望为:
\[4 \times \frac{1}{6} + 3 \times \frac{1}{6} + \cdots + 7 \times \frac{1}{6} = \frac{4 + 3 + 1 + 4 + 9 + 7}{6} = \frac{28}{6} \approx 4.667 \]
此外,我们还需要知道关于序列概率的事情。
对于 \(l = r\) 的情况:每个位置出现的概率都是 \(\frac{1}{n}\),所以像 \((i, i)\) 这样 \(l\)、\(r\) 相等的有序对,其概率为:
那么 \(l \ne r\) 呢?因为题目规定 \(l > r\) 时交换,有序对 \((l, r)\) 与 \((r, l)\) 会被看作同一种区间,所以每个长度 \(\ge 2\) 的区间其实对应两种有序对,概率要乘以 \(2\):
分类讨论
我们把每个 \(a_i\) 拆成二进制、逐位处理。因为 \(a_i \le 10^9 < 2^{30}\),所以只需要考虑 \(0 \sim 29\) 这 \(30\) 位就够了(代码枚举到 \(k = 30\) 只是多算一个恒为 \(0\) 的位,贡献为 \(0\))。
接下来就是逐位计算期望。为什么能单独计算每一位?因为位运算不进位,位与位之间互不影响;再根据期望的线性性,把每一二进制位的期望累加起来就是总期望:
不过呢,每一位都只有 \(0\) 和 \(1\) 两种取值,所以统计时只关心当前位为 \(1\) 的区间,其实际贡献就是 \(2^k\)。
分三种计算来讨论:
与运算
与运算简单说就是:只要有一个位为 \(0\),结果就是 \(0\),否则为 \(1\)。
所以要产生贡献,只能选一段连续全为 \(1\) 的区间。枚举区间的右端点 \(r\),并记 \(last[x]\) 为「最近一个第 \(k\) 位等于 \(x\) 的数所在的位置」,这里 \(last[0]\) 就是最近一个 \(0\) 的位置。只要左端点 \(l\) 满足 \(last[0] < l \le r\),区间 \([l, r]\) 内就没有 \(0\),这一位结果就是 \(1\)。这样的 \(l\) 共有
个。
不过这 \(r - last[0]\) 个区间里,混着一个长度为 \(1\) 的 \([r, r]\):它的概率只有 \(\frac{1}{n^2}\),其余区间长度都 \(\ge 2\),概率才是 \(\frac{2}{n^2}\)。所以先全部按 \(\frac{2}{n^2}\) 累加,再减掉 \([r, r]\) 多算的那一份 \(\frac{2^k}{n^2}\),得到:
或运算
简单说:只要有一个位为 \(1\),结果就是 \(1\),条件比与运算宽松。
因为条件放宽,当前位的值要分成 \(1\) 和 \(0\) 两种情况讨论。
若当前位为 \(1\),左端点 \(l\) 取 \(1 \sim r\) 任意一个都行,共 \(r\) 个。同样里面藏着长度为 \(1\) 的 \([r, r]\) 需要修正,所以:
若当前位为 \(0\),就只能依靠前面已有的 \(1\)。还记得 \(last\) 数组吗?只要左端点取 \(1 \sim last[1]\),区间里就至少含一个 \(1\),共 \(last[1]\) 种。因为 \(last[1] < r\),这些区间长度都 \(\ge 2\),没有单点,不用修正,所以:
异或运算
简单说:有奇数个 \(1\) 时结果才是 \(1\)。
因此可以以「遇到的 \(1\)」为界来分段,记录当前处在奇数段还是偶数段:\(c_1\) 为奇数段长度、\(c_2\) 为偶数段长度,一旦遇到 \(1\),奇、偶段的身份就要互换。
可以发现,当前位为 \(0\) 时,只有落在偶数段里的区间结果才是 \(1\),个数为 \(c_2\);而且这些区间都截止在 \(r\) 之前、长度 \(\ge 2\),没有单点,所以:
当前位为 \(1\) 时,对应奇数段 \(c_1\);此外长度为 \(1\) 的 \([r, r]\) 自身(异或值也是 \(1\))也要按 \(\frac{1}{n^2}\) 单独加一份,所以:
实现时要注意维护顺序(对照代码):读入 \(0\) 时只 ++c1;读入 \(1\) 时,先按上式累加贡献,再 ++c1 并 swap(c1, c2)——因为碰到 \(1\) 意味着开启新段,奇、偶段的身份互换了。
那么,理论存在,实践开始:
#include<bits/stdc++.h>
#define int long long
using namespace std;
double XOR,AND,OR;
int n,a[100005],last[2],c1,c2;
signed main(){
cin >> n;
for(int i = 1;i <= n;i++)
cin >> a[i];
for(int k = 0;k <= 30;k++){
last[0] = last[1] = c1 = c2 = 0; // 每一二进制位都要重新清零
for(int i = 1;i <= n;i++){
int now = (a[i] >> k) & 1;
double tmp = 1.0 * (1LL << k) / (n * n); // 2^k / n^2 = 该位的"概率×权值"基准
if(now){
// 单点概率 1/n^2,其余概率 2/n^2
AND += 2.0 * tmp * (i - last[0]) - tmp; //由于序列会有长度为1的情况,所以要减去一份多算的tmp
OR += 2.0 * tmp * i - tmp;
XOR += tmp + 2.0 * tmp * c1;
c1++;swap(c1,c2);
}else{
OR += 2.0 * tmp * last[1]; // last[1] < i,区间都含第 i 位且长度≥2,无单点
XOR += 2.0 * tmp * c2;
c1++;
}
last[now] = i;
}
}
cout<<fixed<<setprecision(3)<<XOR<<" "<<AND<<" "<<OR<<"\n";
return 0;
}
谢谢观看!

浙公网安备 33010602011771号