概率和期望

定义

概率,即概率,不提。

期望如下定义:

对于事件 P 可能产生的 n 个贡献为 vali 的结果,每个结果概率为 pi,则称事件 P 的数学期望:

E(P)=i=1nvalipi

翻译成人话就是事件结果反映权值的平均数。

期望线性性?

不同事件间的期望得以累加,形式化地,A,B,E(A+B)=E(A)+E(B)

左边的加指的是事件同时成立。

举个例子。事件 A 为wmw在一段时间 n 内每天等概率地与wymx增加 vali 点亲密度。

E(A)=1nnvali

事件 B 为cjx在这段时间内每天也以 pi 概率与nw增加 vali 点亲密度。

E(B)=nvalipi

由此可以得到:cjx泡妹子看脸两个人每天为人类延续的伟大事业做出的贡献期望 E(A+B) 为:

nvali(pi+1n)

这意味着:处理整个事件的期望时,可以将该事件的子事件按拓扑线性转移。事件的拓扑一般也是线性的。因而导致概率与期望的最好处理方法是 dp。

OSU1

给定打一把音游的结果,计算成绩。只有 P 和 Miss,还有不知道结果的 note,判定对半开,存在 combo 机制为 combo(x)=x2。求这把的期望得分。

这人准度咋这么低啊

思考:如何处理连击分?

如果现在已经有一段长 x 的combo 给出了 x2 的贡献,则下一个 ? 也为 P 时,该 note 做出的分数贡献为:(x+1)2x2=2x+1

我们发现不仅要计算期望得分,还得计算期望 combo。

fi 表示到第 i 个 note 时的期望得分,gi 表示到 i 个note 时的期望combo。

stri=x:此时不得分,且会断连。

f[i]=f[i1],g[i]=0

stri=o:此时一定按 combo 得分,对combo 做出概率为 1 的贡献。

f[i]=f[i1]+2g[i1]+1,g[i]=g[i1]+1

stri=?:此时有一半概率不得分,按照期望公式:

f[i]=f[i1]+12(2g[i1]+1)

g[i]=12(g[i1]+1)

注意不得分时是断连,所以 g[i1] 也要加入 12 中。

#include<bits/stdc++.h>
#define MAXN 300005
using namespace std;
int n;
char str[MAXN];
double f[MAXN],g[MAXN];
int main(){
	scanf("%d",&n);
	scanf("%s",str+1);
	for(int i=1;i<=n;i++){
		if(str[i]=='x')f[i]=f[i-1],g[i]=0;
		if(str[i]=='o')f[i]=f[i-1]+2*g[i-1]+1,g[i]=g[i-1]+1;
		if(str[i]=='?')f[i]=f[i-1]+1.0*(2*g[i-1]+1)/2,g[i]=1.0*(g[i-1]+1)/2;
	}
	printf("%.4f",f[n]);
	return 0;
}

OSU2

byd这把准度完全看命了。连击分变成了 x3 这意味着新的一次打击能做出 (x+1)3x3=3x2+3x+1 的贡献。

同时 x2 也会增加 2x+1,这是上一道题的结论了。

所以开三个 dp 数组维护即可。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n;
double dp[5][MAXN];
double v[MAXN];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lf",&v[i]);
	for(int i=1;i<=n;i++){
		dp[1][i]=(1+dp[1][i-1])*v[i];
		dp[2][i]=(dp[2][i-1]+2*dp[1][i-1]+1)*v[i];
		dp[3][i]=dp[3][i-1]+(3*dp[2][i-1]+3*dp[1][i-1]+1)*v[i];
	}
	printf("%.1f",dp[3][n]);
	return 0;
}

绿豆蛙的归宿

此时事件的拓扑不是线性的了,不过是 DAG,所以直接跑一遍拓扑排序。

注意此题中一个点 v 能够对期望做出的贡献为:到达连接该点的概率 pu 与边权 w 之积。

dpv=1sizu(u,v)Edpu+puw(u,v)

所以如果顺推得先处理一遍概率。

