笔记——概率期望DP·Part1

笔记——概率期望DP·Part1

概率#

概率是反映随机事件出现的可能性大小的量度。

一般用 A 表示事件,P(A)A 发生的概率。

性质#

  • 对于任意一个事件 AP(A)=1P(¬A)

  • 有限可加性:当 n 个事件 A1,,An 两两互不相容时:P(A1...An)=P(A1)++P(An)

是并,在这里相当于逻辑或。

  • 当事件 A,B 满足 A 包含于 B 时:P(BA)=P(B)P(A),P(A)P(B)

BA 就是从 B 中剔除 A,可以理解为 B 发生而 A 不发生。

  • 对任意两个事件 ABP(BA)=P(B)P(AB)

这条相当于上一条,可以把 AB 视为一个整体。

P(AB)=P(A)+P(B)P(AB)

可以从上一条推出来。

P(BA)=P(B)P(AB)P(ABA)=P(B)P(AB)P(AB)P(A)=P(B)P(AB)P(AB)=P(A)+P(B)P(AB)

条件概率#

条件概率是指事件 A 在另外一个事件 B 已经发生条件下的发生概率。条件概率表示为:P(A|B),读作「在 B 的条件下发生 A 的概率」

注意:P(A|B)P(B|A) 不同。

AB 的联合概率,即共同发生的概率,表示为 P(AB) 或者 P(A,B) 或者 P(AB)

是交,这里可以理解为逻辑与。

P(AB)=P(A)×P(B|A)=P(B)×P(A|B)

P(A|B)=P(AB)P(B)

统计独立性#

当且仅当两个随机事件 AB 满足 P(AB)=P(A)×P(B) 的时候,它们才是统计独立的,这样联合概率可以表示为各自概率的简单乘积。

同样,对于两个独立事件 ABP(A|B)=P(A) 以及 P(B|A)=P(B)

换句话说,如果 AB 是相互独立的,那么 AB 这个前提下的条件概率就是 A 自身的概率;同样,BA 的前提下的条件概率就是 B 自身的概率。

互斥性#

当且仅当 AB 满足 P(AB)=0P(A)0P(B)0 的时候,AB 是互斥的。

因此,P(A|B)=0P(B|A)=0

换句话说,如果 B 已经发生,由于 A 不能和 B 在同一场合下发生,那么 A 发生的概率为零;同样,如果 A 已经发生,那么 B 发生的概率为零。

期望#

数学期望(mean)(或均值,亦简称期望)是试验中每次可能结果的概率乘以其结果的总和,是最基本的数学特征之一。它反映随机变量平均取值的大小。

x 的多个随机变量为 a1,a2,a3,,an

对应的概率为 p1,p2,p3pn

则期望为 E(x)=i=1naipi

性质#

  • E(cx)=cE(x)c为常数。

相当于在合式里面乘了一个数 c,将 c 提出来。

  • E(x+y)=E(x)+E(y)xy 是任意两个随机变量

  • E(xy)=E(x)×E(y)xy 是相互独立的随机变量

题目#

了解了这些基础的东西之后,就该做题了吧!

P1654 OSU!#

题意#

你有一个长度为 n 的01串。

这个串的第 i 位有 pi 的概率为 1(1pi) 的概率为 0

定义这个串的分数为:这个串中连续的 X1 可以产生 X3

问这个串的期望分数。

做法#

先考虑一个简单的东西。

x3i 表示以 i 结尾的 x3 的期望,求 x3i

容易发现,(x+1)3=x3+3x2+3x+1

那就是说 E(x3)=E((x1)3)+3E((x1)2)+3E(x1)+1

那么我们就可以得到 x3i=(x3i1+3x2i1+3x1i1+1)×pi

然后我们设 dpi 表示我们要求的答案,前 i 位的期望分数。

那么 dpi=dpi1+pi×(3x2i1+3x1i1+1)

为什么呢?

