概率问题的解决方法通常是设计状态并画出状态转移图,根据状态转移图用递推或解方程的方法解出目标状态的概率或期望。
概率问题的状态转移方程:
Pcur=∑(PlastToCur×Plast)
期望问题的状态转移方程:
Ecur=∑(Pnext×(Enext+Costnext))
当然,也有可以直接用概率或期望的公式算出来的题目。
LightOJ 1027
大意
起始状态为主人公在一个房间中,房间中有若干个门,打开每个门,通过等待一定的时间,要么重新回到房间内,要么走出房间,问走出房间消耗的时间的期望是多少。
思路
本题的状态只有两个, S1 表示正在选择进入哪个门的状态, S2 表示走出迷宫的状态。 S1 可以转移到两种状态即 S1 和 S2 ,根据这些关系把状态转移图画出来。下一步是根据状态图由已知状态的期望得出我们要求的状态的期望。这里只有两种状态,因此只需要列状态转移方程,解方程即可(相当于做了一次状态转移)。本题的方程可以表示为:
E1=P2×(0+ECost2)+P1×(E1+ECost1)
其中, E1 表示从 S1 到 S2 的期望时间(要求的量), ECost1 表示从 S1 到 S1 等待的期望时间, ECost2 表示从 S1 到走出房间等待的期望时间, P1 表示从 S1 转移到 S1 的概率, P2 表示从 S1 转移到 S2 的概率。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
int t, c, n, a, s, cnt, g;
int main() {
scanf("%d", &t);
for(int c = 1; c <= t; c++) {
printf("Case %d: ", c);
scanf("%d", &n);
s = cnt = 0;
for(int i = 1; i <= n; i++) {
scanf("%d", &a);
s += abs(a);
cnt += (a > 0);
}
g = __gcd(s, cnt);
if(cnt == 0) {
puts("inf");
}
else {
printf("%d/%d\n", s / g, cnt / g);
}
}
return 0;
}
LightOJ 1038
大意
题给一个数
N
,初始有
思路
因为本题的组数有很多,所以应该预处理出所有
d[i](1≤i≤1e5)
,然后根据输入直接输出。首先我们可以定义状态
i
表示当前的数为
d[i]=∑j∈divisor(i)(d[j]cnt(i))+1
因 divisor(i) 中含有 i (相当于状态图中出现了环),因此不能直接递推,要将式子变形为
然后就可以将 d[i] 提出,并且设 sum=∑j∈divisor(i)andj≠i(d[j]cnt(i)) ,于是有
d[i]=sum+cnt(i)cnt(i)−1
由于求 divisor(i) 的复杂度是 O(log(i)) 的,所以总的复杂度是 O(nlog(n)) 。
代码
#include <cstdio>
const int maxn = 1e5 + 5;
int t, n, cnt;
double sum, d[maxn];
int main() {
d[1] = 0;
for(int i = 2; i < maxn; i++) {
cnt = 0;
sum = 0;
for(int j = 1; j * j <= i; j++) {
if(i % j != 0) {
continue;
}
sum += d[j];
cnt++;
if(j * j != i) {
sum += d[i/j];
cnt++;
}
}
d[i] = (sum + cnt) / (cnt - 1);
}
scanf("%d", &t);
for(int c = 1; c <= t; c++) {
scanf("%d", &n);
printf("Case %d: %.10f\n", c, d[n]);
}
return 0;
}
LightOJ 1265
大意
主人公参加一个类似于荒岛求生的真人秀节目,荒岛上有鹿,老虎和人类(主人公)三种生物。另外有五条规则:
- 两虎相争两者皆死
- 老虎会吃掉遇上的人
- 老虎会吃掉遇上的鹿
- 人可以决定杀不杀死遇上的鹿
- 两只鹿相遇后什么事都不发生
题给老虎和鹿的数量,求人生存下来的概率是多少。
思路
这题当然可以定义状态
(i,j)
——当前存在着
i
只老虎和
代码
#include <cstdio>
int t, n, m;
double ans;
int main() {
scanf("%d", &t);
for(int c = 1; c <= t; c++) {
printf("Case %d: ", c);
scanf("%d%d", &n, &m);
if(n == 0) {
puts("1.0000000000");
continue;
}
if(n % 2 == 1) {
puts("0.0000000000");
continue;
}
ans = 1;
while(n > 0) {
ans *= 1.0 * (n - 1.0) / (n + 1.0);
n -= 2;
}
printf("%.10f\n", ans);
}
return 0;
}
LightOJ 1408
大意
一个人如果连续击中 k1 次球或者连续击不中球 k2 次练习就结束了,其中击不中球的概率为 p 。求击球次数的期望值。
思路
我们令状态
1
转移到
2
转移到
……
k1−1
转移到
k1
和
−1
−1
转移到
−2
(击不中)和
1
(击中)
……
由于状态的转移产生回路,因此无法用动态规划解决。但我们不难发现,我们仍然可以列出
E(1)=qE(2)+pE(−1)+1
若将 E(−1) 看成常量的话,则上式可以看成连接 E(i) 和 E(i+1) 的桥梁。将上式中的 E(2) 展开又如何呢?我们有
E(1)=q2E(3)+p(q+1)E(−1)+q+1
若一直展开下去,就会有
E(1)=qk1−1E(k1)+p∑i=0k1−2qi×E(−1)+∑i=0k1−2qi
同样,对于 E(−1) ,用类似的处理方法有
E(−1)=qk2−1E(−k2)+q∑i=0k2−2pi×E(1)+∑i=0k2−2pi
显然 E(1)=E(−1)=0 ,另外和式也都可以展开,因此
E(1)=(1−qk1−1)E(−1)+1−qk1−11−q
E(−1)=(1−pk2−1)E(1)+1−pk2−11−p
联立两式,设 a=qk1−1,b=pk2−1 ,得
E(1)=p(1−a)(1−b)+q(1−a)pq(a+b−ab)
E(−1)=(1−b)E(1)+1−bq
进而得到答案
E(0)=qE(1)+pE(−1)
代码
#include <cstdio>
#include <cmath>
const double eps = 1e-10;
int k1, k2;
double p, q;
int main() {
int t;
int cas = 1;
scanf("%d",&t);
while(t--) {
scanf("%lf%d%d", &p, &k1, &k2);
if(p < eps) {
printf("Case %d: %d\n",cas++, k1);
continue;
}
if(1 - p < eps) {
printf("Case %d: %d\n",cas++, k2);
continue;
}
q = 1.0 - p;
double s1 = 1 - pow(q, k1 - 1);
double s2 = 1 - pow(p, k2 - 1);
double tmp1 = s1 / p;
double tmp2 = s2 / q;
double g = (tmp1 * tmp2 * q +tmp2) / (1 - tmp1 * tmp2 * p * q);
double f = tmp1 * (p * g + 1);
printf("Case %d: %lf\n",cas++, q * f + p * g + 1);
}
}
POJ 2096
大意
某个系统中有
s
个子系统和
思路
这个问题的时间线上有很多状态,但归根结底可以用
(i,j)
来表示这些状态。
d[i][j]
表示出现
i
个
d[i][j]=ijnsd[i][j]+(n−i)jnsd[i+1][j]+i(s−j)nsd[i][j+1]+(n−i)(s−j)nsd[i+1][j+1]+1
最后根据方程从 d[n][s] 向前递推,最后 d[0][0] 就是答案。
代码
#include <cstdio>
const int maxn = 1010;
int n, s;
double d[maxn][maxn];
int main() {
scanf("%d%d", &n, &s);
for(int i = n; i >= 0; i--) {
for(int j = s; j >= 0; j--) {
if(i == n && j == s) {
continue;
}
d[i][j] = 1.0 * n * s;
d[i][j] += 1.0 * (n - i) * j * d[i+1][j];
d[i][j] += 1.0 * i * (s - j) * d[i][j+1];
d[i][j] += 1.0 * (n - i) * (s - j) * d[i+1][j+1];
d[i][j] /= (1.0 * n * s - i * j);
}
}
printf("%.4f\n", d[0][0]);
return 0;
}
Codeforces 540D
大意
某个小岛上有
r
个石头,
思路
我们可以用
d[i][j][k]=PrMeets(i,j)×d[i][j−1][k]+PsMeetp×d[i][j][k−1]+PrMeetp×d[i−1][j][k]
最后剩下两种生物的时候就能得到结果。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 105;
int r, s, p, m;
double sum, a, b, c, d[maxn][maxn][maxn];
int main() {
scanf("%d%d%d", &r, &s, &p);
memset(d, 0, sizeof(d));
d[r][s][p] = 1;
for(int i = r; i > 0; i--) {
for(int j = s; j > 0; j--) {
for(int k = p; k > 0; k--) {
sum = i * j + j * k + k * i;
d[i][j-1][k] += i * j / sum * d[i][j][k];
d[i][j][k-1] += j * k / sum * d[i][j][k];
d[i-1][j][k] += i * k / sum * d[i][j][k];
}
}
}
a = b = c = 0;
m = max(max(r, s), p);
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= m; j++) {
a += d[i][j][0];
b += d[0][i][j];
c += d[i][0][j];
}
}
printf("%.10f %.10f %.10f\n", a, b, c);
return 0;
}
Codeforces 697D
思路
如果想以树的节点编号做状态来做树形动态规划的话,对于每个节点,都要枚举子节点的访问顺序。这样的复杂度太高了。所以我们转而用公式法或者贡献度法来解决。对于一个点
v
,
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n, p, ans, c[maxn], d[maxn];
vector <int> G[maxn];
void dfs(int u) {
c[u] = 1;
for(int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
d[v] = d[u] + 1;
dfs(v);
c[u] += c[v];
}
}
int main() {
scanf("%d", &n);
for(int i = 2; i <= n; i++) {
scanf("%d", &p);
G[p].push_back(i);
}
dfs(1);
for(int i = 1; i <= n; i++) {
ans = n - c[i] + d[i] + 2;
printf("%d.", ans / 2);
printf(ans & 1 ? "5 " : "0 ");
}
puts("");
return 0;
}
Codeforces 697E
思路
因为这是一个概率问题,所以先按照状态转移的思想来思考。如果将初始状态定义为
(1,2,3)
,那么状态空间将只有
6
个元素——
因为要得到最最简分数形式的概率,因此光得到递推公式是不够的,我们要求一个能直接计算出结果的封闭形式。于是用待定系数法可以得到一个等比数列的递推公式
d[i]−13=−12(d[i−1]−13)
解这个递推公式得
d[n]=(−1)n+2n−13×2n−1
凑巧的是,
(−1)n+2n−1
正好是
3
的倍数(与
q=2n−1
题目就得解了。另外要注意的是,对分数取
mod
的时候要对分母计算模逆元,也就是要算出
3
的模逆元。计算
最后,偶然发现像P这样的数被称为 Jacobsthalnumbers (维基百科)。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
bool one = true;
int k, odd = 1;
ll a, p, q = 2;
// 快速幂算法
ll modPow(ll a, ll n, ll mod) {
ll ans = 1;
for(; n > 0; n >>= 1) {
if(n & 1) {
ans = (ans * a) % mod;
}
a = (a * a) % mod;
}
return ans;
}
// 求模逆元
ll modInv(ll a, ll mod) {
return modPow(a, mod - 2, mod);
}
int main() {
scanf("%d", &k);
for(int i = 0; i < k; i++) {
scanf("%I64d", &a);
odd &= (a & 1);
q = modPow(q, a, mod);
if(a > 1) {
one = false;
}
}
q = (q * modInv(2, mod)) % mod;
if(one == true) {
p = 0;
}
else {
p = q;
p += (odd ? -1 : 1);
p = (p * modInv(3, mod)) % mod;
}
printf("%I64d/%I64d\n", p, q);
return 0;
}
HDU 5753
大意
题给一个序列
c
,另规定
思路
根据本题的数据规模来看,肯定无法枚举排列然后算期望了。也不好设计出状态来通过状态转移方程解决问题。一个可行的思维是算出序列 c 中每个元素对期望的贡献。
- 当考虑
c 的第 1 个元素c1 时,我们只要考虑前两个元素的排列,当前两个元素呈现出 c1>c2 时 c1 的贡献就会被算进期望中。显然 c1>c2 的概率是 12 。- 当考虑
c
的第
2 个元素 c2 时,我们只要考虑前三个元素的排列,当前三个排列呈现出 c1<c2,c3<c2 时, c2 的贡献就回被算进期望中。显然这种排列的概率是 13 。 - 第
3
到第
n−1 个元素的贡献与第 2 个元素的贡献相似。第n 个元素的贡献与第 1 <script id="MathJax-Element-229" type="math/tex">1</script> 个元素的贡献相似。 于是我们枚举每个元素,将其贡献加入期望中即可。
代码
#include <bits/stdc++.h> using namespace std; int n, a; double ans; int main() { while(~scanf("%d", &n)) { ans = 0; for(int i = 1; i <= n; i++) { scanf("%d", &a); if(i == 1 || i == n) { ans += 1.0 * a / 2; } else { ans += 1.0 * a / 3; } } if(n == 1) { printf("%.5f\n", 1.0 * a); } else { printf("%.5f\n", ans); } } return 0; }
- 当考虑
c
的第