#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
int n,m;
struct node{
	int v;
	double w;
};
vector<node>edge[MAXN];
int siz[MAXN],ind[MAXN],indd[MAXN];
double p[MAXN],dp[MAXN];
queue<int>Q;
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		edge[u].push_back({v,w});
		++siz[u],++indd[v],ind[v]=indd[v];
	}
	p[1]=1.0;
	Q.push(1);
	while(!Q.empty()){
		int u=Q.front();
		Q.pop();
		for(int i=0;i<edge[u].size();i++){
			int v=edge[u][i].v;
			double w=edge[u][i].w;
			--indd[v];
			p[v]+=p[u]/siz[u];
			if(!indd[v])Q.push(v);
		}
	}
	Q.push(1);
	while(!Q.empty()){
		int u=Q.front();
		Q.pop();
		for(int i=0;i<edge[u].size();i++){
			int v=edge[u][i].v;
			double w=edge[u][i].w;
			--ind[v];
			dp[v]+=(dp[u]+p[u]*w)/siz[u];
			if(!ind[v])Q.push(v);
		}
	}
	printf("%.2f",dp[n]);
	return 0;
}

列队春游

乍一看很不好做。实则确实不好做

考虑从单个人入手,将人按身高排序作 p1...pn,假设人的身高是不重复的,则 pk 的视距 valk,满足 0valkk。重复时:0valkk+sizk1,其中 sizk 指升高与 pk 相同的个数。

为了处理方便,令 F(k)=valk+sizk1

于是单个人的贡献为:

E(k)=i=1F(k)iP

即前 i 个人都不高于他的概率。

然后考虑计算这个概率,显然是一个排列组合问题。

假设有 G(k) 个不包括他的人身高不低于他,用总方案数除以可行方案即概率 P

G(k)=nrkk+1,k[1,n],pkpk+1,prkk=prkk+1=...=prkk+sizk1

E(k)=i=1n(ni+1)AniG(k)AnG(k)+1

i=1n(ni+1)AniG(k)AnG(k)+1

=i=1n(ni+1)(niG(k)+1)(niG(k)+2)...(ni)(nG(k))(nG(k)+1)...n

=i=1n(nG(k)1)!(ni1)!n!(niG(k))!

=(nG(k)1)!n!i=1n(ni1)!(niG(k))!

=(nG(k)1)!n!i=1n(G(k)+1)!Cni+1G(k)+1

(nG(k)1)!(G(k)+1)!n!i=1nCni+1G(k)+1

然后发现化不动了。

想到:Cnm=Cn1m+Cn1m1

后面的部分展开:

CnG(k)+1+Cn1G(k)+1...+C1G(k)+1

=CnG(k)+1+...C1G(k)+1+C1G(k)+2 (C1G(k)+2=0)

=CnG(k)+1+Cn1G(k)+1+...C2G(k)+1+C2G(k)+2

=CnG(k)+1+CnG(k)+2=Cn+1G(k)+2

于是原式化为:

E(k)=Cn+1G(k)+2(nG(k)1)!(G(k)+1)!n!

=(n+1)!(nG(k)1)!(G(k)+1)!(G(k)+2)!(nG(k)1)!n!

=n+1G(k)+2

非常简洁。

E=i=1nE(i)=i=1nn+1G(i)+2

G(i) 函数乱搞即可,不放码了。

守卫者的挑战

dp[i][j][k] 表示到第 i 次挑战成功了 j 次,剩余背包容量为 k 时的概率。

这个题很坑,背包容量不足了其实可以先拿着,只要最后的容量大于等于零即可。

因而当前容量可以为负数,所以把容量挪一下,又注意到最多只会消耗 200 的容量,即 n,所以容量超过 n 的当 n 处理。

ans=i>=l,j>=ni<=n,j<=2ndp[n][i][j]

考虑转移,失败时:

dp[i][j][k]=dp[i1][j][k]

成功时:

dp[i][j][k]=dp[i1][j1][kval[i]]

两者的概率分别为 1pi,pi

#include<bits/stdc++.h>
#define MAXN 405
using namespace std;
int n,l,k;
double dp[2][205][MAXN];
int val[MAXN];
double p[MAXN],ans;
int main(){
	scanf("%d%d%d",&n,&l,&k);
	k=min(n,k);
	for(int i=1;i<=n;i++)scanf("%lf",&p[i]),p[i]/=100.0;
	for(int i=1;i<=n;i++)scanf("%d",&val[i]);
	dp[0][0][n+k]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=n;j++)
			for(int k=0;k<=n*2;k++)dp[i%2][j][k]=dp[(i+1)%2][j][k]*(1-p[i]);
		for(int j=0;j<n;j++)
			for(int v=1;v<=n*2;v++){
				int siz=min(v+val[i],2*n);
				dp[i%2][j+1][siz]+=dp[(i+1)%2][j][v]*p[i];
			}	
	}
	for(int i=l;i<=n;i++)for(int j=0;j<=n;j++)ans+=dp[n%2][i][j+n];
	printf("%.6f",ans);
	return 0;
}