首先,显然地,一定会继承前 i1 的分数,所以加上 dpi1

然后,有 pi 的概率是 1,考虑这时产生的贡献。

显然,应该加上 x3i。因为会算重,所以减去 x3i1

那么这一部分就是:

x31x3i1=x3i1+3x2i1+3x1i1+1x3i1=3x2i1+3x1i1+1

那么上面的式子就显然了。

代码#

稍微改了下变量名。

int main(){
	n=read();
	for(int i=1;i<=n;i++)cin>>p[i];
	for(int i=1;i<=n;i++){
		x[i]=(x[i-1]+1)*p[i];
		x2[i]=(x2[i-1]+2*x[i-1]+1)*p[i];
		ans[i]=ans[i-1]+(3*x2[i-1]+3*x[i-1]+1)*p[i];
	}
	cout<<fixed<<setprecision(1)<<ans[n];
	return 0;
}

P4550 收集邮票#

题意#

皮皮想要收集 n 种不同的邮票,但是他只能到凡凡那里购买,每次只能买一张。购买第 k 次邮票需要支付 k 元钱。假设购买到每种邮票的概率都是等概率的 1/n,现在要求计算皮皮收集所有种类的邮票需要花费的钱数的期望。

做法#

dpi 表示已经得到 i 张邮票,还要花费的期望钱数。

容易发现,dp 值的转移和已购买次数有关。已购买次数并不好处理。

那么不妨先求一个简单的东西:买到已经有了 i 张不同邮票,还要购买的期望次数 fi

显然,fi=in×fi+(1in)×fi+1

想当然地,当我们求出了需要的次数时,我们就能求出需要的钱数。

钱数为 ×(+1)2

但这样是过不了样例的。可以类比在第一题中不能直接用 x1 求出 x2x3 的值。

虽然不能直接求出答案,但是我们解决了求出已经购买的次数的问题。

如果我们从后往前计数,就是说,最后一次购买的花费是 1,倒数第二次的花费是 2,以此类推,那么本次的期望花费就是 fi+1+1

所以,dpi=in(dpi+fi+1)+(1in)(dpi+1+fi+1+1)

容易发现,这里的 dpi 需要用到 dpi 才能求出。

这是不方便处理的,所以我们对这个式子进行化简。

dpi=in(dpi+fi+1)+(1in)(dpi+1+fi+1+1)dpi=in×dpi+in(fi+1)+nin(dpi+1+fi+1+1)dpiin×dpi=in(fi+1)+nin(dpi+1+fi+1+1)nin×dpi=in(fi+1)+nin(dpi+1+fi+1+1)dpi=ini(fi+1)+dpi+1+fi+1+1

然后直接转移即可。

代码#

int main(){
	n=read();
	for(int i=n-1;~i;i--)f[i]=f[i+1]+1.0*n/(n-i);
	for(int i=n-1;~i;i--)dp[i]=1.0*i/(n-i)*(f[i]+1)+dp[i+1]+f[i+1]+1;
	cout<<fixed<<setprecision(2)<<dp[0];
	return 0;
}

P3802 小魔女帕琪#

题意#

帕琪拥有 7 种属性的魔法晶体,第 i 种晶体有 ai 个,每次等概率随机使用一个晶体,施放对应属性的魔法。如果连续施放的 7 个魔法中魔法的属性互不相同,则触发一次帕琪七重奏。现在帕琪想知道触发帕琪七重奏的期望次数是多少。

等概率选取就是说:如果有两个 A 一个 B,那么选到 A 的概率就是 23,因为是对晶体等概率选取,而不是对颜色。

做法#

考虑前 7 个晶体能够释放七重奏的概率:

7!×a1n×a2n1×a3n2×a4n3×a5n4×a6n5×a7n6

其中,n=k=17ak

因为当固定顺序时,概率为:a1n×a2n1×a3n2×a4n3×a5n4×a6n5×a7n6

