并不是所有的题都要期望dp,有的可以直接算出概率然后计算期望
题目1:poj 2096:
题意:一个软件有s个子系统,会产生n种bug。
某人一天发现一个bug,这个bug属于某种bug,发生在某个子系统中。
求找到所有的n种bug,且每个子系统都找到bug,这样所要的天数的期望。
需要注意的是:bug的数量是无穷大的,所以发现一个bug,出现在某个子系统的概率是1/s,
属于某种类型的概率是1/n。
(摘自 https://www.cnblogs.com/jackge/archive/2013/05/21/3091757.html)
解法:期望dp入门题,设dp[i][j]为已经得到的bug中包括i种bug,j种系统,要得到n种bug,s个子系统bug的期望天数。显然dp[n][s] = 0,从dp[i][j]花掉一天找bug,肯能达到四种状态:(1)dp[i][j],找到的bug都是已有的.(2) dp[i+1][j] ,找到的bug是新种的。(3) dp[i][j+1] 找到的bug是新子系统的 (4) dp[i+1][j+1],找到的bug是新种的同时是新子系统的。
概率分别为:
(1) i * j / n * s
(2) (n - i) * j / n * s
(3) i * (s - j) / n * s
(4) (n - i) * (s - j) / n * s
根据全期望,期望可以由子期望加权得到,可以列出转移方程。
分别是(四种状态的期望 乘上概率) 的和,再加上1天,这个1天是做出决策转移的代价。
发现转移状态中有相同的状态,移项即可。
注意输出用 %.4f
//逆推
#include<iostream>
using namespace std;
const int maxn = 1e3 + 10;
#include<stdio.h>
#include<string.h>
int n,s;
double dp[maxn][maxn];
int main() {
while(~scanf("%d%d",&n,&s)){
memset(dp,0,sizeof(dp));
dp[n][s] = 0;
for(int i = n; i >= 0; i--) {
for(int j = s; j >= 0; j--) {
if(i == n && j == s) continue;
dp[i][j] += (double)dp[i + 1][j] * (n - i) * j / (double)(n * s);
dp[i][j] += (double)dp[i][j + 1] * i * (s - j) / (double)(n * s);
dp[i][j] += (double)dp[i + 1][j + 1] * (n - i) * (s - j) / (double)(n * s);
dp[i][j] += 1.0;
dp[i][j] /= (1 - (double) (i * j) / (n * s));
}
}
printf("%.4f\n",dp[0][0]);
}
return 0;
}
这题也可以正推(但没必要),令dp[i][j]表示凑齐n种,s个子系统还差,i种j个子系统,决策转移方式类似。
题目2:HDU4405:
题意:在一个1×n的格子上掷色子,从0点出发,掷了多少前进几步,同时有些格点直接相连,即若a,b相连,当落到a点时直接飞向b点。求走到n或超出n期望掷色子次数
(摘自 https://blog.csdn.net/super_rudy/article/details/50577375)
解法:定义dp[i]为当前在i这个位置,走到n需要掷色子的期望次数,转移方程很简单,掷一次色子有6种可能,概率均为1/6,用全期望公式(或子概率加权和)再加上1(掷了一次色子的次数代价),可以得到dp[i]的期望。对于直接相连的点,状态可以直接转移,因为可以直接跳过去。
这题很好的体现为什么要逆推,通常我们设计出的状态都比较适合逆推,因为最终状态(dp[n])的值是已知的,而其他状态的值是未知的。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int n,m;
double dp[maxn];
int t[maxn];
int main() {
while(scanf("%d%d",&n,&m)) {
if(!n && !m) break;
memset(t,0,sizeof(t));
for(int i = 1; i <= m; i++) {
int x,y;
scanf("%d%d",&x,&y);
t[x] = y;
}
dp[n] = 0;
for(int i = n - 1; i >= 0; i--) {
dp[i] = 1;
if(t[i] != 0) dp[i] = dp[t[i]];
else {
for(int j = 1; j <= 6; j++) {
if(i + j > n) continue;
dp[i] += dp[i + j] / 6.0;
}
}
}
printf("%.4lf\n",dp[0]);
}
return 0;
}
题目3:https://cometoj.com/contest/37/problem/C?redirect=%2Fcontest%2F37%2Fproblem%2FC
题目大意:有一个n个点的无向完全图G,删掉一条边的概率是 x / y,定义G的独立集S:S中任意两点a,b不存在边连接a,b。问独立集的个数的期望。
分析:i个点的独立集,要删除 i * (i - 1)条边,概率为pow(x / y,i * (i - 1)),
i个点的独立集有c[n][i]个,可以计算得到大小为i个点的独立集的期望个数:c[n][i] * pow(x / y,i * (i - 1))。
枚举i,每一个得到i个点的独立集的期望个数相加就是最后的答案。注意这里要取模求逆元。
c[n][i] 数组开不下,可以去掉一维,用递推的方式求出所有的c[i]。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 10;
const ll mod = 998244353;
long long x,y,n;
long long fpow(long long a,long long b) {
long long res = 1;
while(b) {
if(b & 1) res = (res % mod) * (a % mod) % mod;
a = (a % mod) * (a % mod) % mod;
b >>= 1;
}
return res;
}
long long ex_gcd(long long a,long long b,long long &x,long long &y) {
if(!b) {
x = 1;
y = 0;
return a;
}
long long g = ex_gcd(b,a % b,y,x);
y -= x * (a / b);
return g;
}
ll c[maxn];
int main() {
scanf("%lld%lld%lld",&n,&x,&y);
c[0] = 1;
for(ll i = 1; i <= n; i++) {
c[i] = (((c[i - 1] % mod) * (n - i + 1) % mod) * fpow(i,mod - 2)) % mod;
}
long long tot = n * (n - 1) / 2;
long long res = 0;
for(long long i = 0; i <= n; i++) {
long long a,b;
long long tmp = i * (i - 1) / 2;
long long tx = fpow(x,tmp);
long long ty = fpow(y,tmp);
long long g = ex_gcd(tx,ty,a,b);
tx /= g;ty /= g;
ex_gcd(ty,mod,a,b);
a = ((a + mod) % mod) * (tx % mod) % mod;
a = (a * c[i]) % mod;
res = (res + a) % mod;
}
printf("%lld\n",res);
return 0;
}
题目4:ZOJ 3640 : Help Me Escape
题目大意:给你一个初始值f,有n个洞口,你每次被随机分到这n个洞口的其中一个,如果你的f>c[i](洞的防御力),那么就可以跳出了,需要的天数是p*c[i]*c[i](p是题目给的一个算式),如果f<=c[i],那么他的攻击力变为f+c[i],然后又随机到一个洞口,天数加1.求最后出去天数的期望值。
(以上文字来自:https://blog.csdn.net/coraline_m/article/details/26285891)
解法:简单分析一下:对于当前攻击力的状态,对于一个随机的洞口:打得过你就跑出去了,打不过你的战斗力会上涨,你的下一个状态只可能是这两种之一,根据全期望可以列出当前攻击力能逃出去的期望转移式子,而第二种是一个相同的子问题,因此可以想到用攻击力作状态写期望dp。
初始化dp[i] = 0;
转移式子:因为遇到每个洞口的概率都是 1 / n,可以枚举洞口。
如果当前攻击力大于这个洞口防御力,则能逃出去,dp[i] += f[i]。
否则你的战斗力会增长c[i],并且你需要再过一天才能再次随机挑战,dp[i] += dp[i + c[i]] + 1。
最后,dp[i] / n,因为每种洞口遇到的概率都是1 / n。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e4 + 10;
int n,f;
double dp[maxn];
int c[maxn];
int cal(int ci) {
return (int)((1 + sqrt(5)) * ci * ci / 2);
}
int main() {
while(~scanf("%d%d",&n,&f)) {
for(int i = 1; i <= n; i++) {
scanf("%d",&c[i]);
}
for(int i = maxn - 1; i >= 0; i--) {
dp[i] = 0;
for(int j = 1; j <= n; j++) {
if(i > c[j]) {
dp[i] += cal(c[j]);
}
else {
dp[i] += dp[i + c[j]] + 1;
}
}
dp[i] /= n;
}
printf("%.3lf\n",dp[f]);
}
return 0;
}
题目5:ZOJ 3551 :Bloodsucker
题目大意:有n个人,其中1个是吸血鬼,n - 1个普通人,每天有两个人会相遇,如果是吸血鬼和人相遇,则有p的概率将这个人变成吸血鬼,问把所有人都变成吸血鬼的期望天数是几天。
解法:以吸血鬼的数量为当前状态,如果是吸血鬼和人相遇,有一定概率会多一个吸血鬼,其他情况吸血鬼数量都不会变,可以以吸血鬼数量的状态来期望dp。dp[i]表示有i个吸血鬼要使得全部人都变成吸血鬼的期望天数,显然dp[n] = 0;
对于当前有i个吸血鬼的状态转移:
算出吸血鬼和人相遇的概率 tp,则多一个吸血鬼的概率为 tp * p,其他情况吸血鬼的数量不变。
则转移方程为:dp[i] = dp[i + 1] * tp * p + dp[i] * (1 - tp * p) + 1。
发现两边都有dp[i] ,可以移项, 化简得到dp[i] = (1 + tmp * dp[i + 1]) / tmp。
采用逆推,边界状态为dp[n] = 0;
其中tp = i * (n - i) / c[n][2];
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 10;
int t,n;
double pt;
double dp[maxn];
long long c[maxn];
int main() {
for(long long i = 1; i < maxn; i++) {
if(i >= 2)
c[i] = i * (i - 1) / 2;
else c[i] = 0;
}
scanf("%d",&t);
while(t--) {
scanf("%d%lf",&n,&pt);
dp[n] = 0;
for(long long i = n - 1; i >= 1; i--) {
dp[i] = 0;
double tmp = ((double) i * (n - i) / c[n]) * pt;
dp[i] = (1 + tmp * dp[i + 1]) / tmp;
}
printf("%.3lf\n",dp[1]);
}
return 0;
}