这个题会卡空间,注意到当前的 i 只会从 i1 转移,第一维用滚动数组滚到 2 容量即可。

卡牌游戏

剩下一个人时该人胜率为 100%,所以应该从这个状态逐步转移到剩下 n 个人时第 i 个人的胜率 dp[n][i]

由于游戏每轮固定杀掉一个人,所以第 i 轮的序列有 i 人,每轮有 1m 的概率抽到一张牌并生成新的序列,也就是说每个状态的转移概率是 1m

1m 的概率抽到第 j 张牌时,从庄家开始的第 cj%i 人将被杀掉,我们计算每个人在一个人被杀掉后的新位置。

又令 dp[i][j] 表示第 i 轮从庄家开始的第 j 个人获胜的概率,这样同时迎合了转移和答案的格式。

对于第i 轮从庄家开始的 j 个人,在抽到第 k 张牌时:

loc=ck%i

dp[i][nloc+j]=1mdp[i1][j],loc>j

dp[i][jloc]=1mdp[i1][j],loc<j

照着打就能过。

#include<bits/stdc++.h>
#define MAXN 55
using namespace std;
int n,m;
double dp[MAXN][MAXN];
int c[MAXN];
int main(){
   scanf("%d%d",&n,&m);
   for(int i=1;i<=m;i++)scanf("%d",&c[i]);
   dp[1][1]=1;
   for(int i=2;i<=n;i++){
   	for(int j=1;j<=i;j++){
   		for(int k=1;k<=m;k++){
   			int loc=c[k]%i;
   			if(!loc)loc=i;
   			if(loc>j)dp[i][j]+=dp[i-1][i-loc+j]/m;
   			if(loc<j)dp[i][j]+=dp[i-1][j-loc]/m;
   		}
   	}
   }
   for(int i=1;i<=n;i++)printf("%.2f",dp[n][i]*100),putchar('%'),putchar(' ');
   return 0;
}

换教室

一道很烦的题,到处都会挂分。

由于不同概率导致的结果会导致路径权值不同,我们使用 dp[i][j][0/1] 表示时间为 i 时申请换了 j 次课,当前时间不换/换 的期望最短路。

如果这次不换:

上次也可能没换,或者换了,然换了要考虑换没换成。