又因为有 7! 种顺序,所以要乘上 7!

容易发现,每一次能够释放七重奏的概率相等。

比方说,求出第 2 到第 8 位的概率。

可以考虑枚举第一位选择的魔法,然后计算,发现和第一次就释放七重奏的概率相等。

所以,最后的答案就是:

(n6)×7!×a1n×a2n1×a3n2×a4n3×a5n4×a6n5×a7n6=7!×a1a2a3a4a5a6a7n(n1)(n2)(n3)(n4)(n5)

代码#

稍微改了一下变量名。

有毒瘤数据,其中 k=17ak=6,如果不化简的话会出现除以 0 的情况。

int main(){
	for(int i=1;i<=n;i++)sum+=a[i]=read();
	for(int i=1;i<=n;i++){
		if(i<7)ans*=1.0*a[i]/(sum-i+1);
		else ans*=1.0*a[i];
	}
	ans*=5040;//7的阶乘等于5040
	cout<<fixed<<setprecision(3)<<ans;
	return 0;
}

P6858 深海少女与胖头鱼#

题意#

题目大意:
n 条带 「圣盾」的「胖头鱼」和 m 条不带圣盾的胖头鱼。每次等概率对一条存活的胖头鱼造成「剧毒」伤害(直接杀死没有圣盾的胖头鱼)。若有圣盾,免疫本次伤害。免疫伤害后,圣盾被破坏。在一条胖头鱼的圣盾被破坏后,给予其他所有没有死亡且没有圣盾的胖头鱼圣盾。现在求杀死所有的胖头鱼所需的期望次数。答案对 998244353 取模。

啊对了,这道题 n1014,m106

做法#

显然,状态里面要记录有护盾和没有护盾的敌人数量。就是说,dpij 表示有 i 个有护盾的,j 个没有护盾的的期望次数。

如果打到了护盾上,那么会变成 (i+j1,i)。因为只有被打到的护盾会消失,其他的都会获得护盾,或者已经拥有护盾。发生这种事情的概率是 ii+j

如果没有打到护盾上,那么会变成 (i,j1)。因为会直接击败一个没有护盾的。发生这件事的概率是 ji+j

那么就有了转移式子:

