程序设计实习 5:表达式的期望值
AC代码
#include <iostream>
#include <cstring>
#include <iomanip>
using namespace std;
inline double fuzzy_logic(double lhs, char op, int rhs) {
switch (op) {
case '^':
return rhs ? 1 - lhs : lhs;
case '|':
return rhs ? 1 : lhs;
case '&':
return rhs ? lhs : 0;
default:
return 0;
}
}
int arr[210];
char ops[210];
double prob[210];
int main() {
int n, n_cases = 0;
while (cin >> n) {
++n;
double result = 0;
int digit = 0;
for (int i = 0; i < n; ++i)
cin >> arr[i];
for (int i = 1; i < n; ++i)
cin >> ops[i];
for (int i = 1; i < n; ++i)
cin >> prob[i];
for (int i = 0; i < 21; ++i) {
double E = (double)((arr[0] >> i) & 1);
for (int j = 1; j < n; ++j)
E = prob[j] * E + (1 - prob[j]) * fuzzy_logic(E, ops[j], (arr[j] >> i) & 1);
result += E * (double)(1 << digit);
++digit;
}
cout << "Case " << ++n_cases << ":" << endl << setiosflags(ios::fixed) << setprecision(6) << result << endl;
}
}
解题思路
这道题笔者去年想了很久才也没有想出怎么解,也因为题目形式过于不按套路,当年北京大学程序设计实习期末考试中也仅有不足20人解出此题。但事实上,这道题目的设计非常漂亮,创造性地融合了动态规划和位运算,并且事实上也不是十分困难。
求离散随机变量的期望很自然的想法就是把分布列求出来,然后 E ( x ) = ∑ x ∈ Ω x ⋅ p ( x ) E(x) = \sum_{x \in \Omega} x\cdot p(x) E(x)=∑x∈Ωx⋅p(x)暴力求解。然鹅,对于这个问题,所有的可能的值有 2 n 2^n 2n种,而 n n n的范围是2~200,所以该方法是不可取的。
按位与、按位或、按位异或的位间独立性
这是这道题最为巧妙的地方——题目设计中涉及的3个符号:&
、|
、^
都有一个共同的特点,就是都是位间独立的,更具体地讲,a ^ b
的第
k
k
k位仅与a
和b
的第
k
k
k位有关。这使我们可以把这个问题拆解成更为精细的子问题:对
[
0
,
20
)
[0, 20)
[0,20)内的每个
i
i
i,求20个整数第
i
i
i位
X
i
X_i
Xi的期望(
X
i
X_i
Xi只可能为0或1,故这个期望是一个
[
0
,
1
]
[0, 1]
[0,1]之间的实数。那么最后求得的期望就是:
E
[
X
]
=
E
[
∑
i
=
0
20
2
i
X
i
]
=
∑
i
=
0
20
2
i
E
[
X
i
]
E[X] = E[\sum_{i = 0}^{20}2^i X_i] = \sum_{i = 0}^{20}2^i E[X_i]
E[X]=E[i=0∑202iXi]=i=0∑202iE[Xi]
这个求和当然是很简单的。
动态规划
现在的事情就是对这样一个问题设计动归:
一个0, 1序列,
a
0
,
a
1
,
⋯
 
,
a
n
a_0, a_1, \cdots, a_n
a0,a1,⋯,an,除
a
0
a_0
a0外每个
a
i
a_i
ai对应一个按位运算符
o
i
o_i
oi,它有
p
i
p_i
pi的概率消失
如果动归子问题设计为:求前 a 0 , a 1 , ⋯ a k a_0, a_1, \cdots a_k a0,a1,⋯ak(即前 k + 1 k+1 k+1个 a i a_i ai)经过可能消失的运算后所得结果(这是一个随机变量,后记为 ξ i \xi_i ξi)的期望 x k x_k xk(即 E [ ξ i ] = x i E[\xi_i] = x_i E[ξi]=xi),那么显然 x 0 = a 0 x_0 = a_0 x0=a0( a 0 a_0 a0是固定且没有消失概率的),而 x n x_n xn则是我们想要的结果,接下来就是需要给 x k x_k xk到 x k + 1 x_{k+1} xk+1 ( k = 0 , ⋯   , n − 1 ) (k = 0, \cdots, n-1) (k=0,⋯,n−1)设计递推公式了,这里就是这个问题的巧妙之处叻
0-1分布的逻辑运算
既然已经将问题转化为对一串0, 1序列进行期望求解,问题便突然简单了起来:如果一个分布是0, 1二值分布,那它的期望不就是为1的概率嘛?所以, x k x_k xk也是 ξ k = 1 \xi_k = 1 ξk=1的概率,那么第 k + 1 k + 1 k+1位显然分2种情况:
- o k + 1 a k + 1 o_{k + 1} a_{k + 1} ok+1ak+1消失了,这件事发生的概率为 p k + 1 p_{k + 1} pk+1,在该条件 ξ k + 1 \xi_{k + 1} ξk+1的期望仍为 x k x_k xk
-
o
k
+
1
a
k
+
1
o_{k + 1} a_{k + 1}
ok+1ak+1没有消失,这件事发生的概率为
1
−
p
k
+
1
1 - p_{k + 1}
1−pk+1,此时
ξ
k
+
1
\xi_{k + 1}
ξk+1的期望
ξ
k
o
k
+
1
a
k
+
1
\xi_{k}o_{k + 1} a_{k + 1}
ξkok+1ak+1的期望——比如如果
o
k
+
1
o_{k + 1}
ok+1是
^
, a k + 1 a_{k + 1} ak+1是1,那么一定有
P ( ξ k + 1 = 1 ) = P ( ξ k = 0 ) = 1 − x k P(\xi_{k + 1} = 1) = P(\xi_{k} = 0) = 1 - x_k P(ξk+1=1)=P(ξk=0)=1−xk
类似地,可以用仅与 x k + 1 x_{k + 1} xk+1有关的表达式来来表示 o k + 1 a k + 1 o_{k + 1} a_{k + 1} ok+1ak+1为“与0”、“与1”、“或0”、“或1”、“异或0”时 ξ k o k + 1 a k + 1 \xi_{k}o_{k + 1} a_{k + 1} ξkok+1ak+1的期望值(分别为 0 , x k , x k , 1 , x k 0, x_k, x_k, 1, x_k 0,xk,xk,1,xk),见AC代码中fuzzy_logic
函数
第二种情形稍微复杂一些。这样,我们可以得到
x
k
+
1
x_{k+1}
xk+1与
x
k
x_k
xk的关系式:
x
k
+
1
=
p
k
+
1
x
k
+
(
1
−
p
k
+
1
)
E
[
ξ
k
+
1
o
k
+
1
a
k
+
1
]
x_{k+1} = p_{k+1}x_k + (1 - p_{k + 1}) E[\xi_{k+1}o_{k+1}a_{k+1}]
xk+1=pk+1xk+(1−pk+1)E[ξk+1ok+1ak+1]
其中
E
[
ξ
k
+
1
o
k
+
1
a
k
+
1
]
E[\xi_{k+1}o_{k+1}a_{k+1}]
E[ξk+1ok+1ak+1]可以用
x
k
x_k
xk表示.
当 a 0 ∼ a n a_0 \sim a_{n} a0∼an表示输入的 n + 1 n+1 n+1个正整数的第 k k k位时, x n x_n xn就是所求结果第 k k k位的期望 E [ X k ] E[X_k] E[Xk]
其他
面对复杂的输出的时候iostream
+ iomanip
的函数真的不如printf
好用>_<