dp[i][j][0]=min{dp[i1][j][0]+dis(c[i],c[i1])dp[i1][j][1]+pi1dis(d[i1],c[i])+(1pi1)dis(c[i1],c[i])

如果这次换了:

那也要同时考虑这次换没换成。

dp[i][j][1]=min{dp[i1][j1][0]+pidis(d[i],c[i1])+(1pi)dis(c[i],c[i1])dp[i1][j1][1]+pipi1dis(d[i],d[i1])+(1pi)pi1dis(c[i],d[i1])+pi(1pi1)dis(d[i],c[i1])+(1pi)(1pi1)dis(c[i1],c[i])

v 很小,使用 Floyd。

注意:

  • disi,j 是浮点类型。
  • 浮点数不可使用 memset()。
  • 有重边自环。
  • Floyd 的枚举顺序为 k,i,j,这个很重要。
  • j=0 时无法转移 dp[i][j][1]
#include<bits/stdc++.h>
#define MAXN 2005
#define MAXM 305
#define int long long
using namespace std;
int n,m,V,e;
double dis[MAXM][MAXM];
int c[MAXN],d[MAXN];
double p[MAXN];
double dp[MAXN][MAXN][2];
double ans=1e9;
signed main(){
	scanf("%lld%lld%lld%lld",&n,&m,&V,&e);
	for(int i=0;i<=V;i++)for(int j=0;j<=V;j++)dis[i][j]=1e9;
	for(int i=1;i<=n;i++)scanf("%lld",&c[i]);
	for(int i=1;i<=n;i++)scanf("%lld",&d[i]);
	for(int i=1;i<=n;i++)scanf("%lf",&p[i]);
	for(int i=1;i<=V;i++)dis[i][i]=dis[i][0]=dis[0][i]=0;
	for(int i=1,u,v,w;i<=e;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		dis[u][v]=min(dis[u][v],1.0*w);
		dis[v][u]=min(dis[v][u],1.0*w);
	}
	for(int k=1;k<=V;k++)
		for(int i=1;i<=V;i++)
			for(int j=1;j<=V;j++){
				dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
				dis[j][i]=min(dis[j][i],dis[i][k]+dis[k][j]);
			}
	for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)dp[i][j][0]=dp[i][j][1]=1e9;
	dp[1][1][1]=dp[1][0][0]=0;
	for(int i=2;i<=n;i++){
		dp[i][0][0]=dp[i-1][0][0]+dis[c[i]][c[i-1]];
		for(int j=1;j<=min(i,m);j++){
			dp[i][j][0]=dp[i-1][j][0]+dis[c[i]][c[i-1]];
			dp[i][j][0]=min(dp[i][j][0],dp[i-1][j][1]+p[i-1]*dis[c[i]][d[i-1]]+(1.0-p[i-1])*dis[c[i]][c[i-1]]);
		}
		for(int j=1;j<=min(i,m);j++){
			dp[i][j][1]=dp[i-1][j-1][0]+p[i]*dis[d[i]][c[i-1]]+(1.0-p[i])*dis[c[i]][c[i-1]];
			dp[i][j][1]=min(dp[i][j][1],dp[i-1][j-1][1]+p[i-1]*p[i]*dis[d[i]][d[i-1]]+p[i-1]*(1.0-p[i])*dis[c[i]][d[i-1]]+(1.0-p[i-1])*p[i]*dis[c[i-1]][d[i]]+(1.0-p[i])*(1.0-p[i-1])*dis[c[i]][c[i-1]]);
		}
	}
	for(int i=0;i<=m;i++)ans=min(ans,min(dp[n][i][0],dp[n][i][1]));
	printf("%.2f",ans);
	return 0;
}

概率充电器

自己想的最多的一道题,虽然最后也颓了下题解。

首先:期望的树上问题大概率用 树形dp 解决,如果去考虑一遍 dfs 预处理后按点间关系实现线性转移大概率要爆。

根据公式:

ans=i=1nE(i)=i=1npiwi

i,wi=1ans=i=1npi

也就是计算每个点被点亮的概率和。

一个点可以这样点亮:

  • 自己以 pi 的概率亮了
  • 被父亲节点以 P(fa) 的概率传导点亮了
  • 被子节点以 P(son) 的概率传导点亮了

第一条可以初始化时就算好,即

dpi=qi

P(i) 函数显然是要通过给出的概率计算的,因为 P(i) 也包含点 i 自己点亮与被传导点亮的概率。

而对于两个点 i,ji 自己点亮后会对 j 产生贡献得到 dpj,而 i 在不亮时被传导点亮的概率显然不是 dpj。因为这其中包括了:“i 点亮而 j 不亮时 i 点亮 j”这一情况的概率。

我们显然不能高效地在 树形dp 中处理这种混乱的关系。而这种关系是由于想要同时考虑 P(fa)P(son) 导致的。

这启示我们分开来看。从任一根节点开始计算,发现P(fa) 会受到除了 i 以外的节点 P(sonfa) 影响,所以要先处理 P(son)

对于节点 i 与他的子节点 jsoni,想当然地想到

dpi=dpi+dpjP(link)

不过这个显然是错的,给定 qi=qj=50%,P(link(i,j))=100%,我们发现 j 成了必然点亮的东西。

不妨思考 i 被点亮的概率 P(i)j 被点亮的概率 P(j) 间的转移,只有 i 自己不亮,j 也没有传过来时 i 才不亮。

P(i)=1((1dpi)(1P(link)P(j)))

=dpi+P(link)P(j)dpiP(j)P(link)

每个子节点 j 都可以在一遍 dfs() 中完成转移。

现在单个考虑第三条如何成立。

j 能被父亲 i 点亮时,P(i) 应该减去 j 对其做出的贡献,不过显然是不能直接减的,我们考虑刚才的式子:

P(i)=dpi+P(link)P(j)dpiP(j)P(link)

此时 dpi 才是父节点 ij 断电时能传导给其的概率。还好它是可以反推的。

dpidpiP(j)P(link)=P(i)P(link)P(j)

dpi=P(i)P(link)P(j)1P(j)P(link)

此时同理得到 j 被父节点传递的概率为:

P(j)=P(link)dpi+dpjP(link)dpidpj

如此,使用两遍 dfs 后汇总即可。

但是注意 P(j)P(link)=1 时就别转了,会爆。这里使用
愤怒的小鸟 里的 fabs() 技巧处理。

#include<bits/stdc++.h>
#define MAXN 500005
using namespace std;
int n;
struct node{
	int v;
	double p;
};
vector<node>edge[MAXN];
double q[MAXN];
double dp[MAXN],ans;
inline void dfs2(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i].v;
		double w=edge[u][i].p;
		if(v==fa)continue;
		dfs2(v,u);
		dp[u]=(dp[u]+w*dp[v]-dp[u]*dp[v]*w);
	}
}
inline bool check(double val){
	return (fabs(val-1.0)<=1e-7);
}
inline void dfs(int u,int fa){
	for(int i=0;i<edge[u].size();i++){
		int v=edge[u][i].v;
		double w=edge[u][i].p;
		if(v==fa)continue;
		if(!check(dp[v]*w)){
			double falink=(dp[u]-w*dp[v])/(1.0-dp[v]*w);
			dp[v]=(dp[v]+w*falink-dp[v]*falink*w);
		}
		dfs(v,u);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++){
		double p;
		scanf("%d%d%lf",&u,&v,&p);
		p/=100.0;
		edge[u].push_back({v,p});
		edge[v].push_back({u,p});
	}
	for(int i=1;i<=n;i++)scanf("%lf",&q[i]),q[i]/=100.0;
	for(int i=1;i<=n;i++)dp[i]=q[i];
	dfs2(1,0);
	dfs(1,0);
	for(int i=1;i<=n;i++)ans+=dp[i];
	printf("%.6f",ans);
	return 0;
}