dpi,j={0i,j=0dpi1,1i>0,j=0ii+j×dpi+j1,1+ji+j×dpi,j1+1else

这样的复杂度为 Θ(nm),可以得到 15 分的好成绩。

如果能快速地求出 dpi,1 的值,上面式子的复杂度就可以优化到 Θ(n) 了。

容易发现:

dpi,1=ii+1dpi,1+1i+1dpi,0+1dpi,1=ii+1dpi,1+1i+1dpi1,1+1i+1+1dpi,1ii+1dpi,1=1i+1dpi1,1+1i+1+11i+1dpi,1=1i+1dpi1,1+1i+1+1dpi,1=dpi1,1+i+2

观察题解可得, 这个式子化简之后就是:dpi,1=n2+5n+22

或者,用矩阵加速递推算这个东西也是很快的。

将这个式子带入之后,就得到了一个 Θ(n) 的做法。

因为需要求逆元,所以最终复杂度为 Θ(nlog2v)

代码#

因为 n1014,还来不及做第一次取余就有可能会溢出,所以读入的时候直接取模就好。

const int mod=998244353;
int n,m;
int ny(int x){//快速幂求逆元
	int ans=1;
	for(int b=mod-2;b;b>>=1){
		if(b&1)ans=(ans*x)%mod;
		x=(x*x)%mod;
	}
	return ans;
}
int g(int x){//快速求出j=1的值
	return ((((x*x)%mod+(5*x)%mod+2)%mod)*ny(2))%mod;
}
int f(int a,int b){
	if(!b)return (g(a-1)+1)%mod;
	if(b==1)return g(a);
	return (((a*ny(a+b)%mod)*g(a+b-1))%mod+((b*ny(a+b)%mod)*f(a,b-1))%mod+1)%mod;
}
signed main(){
	n=read()%mod,m=read()%mod;
	cout<<f(n,m);
	return 0;
}

P1850 [NOIP2016 提高组] 换教室#

题意#

题目大意:有 n 个时间段,每个时间段有一个课程。每个课程有 2 个教室可选。你需要按时间顺序完成这些课程。有一张 v 个点的无向联通图表示教室之间的路径和花费的体力值。

你可以提交申请以更换教室。一开始,第 i 节课必然会在第 ci 个教室进行。如果你提交了换教室的申请,你将有 ki 的概率在 di 号教室上课,有 (1ki) 的概率在 ci 号教室上课。

你至多可以提交 m 份申请。问总体力值期望最小是多少。

n,m2000,v300

做法#

首先,跑一遍Floyd,求出点与点之间的最短路。

dpi,j,0/1 表示上完前 i 节课,提交恰好 j 份申请,第 i 节课是否提交了申请的最小期望总体力值。

考虑转移。

如果一节课没有提交申请,转移时有两种选择:上节课交了申请和没有交申请。枚举所有情况取最小值即可。

如果一节课交了申请,转移时有两种选择:上节课交了申请和没有交申请。枚举所有情况取最小值即可。

具体的式子太长了,见代码。

代码#

const int maxn=2010;
int n,m,v,e;
int dis[maxn][maxn];
int c[maxn],d[maxn];
double k[maxn];
double dp[maxn][maxn][2];
double ans=1e9;
void init(){//初始化dp数组为一个极大值
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			dp[i][j][0]=dp[i][j][1]=1e9;
		}
	}
	dp[1][0][0]=dp[1][1][1]=0;//上完第一节课走的路程为0
}
signed main(){
	n=read(),m=read(),v=read(),e=read();
	init();
	for(int i=1;i<=n;i++)c[i]=read();
	for(int i=1;i<=n;i++)d[i]=read();
	for(int i=1;i<=n;i++)scanf("%lf",&k[i]);
	for(int i=1;i<=e;i++){
		int a=read(),b=read(),c=read();
		dis[a][b]=dis[b][a]=min(dis[a][b],c);
	}
	Floyd();//求最短路
	for(int i=2;i<=n;i++){//计算dp值
		for(int j=0;j<=min(m,i);j++){
			dp[i][j][0]=min(dp[i-1][j][0]+dis[c[i-1]][c[i]],dp[i-1][j][1]+k[i-1]*dis[d[i-1]][c[i]]+(1.0-k[i-1])*dis[c[i-1]][c[i]]);//这节课没交申请,一个是定值,另一个枚举两种情况
			if(j)dp[i][j][1]=min(dp[i-1][j-1][0]+k[i]*dis[c[i-1]][d[i]]+(1.0-k[i])*dis[c[i-1]][c[i]],dp[i-1][j-1][1]+k[i]*k[i-1]*dis[d[i-1]][d[i]]+k[i]*(1.0-k[i-1])*dis[c[i-1]][d[i]]+(1.0-k[i])*k[i-1]*dis[d[i-1]][c[i]]+(1.0-k[i])*(1.0-k[i-1])*dis[c[i-1]][c[i]]);//这节课交了申请,枚举两种情况和枚举四种情况
		}
	}
	for(int i=0;i<=min(n,m);i++){//得到答案
		ans=min(ans,min(dp[n][i][0],dp[n][i][1]));
	}
	cout<<fixed<<setprecision(2)<<ans;
	return 0;
}

P4316 绿豆蛙的归宿#

题意#

给出张 n 个点 m 条边的有向无环图,起点为 1,终点为 n,每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。

绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果该节点有 k 条出边,绿豆蛙可以选择任意一条边离开该点,并且走向每条边的概率为 1k 。现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?

做法#

