《算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge。
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。
“ 黑白配” ,链接: http://oj.ecustacm.cn/problem.php?id=1828
题目描述
【题目描述】 黑白配是一个经典的游戏,在每一轮中,孩子们将手朝上(白色)或者朝下(黑色)。
如果所有的孩子做出相同的选择,只有一个例外,那么那个例外的孩子将会被淘汰。
游戏重复进行,直到只剩下两个孩子停止。
每个孩子有一个固定的概率独立选择是否将手朝上。
给定n个孩子的概率,请输出游戏期望回合数是多少。。
【输入格式】 第一行为正整数n,n不超过20,表示孩子数量。
接下来n行,每行一个数字pi表示孩子i的概率,0.1≤pi≤0.9。。
【输出格式】 输出一个数字表示期望回合数。
注意:输出结果与标准结果的绝对误差或者相对误差小于10^-6即视为正确。。
【输入样例】
样例1:
3
0.5
0.5
0.5
样例2:
5
0.1
0.3
0.5
0.7
0.9
【输出样例】
样例1:
1.3333333
样例2:
7.4752846
题解
首先手动计算题目的概率和期望,目的是了解题目的要求。这一题的计算不难。
第一个样例,3人,每人出白或出黑都是0.5。第一个回合有8种情况,其中2种是全黑或全白,6种是淘汰1人剩2人。8种情况中剩下2人的概率是6×0.5×0.5×0.5 = 0.75,结束的回合数期望值是1/0.75=1.3333333。
第二个样例,5人参加。第一轮以一定概率淘汰1人剩4人;第二轮以一定概率淘汰1人剩3人;第3论以一定概率淘汰1人剩下2人,结束。把所有情况的概率加起来,然后计算期望值。
例如可能的情况有:
(1)第一个回合淘汰第1人、第二个回合淘汰第2人、第三个回合淘汰第3人,剩下2人,结束。
第一个回合淘汰第1人概率 = 第1人白且其他人全黑+第1人黑且其他人全白 = 0.1×(1-0.3)×(1-0.5)×(1-0.7)×(1-0.9) + (1-0.1)×0.3×0.5×0.7×0.9 = 0.0861;
第二个回合淘汰第2人的概率 = 第2人白且其他人全黑+第2人黑且其他人全白 = 0.3×(1-0.5)×(1-0.7)×(1-0.9) + (1-0.3)×0.5×0.7×0.9 =0.225
第三个回合淘汰第3人的概率 = 第3人白且其他人全黑+第3人黑且其他人全白 = 0.5×(1-0.7)×(1-0.9) + (1-0.5)×0.7×0.9 =0.33
总概率是0.0861×0.225 = 0.006392925。
(2)第一个回合淘汰第1人,第二个回合淘汰第3人,第二个回合淘汰第2人…
等等,把所有情况的概率相加得到总概率p,1/p是回合数的期望值。
以上步骤需要计算所有可能的情况的概率。并且每次淘汰一人时,只需考虑相关两个回合的概率。例如n个人淘汰1人的概率等于:淘汰这n个人中第1人的概率 × n中不包括第1人的概率 + 淘汰第2人的概率× n中不包括第2人的概率 + … + 淘汰第n人的概率× n中不包括第n人的概率。
这显然符合DP的“重叠子问题、最优子结构”的思想。
如何编程?有两个核心技巧。
(1)用二进制来表示人,编程非常简洁。例如3个人,用000 ~ 111这8个数字表示游戏的组合情况:000是全朝上;111是全朝下;001是前2人朝上、第3人朝下;010是第1和第3人朝上,第2人朝下;…等等。
(2)用DP计算。这种题是DP的套路题,称为“概率DP”,编程简洁易懂。下面是状态定义和状态转移。
定义状态dp:dp[i]表示人数状态为i时的期望轮数。例如:11111是还有5人时的期望轮数;10110是还有第1、3、4人时的期望轮数;等等。
状态转移,见代码中的注释。。
计算复杂度 O ( 2 n ) O(2^n) O(2n)。
【重点】 概率DP。
C++代码
#include<bits/stdc++.h>
using namespace std;
double p[20];
double dp[(1 << 20) + 10]; //dp[i]表示剩余人数状态为i时的期望轮数
int bin_count(int x){
//返回x中有几个1,例如x=1011,返回3
int ans = 0;
while(x)
++ans, x -= (x & (-x));
//x & (-x)是lowbit,即x的最后一个1,例如x=110,x & (-x)=2
//x -= (x & (-x))的功能是去掉x的最后一个1
return ans;
}
int main(){
int n; cin >> n;
for(int i = 0; i < n; i++) cin >> p[i];
for(int x = 0; x < (1 << n); x++){
//第x种情况,例如x=10110表示还有第1人、第3人、第4人
if(bin_count(x) <= 2) continue; //少于等于2人,结束
double aw = 1.0, ab = 1.0; //当前情况下所有人出白的概率,出黑的概率
for(int i = 0; i < n; i++)
if(x & (1 << i)) //统计剩下的人
aw *= p[i], ab *= (1 - p[i]); //aw:所有人朝上的概率,ab:所有人朝下的概率
double tot = 0.0; //tot表示当前情况x转移到其他情况的概率(有人淘汰的概率)
for(int i = 0; i < n; i++)
if(x & (1 << i)){
//pi表示这x人中淘汰第i个人的概率
double pi = aw / p[i] * (1 - p[i]) + ab / (1 - p[i]) * p[i];
//pi = 除了i以外的人都朝上的概率*i朝下的概率 + 除了i都朝下的概率*i朝上的概率
dp[x] += pi * dp[x - (1 << i)]; //淘汰第i人后,继续算剩下的人
tot += pi; //所有情况的概率求和
}
dp[x] = (dp[x] + 1) / tot;
}
printf("%lf",dp[(1<<n)-1]); //n个1,就是n个人的期望轮数
return 0;
}
Java代码
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
double[] p = new double[n];
for (int i = 0; i < n; i++) p[i] = sc.nextDouble();
double[] dp = new double[(1 << 20) + 10];
int all = (1 << n) - 1;
for (int x = 0; x <= all; x++) {
if (Integer.bitCount(x) <= 2) continue;
double aw = 1.0, ab = 1.0;
for (int i = 0; i < n; i++) {
if ((x & (1 << i)) != 0) {
aw *= p[i];
ab *= (1 - p[i]);
}
}
double tot = 0.0;
for (int i = 0; i < n; i++) {
if ((x & (1 << i)) != 0) {
double pi = aw / p[i] * (1 - p[i]) + ab / (1 - p[i]) * p[i];
dp[x] += pi * dp[x - (1 << i)];
tot += pi;
}
}
dp[x] = (dp[x] + 1) / tot;
}
System.out.printf("%.12f\n", dp[all]);
}
}
Python代码
p = []
dp = []
def bin_count(x):
ans = 0
while x:
ans += 1
x -= x & (-x)
return ans
n = int(input())
for _ in range(n): p.append(float(input()))
for i in range(1 << n): dp.append(0.0)
for x in range(1 << n):
if bin_count(x) <= 2: continue # 少于等于2人,结束
aw, ab = 1.0, 1.0
for i in range(n):
if x & (1 << i):
aw *= p[i]
ab *= (1 - p[i])
tot = 0.0
for i in range(n):
if x & (1 << i):
pi = aw / p[i] * (1 - p[i]) + ab / (1 - p[i]) * p[i]
dp[x] += pi * dp[x - (1 << i)]
tot += pi
dp[x] = (dp[x] + 1) / tot
print(dp[(1 << n) - 1])