分手是祝愿

题太吊了。

看了快一个小时一点思路没有,所以直接说题解做法。

首先,对于这样的一个 01串,存在唯一解法使得串能被归零。

解为:从右往左依次扫描,发现开着的灯就关掉。

证明:不难想这一定是可行解,现在要证这样的解法是唯一解。

对于任意一盏灯,其开关次数都不会超过一次,因为这样的操作是取异或,开关两次相当于没动,三次相当于一次。

不妨把一种解法对开关灯的编号汇总为集合 Si,易知一种解法 Sk 对状态 C 是对应的,要证明是唯一解,只需证 映射 f:SC 为单射。

|C|=|S|=2n,只需证 f 为满射。

反证,不妨设存在状态 C0 使得 Si,f:SiC0,然无论如何,上文解法一定可以将 C0 变为全 0 的串,矛盾。

因此,任何情况都有唯一关灯方法即上文提到的方法。

这个结论有什么用呢?这告诉我们,在 n 盏灯中,当且仅当某 val 盏灯都被关闭时,初始状态可被清零。

也就是说在无穷的随机开关灯中,B 君只有 val 次操作是正确的,且每多一次无用操作,都需要一次额外的操作来偿还这次失误。

这不就成概率期望题了?

初始化后,令 dpi 表示还剩 i(ival) 盏必须关闭的灯时,把 i 关到 i1 时的期望操作。

则当前情况下:有 in 的概率是有效操作,而有 nin 的操作是无效操作,需要偿还。且此时必须要关掉的灯变成了 i+1 盏,还需要期望为 dpi+1 次操作才能复原。复原后,又需要期望为 dpi 次操作才能变到 i1

dpi=in+nin(dpi+1+dpi+1)

整理一下:

dpi+inndpi=i+(ni)(dpi+1+1)n

indpi=(ni)dpi+1+nn

dpi=(ni)dpi+1+ni,dpn+1=0

我们发现完成归零时的期望转移和状态完全无关了,完美解决了难以设计状态的问题。

又因为剩下 k 次时只需要 k 次操作就可以归零。

ans={k,val<kk+i=k+1ndpi

然后就没了。

#include<bits/stdc++.h>
#define MAXN 100005
#define int long long
using namespace std;
const int p=100003;
int f[MAXN];
inline void INIT(){
	f[0]=1;
	for(int i=1;i<MAXN;i++)f[i]=f[i-1]*i%p;
}
int n,k;
int l[MAXN],val,ans;
int dp[MAXN];
inline int qpow(int base,int power){
	int res=1;
	while(power){
		if(power&1)res=res*base%p;
		base=base*base%p;
		power>>=1;
	}
	return res%p;
}
signed main(){
	INIT();
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=n;i++)scanf("%lld",&l[i]);
	for(int i=n;i>=1;i--){
		if(l[i]){
			++val;
			for(int j=1;j*j<=i;j++){
				if(i%j==0){
					l[j]^=1;
					if(j*j!=i)l[i/j]^=1;
				}
			}
		}
	}
	for(int i=n,tmp=0;i>=1;i--){
		tmp=((n-i)*dp[i+1]%p+n)%p;
		tmp=tmp*qpow(i,p-2)%p;
		dp[i]=tmp;
	}
	if(val<=k)ans=val;
	else{
		ans=k;
		for(int i=val;i>k;i--)ans=(ans+dp[i])%p;
	}
	printf("%lld",ans*f[n]%p);
	return 0;
}

还有几道难题需要高斯消元,不过我现在不会,此帖暂时完结。

posted @   Cl41Mi5deeD  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示