先考虑设 dpi 表示从 1 号点到 i 号点的路径长度期望。

那么转移就要考虑这个点从前面某个点转移来的概率。

这并不好求。

考虑反着定义状态。设 dpi 表示从 i 号点走到 n 号点的期望花费。

那么 dpi=(i,k)E1outi(dpk+w(i,k))

其中,outi 表示点 i 的出度,w(u,v) 表示边 (u,v) 的边权。

方便起见,使用刷表法。

代码#

void topsort(){//拓扑排序
	queue<int>q;
	q.push(n);
	while(!q.empty()){
		int now=q.front();q.pop();
		for(edge nxt:g[now]){
			--in[nxt.to];
			dp[nxt.to]+=1.0*(dp[now]+nxt.cost)/out[nxt.to];//刷表法
			if(!in[nxt.to])q.push(nxt.to);
		}
	}
}
signed main(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w);
	}
	topsort();
	cout<<fixed<<setprecision(2)<<dp[1];
	return 0;
}

P6154 游走#

题意#

B 城可以看作一个有 n 个点 m 条边的有向无环图可能存在重边

zbw 在 B 城随机游走,他会在所有路径中随机选择一条路径,选择所有路径的概率相等。路径的起点和终点可以相同。

定义一条路径的长度为经过的边数,你需要求出 zbw 走的路径长度的期望,答案对 998244353 取模。

做法#

因为选择每一条路径的概率相等,所以考虑求出路径总数和所有路径长度的和。

因为每个点都可以作为起点出现,所以考虑以每一个点为起点,求一遍路径总长和路径总数,然后再加起来。

显然这样的复杂度为 Θ(n2),无法通过此题。

考虑加入一个超级源点 S 和一个超级汇点 TS 像所有点连边,所有点向 T 连边。

显然,这时原图路径的总数等于从超级源点到超级汇点的路径总数。

那路径总长度等于从超级源点到超级汇点的路径长度总和吗?

容易发现,每条路径都会经过 (S,s)(t,T) 这两条边。

所以,每条路径的长度都多了 2,减去即可。

代码#

void topsort(){
	queue<int>q;
	cnt[s]=1;
	sum[s]=0;
	q.push(s);
	while(!q.empty()){
		int now=q.front();q.pop();
		for(int nxt:g[now]){
			--in[nxt];
			cnt[nxt]=(cnt[nxt]+cnt[now])%mod;
			sum[nxt]=(sum[nxt]+sum[now]+cnt[now])%mod;
			if(!in[nxt])q.push(nxt);
		}
	}
}
int ksm(int x){
	int ans=1;
	for(int b=mod-2;b;b>>=1){
		if(b&1)ans=(ans*x)%mod;
		x=(x*x)%mod;
	}
	return ans;
}
signed main(){
	n=read(),m=read();
	s=0,t=n+1;
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		g[u].push_back(v);
		++in[v];
	}
	for(int i=1;i<=n;i++){
		g[s].push_back(i);
		g[i].push_back(t);
		++in[i],++in[t];
	}
	topsort();
	sum[t]=(((sum[t]-(cnt[t]<<1))%mod)+mod)%mod;
	cout<<(sum[t]*ksm(cnt[t]))%mod;
	return 0;
}

P5104 红包发红包#

题意#

有一个抢红包的系统,红包总金额为 w 元,共有 n 个人参与抢红包。

如果红包里还剩 w 元,那么这次抢到的金额是 [0,w] 区间内的等概率随机实数 x

求第 k 个人期望抢到多少钱,答案对 109+7 取模。

kn1018,w1018

做法#

fi 表示第 i 个人抢到的钱数。

根据题意,f1=w2

考虑求出其他的 f 值。如果当前还剩 x 元,那么这次期望能抢到 x2 元,期望剩余 x2 元。

不妨设 gi 表示第 i 个人抢完后,剩余钱数的期望。根据上面的计算,fi=gi

