期望与概率 dp
期望与概率 dp
\(\text{By DaiRuiChen007}\)
I. [洛谷4316] - 绿豆蛙的归宿
思路分析
DAG 上做期望 dp,可以爆搜,也可以拓扑排序的时候转移,不过还是爆搜比较简洁
记录一下走到当前节点的步数,到终点返回步数,每一步乘上走这种选择的概率求和即可
时间复杂度 \(\Theta(n)\)
总结:
对于一般的概率/期望 dp,比较常见的做法是记搜求解,或者确定终止条件倒推,有时候也可以使用刷表法正推做
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1;
int n,m;
struct node {
int des,val;
};
vector <node> edge[MAXN];
inline double dfs(int p,int d) {
if(p==n) return d;
double ret=0;
for(auto e:edge[p]) ret+=dfs(e.des,d+e.val)/((int)edge[p].size());
return ret;
}
signed main() {
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;++i) {
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
edge[u].push_back((node){v,w});
}
printf("%.2lf\n",dfs(1,0));
return 0;
}
II. [洛谷4550] - 收集邮票
思路分析
假设 \(x\) 为集齐邮票的步数,那么本题所求答案等价于:
\text{Answer}
&=E\left(\sum\limits_{i=1}^x i\right)\\
&=E\left(\dfrac{x(x+1)}2\right)\\
&=\dfrac 12(E(x)+E(x^2))
\end{aligned}
\]
注意答案为什么不是 \(\dfrac{E(x)E(x+1)}2\),因为相关期望不具有可乘性
所以分别 dp 转移 \(E(x)\) 和 \(E(x^2)\)
设 \(F_i\) 为集齐已经拥有 \(i\) 张邮票,集齐 \(n\) 张邮票还需要的步数
设 \(dp_{i,0}\) 表示已经拥有 \(i\) 张邮票,直到买完 \(n\) 张邮票购买邮票张数的期望,即 \(E(F_i)\),显然 \(dp_{n,0}=0\),枚举这一次选择买到的是否是之前已经获得过的,有:
\]
由定义可知,\(E(F_i+1)=E(F_i)+1=dp_{i,0}+1\) 移项可得:
dp_{i,0}&=\dfrac in\times (dp_{i,0}+1)+\dfrac{n-i}ndp_{i+1,0}\\
dp_{i,0}&=dp_{i+1,0}+\dfrac n{n-i}
\end{aligned}
\]
设 \(dp_{i,1}\) 表示已经拥有 \(i\) 张邮票,直到买完 \(n\) 张邮票的购买邮票张数的平方的期望,即 \(E(F_i^2)\),显然 \(dp_{n,0}=0\),枚举这一次选择买到的是否是之前已经获得过的,有:
\]
通过定义二项式定理和 \(dp\) 的定义爆拆 \(E\left((f_i+1)^2\right)\)
E\left((F_i+1)^2\right)
&=E(F_i^2+2F_i+1)\\
&=E(F_i^2)+2E(F_i)+1\\
&=dp_{i,1}+2dp_{i,0}+1
\end{aligned}
\]
将 \(E\left((F_i+1^2\right)=dp_{i,1}+2dp_{i,0}+1\) 带入并且移项,有:
dp_{i,1}&=\dfrac in(dp_{i,1}+2dp_{i,0}+1)+\dfrac{n-i}n(dp_{i+1,1}+2dp_{i+1,0}+1)\\
dp_{i,1}&=\dfrac {2i}{n-i}dp_{i,0}+2dp_{i+1,0}+dp_{i+1,1}+\dfrac n{n-i}
\end{aligned}
\]
答案为 \(\dfrac{dp_{1,0}+dp_{1,1}}2\)
暴力转移,时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define div division
using namespace std;
const int MAXN=1e4+1;
double dp[MAXN][2];
inline double div(int x,int y) {
return (double)x/(double)y;
}
signed main() {
int n;
scanf("%d",&n);
dp[n][0]=dp[n][1]=0;
for(int i=n-1;i>=0;--i) {
dp[i][0]=dp[i+1][0]+div(n,n-i);
dp[i][1]=2*div(i,n-i)*dp[i][0]+2*dp[i+1][0]+dp[i+1][1]+div(n,n-i);
}
printf("%.2lf\n",(dp[0][0]+dp[0][1])/2);
return 0;
}
另解思路
设 \(dp_{i,0}\) 表示已经拥有 \(i\) 张邮票,直到买完 \(n\) 张邮票的期望购买邮票张数,显然 \(dp_{n,0}=0\),枚举这一次选择买到的是否是之前已经获得过的,有:
\]
移项可得:
\]
设 \(dp_{i,1}\) 表示已经拥有(白捡)\(i\) 张邮票,直到买完 \(n\) 张邮票的期望购买花费,显然 \(dp_{n,1}=0\),考虑这一次选择的是否是之前已经有过的
从某个状态开始转移倒退,假设从这个状态开始直到完成的期望步数是 \(E(c)\),那么在这个基础上,求出在这之前再做一次选择的价格 \(c'\) 的期望 \(E(c')\),我们可以假设这一次选择是在期望 \(c\) 次选择之后做的,所以 \(E(c')\) 相当于 \(E(c+1)=E(c)+1\),每一步对本次价格 \(c'\) 的贡献都是 \(1\),整合起来恰好就是 \(E(c)\),所以有:
\]
移项可得:
\]
直接转移,时间复杂度 \(\Theta(n)\)
这种做法的递推式也可以用严谨的级数求和来证明,但我不会。。。
总结:
另解考虑用总步数的期望去计算单次代价的期望,高妙 trick。。。
注意,本题虽然可以设 \(x\) 为集齐邮票的期望步数,但是 \(E\left(\sum\limits_{i=1}^x i\right)\neq \dfrac12E(x+1)\times E(x)\),因为相关期望不具有可乘性,所以这种想法不可行
当然,直接将 \(E\left(\sum\limits_{i=1}^x i\right)\) 拆成 \(\dfrac12(E(x^2)+E(x))\) 的想法似乎更平凡,更可做些
另解代码
#include<bits/stdc++.h>
#define div division
using namespace std;
const int MAXN=1e4+1;
double dp[MAXN][2];
inline double div(int x,int y) {
return (double)x/(double)y;
}
signed main() {
int n;
scanf("%d",&n);
dp[n][0]=dp[n][1]=0;
for(int i=n-1;i>=0;--i) {
dp[i][0]=dp[i+1][0]+div(n,n-i);
dp[i][1]=div(i,n-i)*dp[i][0]+dp[i+1][0]+dp[i+1][1]+div(n,n-i);
}
printf("%.2lf\n",dp[0][1]);
return 0;
}
III. [洛谷4819] - 杀人游戏
思路分析
将每个人向所有他知道身份的人连一条边
如果询问了某一个人,那么能够被他到达的点都可以确定身份
如果图中有环,只需要选择其中的一个点,这一步和直接将环缩成一个点没有区别
所以考虑用 tarjan 将原图缩成 DAG,然后在每个入度为 \(0\) 的强连通分量中选一个人问,假设这样的强连通分量共有 \(x\) 个,则答案为 \(1-\dfrac xn\)
不过,有时候可能会剩下最后一个人不知道身份,那么这个人一定是杀手,所以我们可以考虑这样一种情况:
- 某一个强联通分量的大小为 \(1\)
- 这个强联通分量删去后没有增加其他入度为 \(0\) 的强连通分量
那么这个强连通分量可以不选人询问,最后排除法一下就知道这个人的身份了
注意这样的人最多只有一个,此时答案为 \(1-\dfrac{x-1}n\)
注意建新图的时候不要连重边,可以拿个 map
记录一下
时间复杂度 \(\Theta(m\log m)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
int low[MAXN],dfn[MAXN],dfncnt,sk[MAXN],top;
bool ins[MAXN];
int scc,bel[MAXN],deg[MAXN],siz[MAXN];
vector <int> edge[MAXN],g[MAXN];
map <pair<int,int>,bool> vis;
inline void tarjan(int p) {
low[p]=dfn[p]=++dfncnt;
sk[++top]=p;
ins[p]=true;
for(int v:edge[p]) {
if(!dfn[v]) {
tarjan(v);
low[p]=min(low[p],low[v]);
} else if(ins[v]) low[p]=min(low[p],low[v]);
}
if(low[p]==dfn[p]) {
++scc;
int k;
do {
k=sk[top--];
ins[k]=false;
bel[k]=scc;
++siz[scc];
} while(k!=p);
}
}
signed main() {
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i) {
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
}
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
for(int u=1;u<=n;++u) {
for(int v:edge[u]) {
if(bel[u]==bel[v]||vis[make_pair(bel[u],bel[v])]) continue;
g[bel[u]].push_back(bel[v]);
++deg[bel[v]];
vis[make_pair(bel[u],bel[v])]=true;
}
}
bool use=false;
int res=0;
for(int i=1;i<=scc;++i) {
if(deg[i]) continue;
++res;
if(siz[i]>1||use) continue;
bool ok=true;
for(int x:g[i]) {
if(deg[x]==1) {
ok=false;
break;
}
}
if(ok) --res,use=true;
}
printf("%.6lf\n",(double)(n-res)/(double)n);
return 0;
}
IV. [BZOJ3029] - 守卫者的挑战
思路分析
大力概率 dp,设 \(dp_{i,j,k}\) 表示考虑前 \(i\) 场战斗,胜利 \(j\) 场,背包容量为 \(k\) 的概率
边界条件为:\(dp_{0,0,K}=1\)
注意到 \(k\ge n\) 就一定满足题意,所以计算背包容量的时候不需要考虑超过 \(n\) 的情况,把背包容量当成 \(n\) 就行
然后大力刷表即可转移:
dp_{i+1,j+1,k+a_i}&\gets dp_{i,j,k}\times p_{i+1}\%\\
dp_{i+1,j,k}&\gets dp_{i,j,k}\times\left(1-p_{i+1}\%\right)
\end{aligned}
\]
统计答案为:
\]
本题卡空间,滚动数组压掉第一维,空间复杂度 \(\Theta(n^2)\)
大力转移即可,时间复杂度 \(\Theta(n^3)\)
代码呈现
#include<bits/stdc++.h>
#define double long double
using namespace std;
const int MAXN=201;
double p[MAXN],dp[2][MAXN][MAXN<<1];
int a[MAXN],n,l,k;
inline int f(int x) {
return min(n,x)+n;
}
signed main() {
scanf("%d%d%d",&n,&l,&k);
for(int i=1;i<=n;++i) scanf("%Lf",&p[i]),p[i]/=100;
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
dp[0][0][f(k)]=1;
for(int i=0;i<n;++i) {
int cur=i&1;
for(int j=0;j<=n;++j) {
for(int k=-n;k<=n;++k) {
if(!dp[cur][j][f(k)]) continue;
dp[cur^1][j+1][f(k+a[i+1])]+=p[i+1]*dp[cur][j][f(k)];
dp[cur^1][j][f(k)]+=(1-p[i+1])*dp[cur][j][f(k)];
}
}
memset(dp[cur],0,sizeof(dp[cur]));
}
double res=0;
for(int i=l;i<=n;++i) {
for(int j=0;j<=n;++j) {
res+=dp[n&1][i][f(j)];
}
}
printf("%.6Lf\n",res);
return 0;
}
V. [AcWing218] - 扑克牌
思路分析
大力记搜,记录当前四种花色的牌出现的次数,以及大王小王用来当什么了(没用则是 \(0\)),每次考虑大王小王的花色时,在可能的四种情况中取个最小值即可
状态总数 \(S\) 大约是 \(13^4\times5^2\)
时间复杂度 \(\Theta(S)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
double dp[14][14][14][14][5][5];
bool vis[14][14][14][14][5][5];
int A,B,C,D;
inline double dfs(int a,int b,int c,int d,int x,int y) {
if(vis[a][b][c][d][x][y]) return dp[a][b][c][d][x][y];
int tot=a+b+c+d+(x?1:0)+(y?1:0);
if(a+(x==1)+(y==1)>=A&&b+(x==2)+(y==2)>=B&&c+(x==3)+(y==3)>=C&&d+(x==4)+(y==4)>=D) {
vis[a][b][c][d][x][y]=true;
return dp[a][b][c][d][x][y]=(double)tot;
}
double ans=0;
if(a!=13) ans+=dfs(a+1,b,c,d,x,y)*(13-a)/(54-tot);
if(b!=13) ans+=dfs(a,b+1,c,d,x,y)*(13-b)/(54-tot);
if(c!=13) ans+=dfs(a,b,c+1,d,x,y)*(13-c)/(54-tot);
if(d!=13) ans+=dfs(a,b,c,d+1,x,y)*(13-d)/(54-tot);
if(!x) {
double ret=INF;
for(int i:{1,2,3,4}) {
double s=dfs(a,b,c,d,i,y);
if(s) ret=min(ret,s);
}
ans+=ret/(54-tot);
}
if(!y) {
double ret=INF;
for(int i:{1,2,3,4}) {
double s=dfs(a,b,c,d,x,i);
if(s) ret=min(ret,s);
}
ans+=ret/(54-tot);
}
vis[a][b][c][d][x][y]=true;
return dp[a][b][c][d][x][y]=ans;
}
signed main() {
scanf("%d%d%d%d",&A,&B,&C,&D);
if(max(A-13,0)+max(B-13,0)+max(C-13,0)+max(D-13,0)>2) puts("-1.000");
else printf("%.3lf\n",dfs(0,0,0,0,0,0));
return 0;
}
VI. [BZOJ4374] - Little Elephant and Boxes
思路分析
数据范围显然提示我们用折半搜索
我们可以固定钻石的条件,将对应状态下使用的金钱进行计算
更准确地讲,分为以下步骤:
- 01 背包预处理,用 \(dp_{i,j}\) 表示用 \(i\) 颗钻石购买 \(j\) 件物品所需要最少的金钱
- 折半搜索,分别记录下前一半和后一半中:在获得 \(i\) 个钻石的情况下,获得了多少金钱以及这种概况对应的概率
- 枚举购买的物品数量,以及花费了多少钻石去买这些物品,同时更新出购买这么多件物品至少至多要花费多少钱
- 枚举前一半和后一半分别贡献了多少钻石,以及前一半中贡献了多少金钱,以及其对应概率
- 根据刚才所计算出来的最多和最少花费,得到后一半最多最少分别要贡献多少金钱,计算出此时后一半有多少概率恰好获得的金钱在要求的范围(使得恰好买了那么多件物品)
- 将其前一半的概率和后一半的概率以及获得的物品总数相乘,统计贡献
注意到后一半的概率可以用前缀快速计算
时间复杂度 \(\Theta\left(mn\log (\frac n2!)\times2^\frac n2\right)\)
代码呈现
#include<bits/stdc++.h>
#define pid pair <int,double>
using namespace std;
const int MAXN=31,INF=INT_MAX;
int v[MAXN],p[MAXN],c[MAXN],d[MAXN],dp[MAXN][MAXN]; //use i diamond, buy j item, minimum cost
vector <pid> L[MAXN],R[MAXN]; //get i diamond, <j money, p probability>
int n,m;
inline void dfs(int item,int di,int co,double pr,bool op) {
if(!op&&item>n/2) {
L[di].push_back(make_pair(co,pr));
return ;
}
if(op&&item>n) {
R[di].push_back(make_pair(co,pr));
return ;
}
if(p[item]!=100) dfs(item+1,di+1,co,pr*(1-p[item]*0.01),op);
if(p[item]!=0) dfs(item+1,di,co+v[item],pr*p[item]*0.01,op);
}
inline void solve() {
scanf("%d%d",&n,&m);
memset(dp,0x3f,sizeof(dp));
for(int i=0;i<=n;++i) L[i].clear(),R[i].clear();
for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&p[i]);
dfs(1,0,0,1,false);
dfs(n/2+1,0,0,1,true);
for(int i=0;i<=n;++i) {
sort(L[i].begin(),L[i].end());
sort(R[i].begin(),R[i].end());
for(int j=1;j<(int)L[i].size();++j) L[i][j].second+=L[i][j-1].second;
for(int j=1;j<(int)R[i].size();++j) R[i][j].second+=R[i][j-1].second;
}
dp[0][0]=0;
for(int i=1;i<=m;++i) {
scanf("%d%d",&c[i],&d[i]);
for(int j=n;j>=d[i];--j) {
for(int k=i;k>=1;--k) {
dp[j][k]=min(dp[j][k],dp[j-d[i]][k-1]+c[i]);
}
}
}
double ans=0;
for(int item=1;item<=m;++item) {
int now=INF,nxt=INF;
for(int di=0;di<=n;++di) {
now=min(dp[di][item],now);
if(item<m) nxt=min(dp[di][item+1],nxt);
for(int xi=0;xi<=di;++xi) {
int yi=di-xi;
for(int i=0;i<(int)L[xi].size();++i) {
int co=L[xi][i].first;
double pl=L[xi][i].second,pr=0;
if(i) pl-=L[xi][i-1].second;
int indxl=(lower_bound(R[yi].begin(),R[yi].end(),make_pair(now-co,(double)0))-R[yi].begin())-1;
int indxr=(lower_bound(R[yi].begin(),R[yi].end(),make_pair(nxt-co,(double)0))-R[yi].begin())-1;
if(indxl>=0) pr-=R[yi][indxl].second;
if(indxr>=0) pr+=R[yi][indxr].second;
if(pr>0) ans+=item*pl*pr;
}
}
}
}
printf("%.4lf\n",ans);
}
signed main() {
int T;
scanf("%d",&T);
while(T--) solve();
return 0;
}
VII. [洛谷6969] - Foreign Postcards
思路分析
考虑简单 dp,首先做一个简单分析:\(f_i\) 表示第 \(i\) 个数作为某一选择中的第一张邮票被拿出来的概率
显然有 \(f_1=1\),对于接下来的数考虑 \(i-1\) 所在的那一段是从哪个点考试被拿出来的,有:
\]
通过观察可以发现 \(f_i=\dfrac 1{n-i+2}\)
然后考虑一个简单 dp,设 \(dp_i\) 表示 \(i\) 被翻转的概率,如果 \(S_i=\texttt C\),则直接有 \(f_i\) 的概率成为段首,不被翻转,当然,\(i\) 有 \((1-f_i)\) 的概率被 \(dp_{i-1}\) 包含,所以有:
\]
然后统计答案的时候计算概率求和即可:
\]
暴力,时间复杂度 \(\Theta(n)\)
代码呈现
#include<bits/stdc++.h>
#define double long double
using namespace std;
const int MAXN=1e6+1;
double dp[MAXN];
char str[MAXN];
signed main() {
scanf("%s",str+1);
int n=strlen(str+1);
for(int i=1;i<=n;++i) {
double f=1.0/(n-i+2);
if(i==1) f=1;
if(str[i]=='C') dp[i]+=f;
dp[i]+=dp[i-1]*(1-f);
}
double res=0;
for(int i=1;i<=n;++i) {
if(str[i]=='C') res+=1-dp[i];
else res+=dp[i];
}
printf("%.10Lf\n",res);
return 0;
}
VIII. [BZOJ2201] - 彩色圆环
思路分析
考虑链的情况,然后再把 \(1\) 所在的那个环算进去组合成一个链
设 \(dp_{i,0/1}\) 表示考虑到第 \(i\) 颗珠子,当前珠子的颜色与第一颗珠子的颜色不相同/相同时的答案期望,边界条件 \(dp_{1,1}=1\)
枚举最后一个段的颜色和起终点有转移:
dp_{j,0}&\gets dp_{i,0}\times(j-i)\times \left(\dfrac 1m\right)^{j-i}\times \dfrac{m-2}{m}\\
dp_{j,0}&\gets dp_{i,1}\times(j-i)\times \left(\dfrac 1m\right)^{j-i}\times \dfrac{m-1}{m}\\
dp_{j,1}&\gets dp_{i,0}\times(j-i)\times \left(\dfrac 1m\right)^{j-i}\times \dfrac{1}{m}\\
\end{aligned}
\]
统计答案的时候,我们可以考虑 \(1\) 所在区间的长度,然后将这个区间缩成一个点扔到剩下的区间里面
\]
注意我们在转移 dp 的时候默认没有其他颜色与第一个相同,故有正确性
时间复杂度 \(\Theta(n^2)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=201;
double dp[MAXN][2],f[MAXN];
signed main() {
int n,m;
scanf("%d%d",&n,&m);
f[1]=1;
for(int i=2;i<=n;++i) f[i]=f[i-1]/m;
dp[1][1]=1;
for(int i=1;i<n;++i) {
for(int j=i+1;j<=n;++j) {
dp[j][0]+=dp[i][0]*f[j-i]*(j-i)*(m-2)/m;
dp[j][0]+=dp[i][1]*f[j-i]*(j-i)*(m-1)/m;
dp[j][1]+=dp[i][0]*f[j-i]*(j-i)/m;
}
}
double res=n*f[n];
for(int i=1;i<n;++i) res+=i*i*dp[n-i+1][0]*f[i];
printf("%.5lf\n",res);
return 0;
}
IX. [BZOJ2720] - 列队春游
思路分析
注意到 \(a_j\ge a_i\) 的 \(j\) 对于每个 \(i\) 都仅有 \(1\) 个
所以只需要考虑对于某个 \(i\),有多少个 \(j\) 满足 \(a_j< a_i\) 且 \(i\) 能看到 \(j\)
假设对于某个 \(i\),所有满足 \(a_j< a_i\) 且 \(j\neq i\) 的 \(j\) 构成集合 \(\mathbf S_i\)
显然,\(\mathbf S_i\) 中的每个数对答案的贡献都是相等的,所以我们只需要考虑其中一个然后 \(\times|\mathbf S_i|\) 就行
假设我们考虑某个 \(x\in \mathbf S_i\) 被 \(i\) 看见的概率,显然 \(\mathbf S_i\) 中的其他数对 \(i\) 看见 \(x\) 并没有任何影响,所以只考虑 \(x\) 和所有 \(\ge a_i\) 的数
首先我们将所有 \(\ge a_i\) 的 \(n-|\mathbf S_i|\) 个数排列(包括 \(a_i\)),此时我们在 \(n-|\mathbf S_i|+1\) 个空中随便找一个空给 \(x\) ,剩下的 \(|\mathbf S_i|\) 个数随便插,注意到 \(x\) 能被 \(i\) 看见,当且仅当 \(x\) 在最开始 \(n-|\mathbf S_i|+1\) 个空中,选择了 \(i\) 前面的那一个, 因此 \(x\) 被 \(i\) 看见的概率是 \(\dfrac{1}{n-|\mathbf S_i|+1}\)
带入答案计算,有:
\text{Answer}
&=n+\sum_{i=1}^n |\mathbf S_i|\times\dfrac 1{n-|\mathbf S_i|+1}\\
&=\sum_{i=1}^n \dfrac{|\mathbf S_i|}{n-|\mathbf S_i|+1}+1\\
&=\sum_{i=1}^n \dfrac{n+1}{n-|\mathbf S_i|+1}
\end{aligned}
\]
问题就转化成了如何求 \(|\mathbf S_i|\),显然直接装桶算即可
时间复杂度 \(\Theta(w)\),\(w\) 为 \(h_i\) 的值域
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1001;
int s[MAXN];
signed main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) {
int h;
scanf("%d",&h);
++s[h];
}
double res=0,tot=0;
for(int i=1;i<MAXN;++i) {
res+=s[i]*(n+1)/(n-tot+1);
tot+=s[i];
}
printf("%.2lf\n",res);
return 0;
}
X. [洛谷4637] - 扫雷机器人
思路分析
非常强的思维题,推理过程相当精彩
考虑求出期望的过程:
- 每种引爆地雷的顺序,共有 \(n!\) 种情况
- 对于每种顺序,计算出其中实际引爆的地雷个数,也就是所有没有在之前被引爆的地雷个数
- 将每种方案中实际引爆的地雷个数当成操作次数,乘上概率 \(\dfrac1{n!}\),求和计算期望
由于期望具有线性性,所以我们可以考虑计算每个地雷对答案的贡献,也就是说,对于每个地雷,求出其会在多少种方案中被实际引爆
求出所有可以导致第 \(i\) 个地雷被引爆(直接或间接)的地雷集合,设为 \(\mathbf S_i\),那么 \(i\) 第一个被引爆,当且仅当所有 \(\mathbf S_i\) 中的地雷都在 \(i\) 之后被引爆,
所以问题就转化成了:对于第 \(i\) 个元素,求出在所有排列中,满足 \(i\) 是 \(\mathbf S_i\) 中所有元素里的第一个的情况总数
由于其他的元素在哪里并不重要,所以我们只需要考虑 \(\mathbf S_i\) 中的元素之间的相对关系,不难得到满足 \(i\) 恰好是其中第一个数的方案数占所有方案数的比例为:\(\dfrac{(|\mathbf S_i|-1)!}{|\mathbf S_i|!}=\dfrac{1}{|\mathbf S_i|}\),因此在所以排列中,第 \(i\) 颗地雷被实际引爆的次数应该是 \(\dfrac{n!}{|\mathbf S_i|}\) 次,这也是第 \(i\) 颗地雷对答案的贡献
套用期望的计算公式可以得到:
\text{Answer}
&=\dfrac{1}{n!}\sum_{i=1}^n\dfrac{n!}{|\mathbf S_i|}\\
&=\sum_{i=1}^n\dfrac1{|\mathbf S_i|}
\end{aligned}
\]
所以问题就转化成求所有的 \(|\mathbf S_i|\)
可以将所有地雷向其能够直接引爆的点连一条边,我们需要求出对于每个点 \(i\),有多少个点能够到达 \(i\)
考虑缩点,在 DAG 上做拓扑排序转移一个用 bitset
维护的 \(\mathbf S_i\) 即可
注意:如果我们直接在原图上做 \(n\) 次 BFS,复杂度在完全图上会退化到 \(\Theta(n^3)\)
时间复杂度 \(\Theta(n^2)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=4001;
int p[MAXN],d[MAXN];
vector <int> edge[MAXN],g[MAXN];
bool vis[MAXN];
bitset <MAXN> f[MAXN];
int low[MAXN],dfn[MAXN],dfncnt,sk[MAXN],siz[MAXN],bel[MAXN],deg[MAXN],top,scc;
bool ins[MAXN],link[MAXN][MAXN];
inline void tarjan(int p) {
low[p]=dfn[p]=++dfncnt;
ins[p]=true; sk[++top]=p;
for(int v:edge[p]) {
if(!dfn[v]) {
tarjan(v);
low[p]=min(low[p],low[v]);
} else if(ins[v]) low[p]=min(low[p],low[v]);
}
if(low[p]==dfn[p]) {
++scc;
int k;
do {
k=sk[top--];
bel[k]=scc;
f[scc].set(k);
++siz[scc];
ins[k]=false;
} while(k!=p);
}
}
signed main() {
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d",&p[i],&d[i]);
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) {
if(abs(p[i]-p[j])<=d[i]) {
edge[i].push_back(j);
}
}
}
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;++i) {
for(int j:edge[i]) {
if(bel[i]==bel[j]||link[bel[i]][bel[j]]) continue;
g[bel[i]].push_back(bel[j]);
link[bel[i]][bel[j]]=true;
++deg[bel[j]];
}
}
queue <int> q;
for(int i=1;i<=scc;++i) {
if(!deg[i]) {
q.push(i);
}
}
while(!q.empty()) {
int p=q.front();
q.pop();
for(int v:g[p]) {
f[v]|=f[p];
--deg[v];
if(!deg[v]) q.push(v);
}
}
double res=0;
for(int i=1;i<=n;++i) res+=1.0/((int)f[bel[i]].count());
printf("%.4lf\n",res);
return 0;
}
XI. [洛谷4206] - 聪聪与可可
思路分析
设 \(dp_{x,y}\) 表示聪聪在 \(x\),可可在 \(y\) 时的答案,显然用记搜解决
首先计算出 \(x\) 的移动轨迹,然后对于 \(y\) 的每一种决策都枚举一下即可
以此我们需要预处理出 \(mov_{x,y}\) 表示聪聪在 \(x\),可可在 \(y\) 时聪聪的下一步决策,做 \(n\) 次 BFS 预处理出距离然后直接计算即可
时间复杂度 \(\Theta(n(n+e))\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1001;
vector <int> edge[MAXN];
int dis[MAXN][MAXN],mov[MAXN][MAXN];
double dp[MAXN][MAXN];
bool vis[MAXN];
inline void BFS(int s) {
memset(vis,false,sizeof(vis));
dis[s][s]=0;
vis[s]=true;
queue <int> q;
q.push(s);
while(!q.empty()) {
int p=q.front();
q.pop();
for(int v:edge[p]) {
if(vis[v]) continue;
vis[v]=true;
dis[v][s]=dis[p][s]+1;
q.push(v);
}
}
}
inline double dfs(int c,int m) {
if(c==m) return 0;
int st1=mov[c][m],st2=mov[st1][m];
if(m==st1||m==st2) return 1;
if(dp[c][m]) return dp[c][m];
int siz=edge[m].size();
double ret=(dfs(st2,m)+1)/(1+siz);
for(int i:edge[m]) ret+=(dfs(st2,i)+1)/(1+siz);
return dp[c][m]=ret;
}
signed main() {
int n,m,s,t;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;++i) {
int u,v;
scanf("%d%d",&u,&v);
edge[u].push_back(v);
edge[v].push_back(u);
}
for(int i=1;i<=n;++i) BFS(i);
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) {
mov[i][j]=-1;
for(int k:edge[i]) {
if(mov[i][j]==-1||dis[k][j]<dis[mov[i][j]][j]||(dis[k][j]==dis[mov[i][j]][j]&&k<mov[i][j])) {
mov[i][j]=k;
}
}
}
}
printf("%.3lf\n",dfs(s,t));
return 0;
}
XII. [洛谷2221] - 高速公路
思路分析
查询 \([l,r]\) 中的答案即所有区间中的路径长度和除以路径数量,考虑计算每条路径的计算次数,我们得到:
\]
将 \((i,i+1)\) 的边权转化为点权 \(a_i\) 以处理区间加,对答案稍作转化:
\text{Answer}[l,r]
&=\sum_{i=l}^{r-1} a_i\times(i-l+1)\times(r-i)\div\dbinom {r-l+1}2\\[2ex]
&=\sum_{i=l}^{r-1} a_i\times[i\times(l+r-1)-(l-1)\times r-i^2]\div\dbinom{r-l+1}2\\[2ex]
&=\left[-(l-r)\times r\times \sum_{i=l}^{r-1} a_i+(r+l-1)\sum_{i=l}^{r-1} i\times a_i-\sum_{i=l}^{r-1}i^2\times a_i\right]\div\dbinom{r-l+1}2
\end{aligned}
\]
因此维护 \(\sum a_i,\sum i\times a_i,\sum i^2\times a_i\) 即可,注意到 \(\sum i,\sum i^2\) 可以用数学方法快速计算,因此可以用线段树维护
时间复杂度 \(\Theta(m\log n)\)
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1;
int n,m;
struct node {
int sum0,sum1,sum2,tag;
node() { sum0=sum1=sum2=tag=0; }
inline friend node operator +(const node &x,const node &y) {
node z;
z.sum0=x.sum0+y.sum0;
z.sum1=x.sum1+y.sum1;
z.sum2=x.sum2+y.sum2;
return z;
}
};
class SegmentTree {
private:
node tree[MAXN<<2];
inline int left(int x) { return x<<1; }
inline int right(int x) { return x<<1|1; }
inline int prefix0(int x) { return x; }
inline int prefix1(int x) { return x*(x+1)/2; }
inline int prefix2(int x) { return x*(x+1)*(2*x+1)/6; }
inline int factor0(int l,int r) { return prefix0(r)-prefix0(l-1); }
inline int factor1(int l,int r) { return prefix1(r)-prefix1(l-1); }
inline int factor2(int l,int r) { return prefix2(r)-prefix2(l-1); }
inline void pushup(int pos) { tree[pos]=tree[left(pos)]+tree[right(pos)]; }
inline void addtag(int l,int r,int pos,int v) {
tree[pos].tag+=v;
tree[pos].sum0+=factor0(l,r)*v;
tree[pos].sum1+=factor1(l,r)*v;
tree[pos].sum2+=factor2(l,r)*v;
}
inline void pushdown(int l,int r,int pos) {
if(!tree[pos].tag) return ;
int mid=(l+r)>>1;
addtag(l,mid,left(pos),tree[pos].tag);
addtag(mid+1,r,right(pos),tree[pos].tag);
tree[pos].tag=0;
}
public:
inline void Modify(int ul,int ur,int k,int l=1,int r=n,int pos=1) {
if(ul<=l&&r<=ur) {
addtag(l,r,pos,k);
return ;
}
pushdown(l,r,pos);
int mid=(l+r)>>1;
if(ul<=mid) Modify(ul,ur,k,l,mid,left(pos));
if(mid<ur) Modify(ul,ur,k,mid+1,r,right(pos));
pushup(pos);
}
inline node Query(int ql,int qr,int l=1,int r=n,int pos=1) {
if(ql<=l&&r<=qr) return tree[pos];
pushdown(l,r,pos);
int mid=(l+r)>>1;
if(qr<=mid) return Query(ql,qr,l,mid,left(pos));
if(mid<ql) return Query(ql,qr,mid+1,r,right(pos));
return Query(ql,qr,l,mid,left(pos))+Query(ql,qr,mid+1,r,right(pos));
}
} S;
inline void reduct(int &x,int &y) {
int z=__gcd(x,y);
x/=z,y/=z;
}
signed main() {
cin>>n>>m;
while(m--) {
char op;
int l,r;
cin>>op>>l>>r;
if(op=='C') {
int v;
cin>>v;
S.Modify(l,r-1,v);
}
if(op=='Q') {
auto res=S.Query(l,r-1);
int x=(l+r-1)*res.sum1-res.sum2-r*(l-1)*res.sum0;
int y=(r-l+1)*(r-l)/2;
reduct(x,y);
printf("%lld/%lld\n",x,y);
}
}
return 0;
}
XIII. [洛谷7648] - MNOGOMENT
思路分析
设 \(dp_{t,x,y,i}\) 表示当前时间 \(t\) 秒,比分 \(x:y\),球在 \(i\) 手中,每次枚举可能性大力转移即可
时间复杂度 \(\Theta(TR^2n^2)\),需要一定的卡常技巧才能通过本题
代码呈现
TLE on #12,用时 1.06s
#include<bits/stdc++.h>
using namespace std;
const int MAXN=201,MAXT=501;
int siz[MAXN];
vector <int> E[MAXN],F[MAXN];
float p[MAXN],dp[MAXT][MAXN][11][11];
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n,R,T;
cin>>n>>R>>T;
for(int i=1;i<=n;++i) {
int e,f;
cin>>p[i]>>e>>f;
siz[i]=e+f+1;
while(e--) {
int x;
cin>>x;
E[i].push_back(x);
}
while(f--) {
int x;
cin>>x;
F[i].push_back(x+n);
}
}
for(int i=n+1;i<=n+n;++i) {
int e,f;
cin>>p[i]>>e>>f;
siz[i]=e+f+1;
while(e--) {
int x;
cin>>x;
E[i].push_back(x+n);
}
while(f--) {
int x;
cin>>x;
F[i].push_back(x);
}
}
dp[0][1][0][0]=1;
for(int t=0;t<T;++t) {
for(int x=0;x<R;++x) {
for(int y=0;y<R;++y) {
for(int i=1;i<=n;++i) {
for(int j:E[i]) dp[t+1][j][x][y]+=dp[t][i][x][y]/siz[i];
for(int j:F[i]) dp[t+1][j][x][y]+=dp[t][i][x][y]/siz[i];
dp[t+1][n+1][x+1][y]+=dp[t][i][x][y]/siz[i]*p[i];
dp[t+1][n+1][x][y]+=dp[t][i][x][y]/siz[i]*(1-p[i]);
}
for(int i=n+1;i<=n+n;++i) {
for(int j:E[i]) dp[t+1][j][x][y]+=dp[t][i][x][y]/siz[i];
for(int j:F[i]) dp[t+1][j][x][y]+=dp[t][i][x][y]/siz[i];
dp[t+1][1][x][y+1]+=dp[t][i][x][y]/siz[i]*p[i];
dp[t+1][1][x][y]+=dp[t][i][x][y]/siz[i]*(1-p[i]);
}
}
}
}
for(int x=0;x<=R;++x) {
for(int y=0;y<=R;++y) {
if(x==R&&y==R) continue;
double ans=0;
if(x==R||y==R) {
for(int t=1;t<=T;++t) {
for(int i=1;i<=2*n;++i) {
ans+=dp[t][i][x][y];
}
}
} else for(int i=1;i<=2*n;++i) ans+=dp[T][i][x][y];
cout<<fixed<<setprecision(6)<<ans<<'\n';
}
}
return 0;
}
另解思路
考虑优化转移幅度,以进球为标志,每次转移多个事件
我们发现进球数量和时间不匹配,所以我们可以只考虑每次进球发生的时候
我们可以预处理出 \(i\) 队持球,\(t\) 秒之后 \(j\) 队进球的概率
设 \(dp_{t,x,y,i}\) 表示 \(t\) 秒之后,比分 \(x:y\) 且球在 \(i\) 队一号手上(对面刚刚进球)
枚举下一次进球的时间即可转移
注意,有可能从某个时刻开始没有进球,此时要直接把 \(dp_{t,x,y,i}\) 堆到 \(ans[x:y]\) 中
因此我们需要预处理 \(i\) 队持球时 \(t\) 秒没有进球的情况,直接用刚刚预处理出的概率累加然后做前缀和即可
时间复杂度 \(\Theta(Tn^2+T^2R^2)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=205,MAXT=505;
int siz[MAXN];
vector <int> E[MAXN],F[MAXN];
double p[MAXN];
double hold[MAXT][MAXN]; // t time i player hold the ball (without any score)
double score[2][2][MAXT]; // team i hold the ball, team j score at moment t
double s[2][MAXT]; // team i hold the ball, someone score before moment t
double dp[MAXT][11][11][2]; // time t, score x:y, ball give to team i
double ans[11][11];
signed main() {
int n,R,T;
scanf("%d%d%d",&n,&R,&T);
for(int i=1;i<=n;++i) {
int e,f;
scanf("%lf%d%d",&p[i],&e,&f);
siz[i]=e+f+1;
while(e--) {
int x;
scanf("%d",&x);
E[i].push_back(x);
}
while(f--) {
int x;
scanf("%d",&x);
F[i].push_back(x+n);
}
}
for(int i=n+1;i<=n+n;++i) {
int e,f;
scanf("%lf%d%d",&p[i],&e,&f);
siz[i]=e+f+1;
while(e--) {
int x;
scanf("%d",&x);
E[i].push_back(x+n);
}
while(f--) {
int x;
scanf("%d",&x);
F[i].push_back(x);
}
}
for(int team:{0,1}) {
memset(hold,0,sizeof(hold));
if(team) hold[0][n+1]=1;
else hold[0][1]=1;
for(int t=0;t<=T;++t) {
for(int i=1;i<=n;++i) {
for(int j:E[i]) hold[t+1][j]+=hold[t][i]/siz[i];
for(int j:F[i]) hold[t+1][j]+=hold[t][i]/siz[i];
hold[t+1][n+1]+=hold[t][i]*(1-p[i])/siz[i];
score[team][0][t+1]+=hold[t][i]*p[i]/siz[i];
}
for(int i=n+1;i<=n+n;++i) {
for(int j:E[i]) hold[t+1][j]+=hold[t][i]/siz[i];
for(int j:F[i]) hold[t+1][j]+=hold[t][i]/siz[i];
hold[t+1][1]+=hold[t][i]*(1-p[i])/siz[i];
score[team][1][t+1]+=hold[t][i]*p[i]/siz[i];
}
s[team][t]=score[team][0][t]+score[team][1][t];
if(t) s[team][t]+=s[team][t-1];
}
}
dp[0][0][0][0]=1;
for(int t=0;t<=T;++t) {
for(int x=0;x<=R;++x) {
for(int y=0;y<=R;++y) {
for(int team:{0,1}) {
if(x==R||y==R||t==T) {
ans[x][y]+=dp[t][x][y][team];
continue;
}
ans[x][y]+=dp[t][x][y][team]*(1-s[team][T-t]);
for(int k=1;k+t<=T;++k) {
dp[k+t][x][y+1][0]+=dp[t][x][y][team]*score[team][1][k];
dp[k+t][x+1][y][1]+=dp[t][x][y][team]*score[team][0][k];
}
}
}
}
}
for(int x=0;x<=R;++x) {
for(int y=0;y<=R;++y) {
if(x==R&&y==R) continue;
printf("%.10lf\n",ans[x][y]);
}
}
return 0;
}
期望与概率 dp的更多相关文章
- 【整理】简单的数学期望和概率DP
数学期望 P=Σ每一种状态*对应的概率. 因为不可能枚举完所有的状态,有时也不可能枚举完,比如抛硬币,有可能一直是正面,etc.在没有接触数学期望时看到数学期望的题可能会觉得很阔怕(因为我高中就是这么 ...
- POJ2096Collecting Bugs(数学期望,概率DP)
问题: Ivan is fond of collecting. Unlike other people who collect post stamps, coins or other material ...
- 期望与概率dp
概率与期望dp 定义: 概率:事件A发生的可能性,计作P(A) 期望:事件A结果的平均大小,记住E(x) E(x)=每种结果的大小与其概率的乘积的和 注意计算概率时需要考虑是否要用容斥原理 期望d ...
- 动态规划之经典数学期望和概率DP
起因:在一场训练赛上.有这么一题没做出来. 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6829 题目大意:有三个人,他们分别有\(X,Y,Z\)块钱 ...
- 数学期望和概率DP题目泛做(为了对应AD的课件)
题1: Uva 1636 Headshot 题目大意: 给出一个000111序列,注意实际上是环状的.问是0出现的概率大,还是当前是0,下一个还是0的概率大. 问题比较简单,注意比较大小: A/C & ...
- [转]概率DP总结 by kuangbin
概率类题目一直比较弱,准备把kuangbin大师傅总结的这篇题刷一下! 我把下面的代码换成了自己的代码! 原文地址:http://www.cnblogs.com/kuangbin/archive/20 ...
- LOOPS 概率dp
题意:迷宫是一个R*C的布局,每个格子中给出停留在原地,往右走一个,往下走一格的概率,起点在(1,1),终点在(R,C),每走一格消耗两点能量,求出最后所需要的能量期望 简单概率dp 注意 原地不 ...
- BZOJ 1415 [NOI2005]聪聪与可可 (概率DP+dfs)
题目大意:给你一个无向联通图,节点数n<=1000.聪聪有一个机器人从C点出发向在M点的可可移动,去追赶并吃掉可可,在单位时间内,机器人会先朝离可可最近的节点移动1步,如果移动一步机器人并不能吃 ...
- poj 2096 Collecting Bugs (概率dp 天数期望)
题目链接 题意: 一个人受雇于某公司要找出某个软件的bugs和subcomponents,这个软件一共有n个bugs和s个subcomponents,每次他都能同时随机发现1个bug和1个subcom ...
随机推荐
- 十七、Job与Cronjob
Job 与 Cronjob 一.Job Job 负责批处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束. 特殊说明: 1.spec.template 格式同 Pod 2 ...
- 京东云开发者|关于“React 和 Vue 该用哪个”我真的栓Q
一.前言:我全都要 面对当今前端界两座大山一样的主流框架,React和Vue,相信很多小伙伴都或多或少都产生过这样疑问,而这样的问题也往往很让人头疼和犹豫不决: 业务场景中是不是团队用什么我就用什么? ...
- js函数组合
纯函数和柯里化容易引起洋葱代码 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数 函数组合并没有减少洋葱代码,只是封装了洋葱代码 函数组合执行顺序从右到左 满足结合律既可以把g和h组合 还可以 ...
- maven的下载、安装、配置,idea中配置Maven
下载 下载链接: 点击下载地址 : 找到:对应版本的包下载 安装 下载后的压缩包解压出来,然后将解压后的包放到日常安装软件的位置即安装成功,当然取决于个人意愿,也可以不移动. 打开安装包后的目录结构简 ...
- HTML5和CSS3新特性
1.HTML5新标签和属性 1.1 兼容性前缀与语义化 兼容低版本的写法.比较新的浏览器,可以直接写.兼容性前缀,是每个浏览器私有的. 内核 兼容性前缀 浏览器 Gecko -moz- Firefox ...
- Aspose.Cell和NPOI生成Excel文件2
NPOI还是比较好用的,引用dll程序集即可 1创建workbook和工作流 HSSFWorkbook workbook = new HSSFWorkbook(); MemoryStream ms = ...
- cordon节点,drain驱逐节点,delete 节点
目录 一.系统环境 二.前言 三.cordon节点 3.1 cordon节点概览 3.2 cordon节点 3.3 uncordon节点 四.drain节点 4.1 drain节点概览 4.2 dra ...
- 扫雷(哈希+bfs)
扫雷 题目描述: 小明最近迷上了一款名为<扫雷>的游戏. 其中有一个关卡的任务如下: 在一个二维平面上放置着 n 个炸雷,第 i 个炸雷 (x\(_i\),y\(_i\),r\(_i\)) ...
- 「浙江理工大学ACM入队200题系列」问题 K: 零基础学C/C++84——奇偶ASCII值判断
本题是浙江理工大学ACM入队200题第八套中的K题 我们先来看一下这题的题面. 题面 题目描述 任意输入一个字符,判断其ASCII是否是奇数,若是,输出YES,否则,输出NO; 例如,字符A的ASCI ...
- ArchLinux安装手册(2022-10-01)
准备工作 镜像下载:北京外国语大学镜像 使用ventoy做启动盘: (1) ventoy下载:github下载地址 (2) 解压运行下载好的ventoy,设备选择准备好的U盘(会清空),然后选择安装即 ...