显然 fi=gi12=fi12=w2i

使用快速幂求出即可。

代码#

int ksm(int a,int b){
	int ans=1;
	for(;b;b>>=1){
		if(b&1)ans=(ans*a)%mod;
		a=(a*a)%mod;
	}
	return ans;
}
signed main(){
	w=read()%mod,n=read(),k=read();
	ans=w;
	ans=(ans*ksm(ksm(2,k),mod-2))%mod;
	cout<<ans;
	return 0;
}

P1297 [国家集训队]单选错位#

题意#

给定了一组数 a1,a2,a3,...,an,其中 ai 表示第 i 道题的选项数。

你做对了所有题,但你抄答案时错位了。具体来说,第 i 道题的答案被抄到了第 i+1 道题上(如果 i=n,则答案被抄到了第 1 道题上)。假设你没有做错任何题目,求你期望能做对多少题。

n107

做法#

容易发现每种情况的概率是相等的,考虑求出所有可能情况数和所有可能情况下做对的题目的总数。

显然,总方案数 tot=i=1nai。不难发现,这个数大得离谱,很容易溢出。

先不着急,往下推式子先。

考虑第 i 道题有多少种情况能做对。容易发现,只有第 i 题和 i1 题会产生贡献,其他可以随便选。

考虑这两道题,发现总共有 ai×ai1 中情况。其中有 min(ai,ai1) 种情况可以做对第 i 题。

那么第 i 题对通过总题数的贡献就是:

k=1i2ak×k=i+1nak×min(ai,ai1)=k=1nakmax(ai,ai1)=totmax(ai,ai1)

综上,总贡献就是:

k=1ntotmax(ai,ai+1)tot=k=1n1max(ai,ai1)

这样,溢出的问题就解决了,直接计算即可。

代码#

for(int i=1;i<n;i++){
	ans+=1.0/max(a[i],a[i+1]);
}
ans+=1.0/max(a[1],a[n]);

P1365 WJMZBMR打osu! / Easy#

题意#

n 次点击要做,成功了就是 "o",失败了就是 "x",分数是按 combo 计算的,连续 a 个 combo 就有 a2 分,combo 就是极大的连续 "o"。

现在给定一个字符串,表示 WJMZBMR 在游戏中击打的结果,其中 "?" 号可以代表 "o" 或 "x",各自概率为 50%。求 WJMZBMR 的期望得分。

做法#

类比第一题。

o视为有 1 的概率 1x为有 0 的概率为 1?视为有 12 的概率为 1

和第一题的区别仅仅是从 x3 变成了 x2

于是设 fi 表示以 i 结尾的连续 1 的长度期望,dpi 表示要求的答案。

那么 fi=pi×(1+fi1),dpi=dpi1+pi×(2×fi1+1)

直接转移即可,答案为 dpn

代码#

signed main(){
	n=read();
	scanf("%s",s+1);
	for(int i=1;i<=n;i++){
		if(s[i]=='o')p[i]=1;
		if(s[i]=='x')p[i]=0;
		if(s[i]=='?')p[i]=0.5;
	}
	for(int i=1;i<=n;i++){
		f[i]=p[i]*(f[i-1]+1);
		dp[i]=dp[i-1]+p[i]*(2*f[i-1]+1);
	}
	cout<<fixed<<setprecision(4)<<dp[n];
	return 0;
}

CF16E Fish#

题意#

n 条鱼,编号从 1n ,生活在一个湖中。每天有一对鱼会相遇,并且每对鱼相遇的概率相等。

若两条标号为 ij 的鱼相遇,第一只吃了第二只的概率为 ai,j ,第二只吃了第一只的概率为 aj,i=1ai,j

这样的过程会一直进行下去,直到湖中只剩下一条鱼。请你计算每条鱼最后留在湖中的概率。

n18

做法#

观察数据范围,考虑状压dp。

dp(s) 表示有且仅有 S 中的鱼存活的概率。显然初始状态为 dp(all 1)=1

考虑转移。枚举上一次被吃掉的鱼和吃掉它的鱼。那么转移就是:

dp(S)=ij[iSjS]dp(S{i})×aj,i×2|S|×(|S|+1)

按照式子转移即可。

代码#

signed main(){
	n=read();
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			scanf("%lf",&a[i][j]);
		}
	}
	dp[(1<<n)-1]=1;
	for(int i=(1<<n)-2;i;--i){
		int siz=__builtin_popcount(i);
		for(int lst=0;lst<n;++lst){//上一次被吃掉的
			if((1<<lst)&(i))continue;//被吃了的一定死了
			for(int now=0;now<n;++now){//吃了他的
				if(!((1<<now)&(i)))continue;//吃了他的一定还活着
				if(now==lst)continue;//自己不会吃自己
				int j=i^(1<<lst);
				dp[i]+=dp[j]*a[now][lst]/(siz*(siz+1)/2);
			}
		}
	}
	for(int i=1;i<(1<<n);i++){
		if(__builtin_popcount(i)==1)
			cout<<fixed<<setprecision(6)<<dp[i]<<' ';//偷懒的写法
	}
	return 0;
}

P6046 纯粹容器#

题意#

n 个容器排成了一行,每个容器有一个强度值。

下面会进行进行 n1 轮操作,每轮操作等概率随机挑选两个相邻且未被击倒的容器进行决斗,强度小的容器会被击碎。

求每个容器存活轮数的期望,未被击碎的轮数。

做法#

观察题解,得到一个美妙的公式:

E(x)=i=1ni×p(x=i)=i=1nj=1ip(x=i)=j=1ni=jnp(x=i)=j=1np(xi)

也就是说,我们不用去求一个容器恰好在第 i 轮被击碎的概率了,只需要求出一个容器在第 i 轮仍未被击碎的概率。

容易发现,对于一个容器 i,他会被左边第一个比它大的容器的位置上的容器击碎,或者被右边第一个比它大的容器的位置的容器击碎。因为即使这个位置被击碎了,击碎它的容器也会来到这个位置。

那么不妨计左边第一个比它大的为 pre,右边第一个比它大的为 nxt。那么 i 被击碎的概率就是 ipre 这边被击碎完(事件 A),或者 inxt 被击碎完(事件 B)。

那么我们要算出 P(AB)=P(A)+P(B)P(AB)

计算可得:

P(A)=(n1dlidl)(n1i)P(B)=(n1dridr)(n1i)P(AB)=(n1dldridldr)(n1i)

按照上面的式子计算即可。

代码#

//c(a,b)表示a个中选b个
//inv(a)表示a的逆元
void init(){
	fact[0]=1;
	for(int i=1;i<=n;i++)fact[i]=(fact[i-1]*i)%mod;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int j=i-1;j>=1;--j){//左边第一个比它大的
				if(a[j]>a[i]){
					ld[i]=i-j;
					break;
				}
			}
			for(int j=i+1;j<=n;++j){//右边第一个比它大的
				if(a[j]>a[i]){
					rd[i]=j-i;
					break;
				}
			}
		}
	}
}
signed main(){
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	init();
	for(int i=1;i<=n;i++){
		int ans=0;
		for(int j=1;j<n;j++){
			int pa=0,pb=0,pab=0;
			if(ld[i]&&j-ld[i]>=0){//判断是否存在
				pa=c(n-1-ld[i],j-ld[i])*inv(c(n-1,j))%mod;
			}
			if(rd[i]&&j-rd[i]>=0){//判断是否存在
				pb=c(n-1-rd[i],j-rd[i])*inv(c(n-1,j))%mod;
			}
			if(ld[i]&&rd[i]&&j-ld[i]-rd[i]>=0){//判断是否存在
				pab=c(n-1-ld[i]-rd[i],j-ld[i]-rd[i])*inv(c(n-1,j))%mod;
			}
			ans=(ans+1-pa-pb+(mod<<1)+pab)%mod;
		}
		cout<<ans<<' ';
	}
	return 0;
}

CF1042E Vasya and Magic Matrix#

题意#

一个nm列的矩阵,每个位置有权值ai,j

给定一个出发点,每次可以等概率的移动到一个权值小于当前点权值的点,同时得分加上两个点之间欧几里得距离的平方(欧几里得距离:(x1x2)2+(y1y2)2)),问得分的期望。

做法#

首先,按照 ai,j 从小到大将所有点排序。排序后将所有点重新编号(1inm)。

dpi 表示以 i 为起点的期望得分。那么转移显然就是遍历能到达的所有点,然后转移。显然转移式子为:

dpi=1j<lstdpj+(xixj)2+(yiyj)2

其中,lst 表示最靠前的,权值和 i 相等的位置。显然这样的复杂度显然是 Θ(n2) 的。

考虑如何快速转移。显然需要的 dp 值是一个前缀,记一个前缀和即可。

考虑如何处理每次转移的分数。容易发现(以 x 坐标为例):

k=1n(xxk)2=k=1nx22xxk+xk2=nx22xk=1nxk+k=1nxk2

然后直接用前缀和求出即可。

代码#

signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			a[++node].val=read()+1;
			a[node].x=i;
			a[node].y=j;
		}
	}
	sx=read(),sy=read();
	sort(a+1,a+1+node,cmp);
	for(int i=1;i<=node;i++){
		x[i]=(x[i-1]+a[i].x)%mod;
		x2[i]=(x2[i-1]+a[i].x*a[i].x)%mod;
		y[i]=(y[i-1]+a[i].y)%mod;
		y2[i]=(y2[i-1]+a[i].y*a[i].y)%mod;
	}
	for(int i=1;i<=node;i++){
		if(a[lst].val!=a[i].val)lst=i;
		int xx=((lst-1)*(a[i].x*a[i].x)%mod-2*(a[i].x*x[lst-1])%mod+x2[lst-1])%mod;
		int yy=((lst-1)*(a[i].y*a[i].y)%mod-2*(a[i].y*y[lst-1])%mod+y2[lst-1])%mod;
		dp[i]=(sum[lst-1]+xx+yy)*inv(lst-1)%mod;
		sum[i]=(sum[i-1]+dp[i])%mod;
		if(a[i].x==sx&&a[i].y==sy){
			cout<<dp[i];
			break;
		}
	}
	return 0;
}

CF453A Little Pony and Expected Maximum#

题意#

有一个 m 面的骰子,你要进行 n 次投掷,问投出点数最大值的期望。

做法#

考虑求出掷出 k(1km) 的概率。

n 次取值的总方案数为:mn

n 次,每次的值都小于等于 k 的方案数为:kn

n 次,每次的值都小于等于 k1 的方案数为:(k1)n

那么掷 n 次,至少有一次掷出的值等于 k 的方案数就是:kn(k1)n

所以最大值为 k 的概率就是:kn(k1)nmn

所以最大值的期望就是:k=1nkkn(k1)nmn

不难发现,这样肥肠容易溢出。所以考虑对这个式子进行化简:

k=1nkkn(k1)nmn=k=1nkknmn(k1)nmn=k=1nk[(km)n(k1m)n]

直接计算即可。

代码#

signed main(){
	m=read(),n=read();
	for(int i=1;i<=m;i++){
		ans+=1.0*i*(ksm(1.0*i/m,n)-ksm(1.0*(i-1)/m,n));
	}
	cout<<fixed<<setprecision(5)<<ans;//保留5位小数即可
	return 0;
}

#

这篇博客有点太长了,就先从这断开了。后面大概会写个Part2。

Part2链接


The End

posted @   洛谷Augury  阅读(181)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示