学记笔记 $\times$ 巩固 · 期望泛做$Junior$

时间:2022-03-14 16:02:44

最近泛做了期望的相关题目,大概\(Luogu\)上提供的比较简单的题都做了吧\(233\)

好吧其实是好几天之前做的了,不过因为太颓废一直没有整理……

\(Task1\) 期望的定义

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

需要注意的是,期望值并不一定等同于常识中的“期望”——“期望值”也许与每一个结果都不相等。期望值是该变量输出值的平均数。期望值并不一定包含于变量的输出值集合里。

大数定律规定,随着重复次数接近无穷大,数值的算术平均值几乎肯定地收敛于期望值。

——\(baidu\)百科

\(hhh\)其实意思就是概率的加权(平均值),此处需要注意,这个平均值是逻辑上的而并非运算上的

\(Task2\) 期望的水题们

\(\rm{\color{red}{Description \cdot 1}}\)

\(\color{skyblue}{LuoguP3802}\)

现在帕琪有7种属性的能量晶体,分别为\(a_1,a_2,a_3,a_4,a_5,a_6,a_7\)(均为自然数),每次释放魔法时,会随机消耗一个现有的能量晶体,然后释放一个对应属性的魔法。

现在帕琪想知道,她释放出帕琪七重奏的期望次数是多少,可是她并不会算,于是找到了学OI的你

其中帕琪七重奏的触发条件是:连续释放的7个魔法中,如果魔法的属性各不相同,就能触发一次帕琪七重奏。

\(\rm{\color{red}{Solution \cdot 1}}\)

这个题中,由于权值是“次数”,所以就直接忽略掉,当作概率做。那么接下来考虑,对于一个任意的排列来讲,他满足条件的概率大概可以如下表示:

设\(N = \sum a_i\),那么我们的答案大概可以写成$$P(N) \cdot \frac{a_1}{N} \frac{a_2}{N-1} \frac{a_3}{N-2} \frac{a_4}{N-3} \frac{a_5}{N-4} \frac{a_6}{N-5} \frac{a_7}{N-6}$$其中\(P(N)\)表示排列个数。那么我们思考,题目要求的是连续,而在\(N\)个元素中,总共有\(N-6\)个连续区间长度为\(7\)的区间,每个区间还有\(7!\)种排列,所以\(P(N) = 7!(N - 6)\)那么答案就很简单了。


简单总结一下,这个地方有个坑,就是你不知道为啥要乘上一个\(N-6\),因为题目中要求的是连续所以这样——这很浅显。我们这个地方需要拐过的一个弯是,概率已知的情况下,我们需要考虑全部情况而不是合法情况,这是萌新的一个坑(比如我当时就思索了好久\(stO\))。

还有一个地方需要仔细理解,就是我们的

#include <cstdio>
#include <iostream> using namespace std ;
double N, A1, A2, A3, A4, A5, A6, A7 ; int main(){
cin >> A1 >> A2 >> A3 >> A4 >> A5 >> A6 >> A7 ;
N = A1 + A2 + A3 + A4 + A5 + A6 + A7 ;
printf("%.3lf", 5040.0 * A1 / N * A2 / (N - 1) * A3 / (N - 2)
* A4 / (N - 3) * A5 / (N - 4) * A6 / (N - 5) * A7 ) ;
}

$\rm{\color{red}{Description \cdot 2}} $

\(\color{skyblue}{LuoguP4316}\)

给出一个有向无环图,起点为\(1\)终点为\(N\),每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果有K条离开该点的道路,绿豆蛙可以选择任意一条道路离开该点,并且走向每条路的概率为 \(\frac{1}{K}\) 。 现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?

$\rm{\color{red}{Solution \cdot 2}} $

……这就是个水题……由于是\(DAG\),所以我们完全可以计算出到达每个点的概率,乘上边权即可。

……实在没啥好说的了\(qwq\)……不理解为什么某谷定的难度是提高组难度\(233\)

#include <cstdio>
#include <iostream>
#define MAXN 100010 using namespace std ;
struct edge{
int to, next ; double v ;
}e[MAXN << 1] ; int head[MAXN], cnt ;
int N, M, A, B, C, i ; double Deg[MAXN], Ans ; inline void add(int u, int v, double w){
e[++cnt].to = v, e[cnt].v = w ;
Deg[u] ++, e[cnt].next = head[u], head[u] = cnt ;
}
inline void dfs(int now, double dist){
dist /= Deg[now] ;
for(int k = head[now] ; k ; k = e[k].next){
Ans += e[k].v * dist ;
dfs(e[k].to, dist) ;
}
}
inline int qr(){
int k = 0 ; char c = getchar() ;
while (c < '0' || c > '9') c = getchar() ;
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k ;
}
int main(){
cin >> N >> M ;
for(i = 1; i <= M; ++ i )
A = qr(), B = qr(), C = qr(), add(A, B, C) ;
dfs(1, 1) ;
printf("%.2lf", Ans) ;
}

\(\rm{\color{red}{Description \cdot 3}}\)

\(\color{skyblue}{LuoguP1297}\)

好的,这个题不是那么的水,是一个不错的题。

$\rm{\color{red}{Solution \cdot 3}} $

我们思考两个思路:

·$ 1$

我们考虑如果所有的\(a_i\)都相等,那么做对每个题的概率都还是$\frac{1}{a_i} $

好像很显然?但是我们算的时候不是这么算的,我们考虑此时做对一道题的概率应该是\(P =\)选对第一项\(+\)选对第二项\(+ \cdots +\) 选对第\(a_i\)项。那么我们思考,他做对第\(i\)个题的概率就是\(\frac{\min(a_i, a_{i + 1})}{a_i \times a_{i + 1}}\)也就是说,答案就应该是\(\sum\frac{1}{a_i^2}\) ,就是\(\frac{a_i}{a_i^2}\)

那么如果不相等呢?

很显然,会有一些项变成\(0\),再仔细想一下的话就可以发现做对第\(i\)道题的概率是\(min(a_i,a_{i+1}) \cdot \frac{1}{a_i \cdot a_{i+1}}\)

好的,由于权值是单位\(1\),所以这就做完了,对于\(i=n\)的情况特判即。

但是这个式子可以继续化简,大概经历这么一个简单的过程:

\(\begin{align}min(a_i,a_{i+1}) \cdot \frac{1}{a_i \cdot a_{i+1}} &=min(a_i,a_{i+1}) \cdot \frac{1}{max(a_i,a_{i+1}) \cdot min(a_i,a_{i+1})} \\ &=\frac{1}{max(a_i,a_{i+1}) }\end{align}\)

可以使常数小一点\(233\)

·$ 2$

我们用一种玄学的思路思考,那么不妨\(yy\)一个结论\(:\) 一组选项中,选项数量少的一定可以不去管,所以概率为\(\frac{1}{max(a_i,a_{i+1}) }\)

那这个结论为什么是对的呢?我们思考,其实选项数量少的选项在决策过程中,不会产生任何贡献,因为如果\(a_i>a_{i+1}\),那么我们即使在\(a_{i+1}\)种情况里选对了,也不一定会在\(a_i\)情况里面选对;反之亦然。

好的,猜结论是种技巧——\(zhx\)

其实根本不用猜吧,这个题这么水

#include <cstdio>
#include <iostream>
#define MAXN 10001000 using namespace std ;
double Ans = 0.0 ;
long long N, A, B, C, Aa[MAXN], i ; int main(){
scanf("%d%d%d%d%d", &N, &A, &B, &C, Aa + 1);
for (i = 2 ; i <= N; ++ i)
Aa[i] = ((long long)Aa[i - 1] * A + B) % 100000001 ;
for (i = 1; i <= N ; ++ i) Aa[i] = Aa[i] % C + 1 ;
for (i = 1; i < N ; ++ i) Ans += (double)1.0 / max(Aa[i], Aa[i + 1]) ;
Ans += (double)1.0 / max(Aa[N], Aa[1]) ;
printf("%.3lf", Ans) ;
}

\(Task ~3~~\) 期望\(\rm{DP}\)

其实期望\(DP\)虽然各大\(OJ\)上给的\(Label\)比较高,但实质上考察的就是比较简单的状态转移而已——无非就是转移过程中把概率的因素算在其中了。

比如说一道小水题:

\(\rm{\color{red}{Description \cdot 4}}\)

\(Link\)

某一天WJMZBMR在打osu

我们来简化一下这个游戏的规则

有nn次点击要做,成功了就是o,失败了就是x,分数是按combo计算的,连续\(a\)个combo就有\(a\times a\)分,combo就是极大的连续o

比如ooxxxxooooxx,分数就是$ 2\times 2 + 4 \times 4 = 4 +16=20$。

Sevenkplus闲的慌就看他打了一盘,有些地方跟运气无关要么是o要么是x,有些地方o或者x各有50%的可能性,用?号来表示。

比如oo?xx就是一个可能的输入。 那么WJMZBMR这场osu的期望得分是多少呢?

比如oo?xx的话,?o的话就是oooxx$ => 9$,是x的话就是ooxxx \(=> 4\)

期望自然就是\((4+9)/2 =6.5\)了

\(\rm{\color{red}{Solution \cdot 4}}\)

其实是一个很简单的题,我们只需要思考状态是怎么转移的即可。

很显然我们应该考虑多一个字符的时候,会对原序列产生什么新的影响。

我们不妨记\(dp_i\)为期望的得分(答案),\(Len_i\)表示到\(i\)位置为止的连续的o的期望长度。

如果新的是o,那么我们很简单的可以得出来:

\(Len_i = Len_{i - 1} + 1,\) 这个结论是平凡的。那么对于\(dp_i\),我们考虑,由于本题的权值是平方和,所以说我们的\(dp_i = dp_{i - 1} + 2 * Len_{i - 1} * 1 + 1\)(完全平方公式)

那么如果新的是x,那么我们的状态就要“清零”:

\[dp_i = dp_{i -1}, Len_i = 0
\]

如果新的是?,那么我们只需要把第一个方程乘上0.5即可

\(Len_i = \frac{Len_{i - 1} + 1}{2},dp_i = dp_{i - 1} + Len_{i - 1} + 0.5\)

#include <cstdio>
#include <iostream>
#define MAXN 10001000 using namespace std ;
double Ans = 0.0 ;
long long N, A, B, C, Aa[MAXN], i ; int main(){
scanf("%d%d%d%d%d", &N, &A, &B, &C, Aa + 1);
for (i = 2 ; i <= N; ++ i)
Aa[i] = ((long long)Aa[i - 1] * A + B) % 100000001 ;
for (i = 1; i <= N ; ++ i) Aa[i] = Aa[i] % C + 1 ;
for (i = 1; i < N ; ++ i) Ans += (double)1.0 / max(Aa[i], Aa[i + 1]) ;
Ans += (double)1.0 / max(Aa[N], Aa[1]) ;
printf("%.3lf", Ans) ;
}

\(\rm{\color{red}{Description \cdot 5}}\)

\(Link\)

换教室……童年噩梦233

简化题目,其实就是说,给定一张无向图,我们一开始在每个时刻都必须要去一个点,或者通过一定概率去另外一个点(有次数限制),最后求最短路。

\(n,m \leq 2000, v \leq 300\)

\(\rm{\color{red}{Solution \cdot 5}}\)

首先考虑,无论怎么移动,我们必须要走最短路,这是显然的,观察数据范围,可知要用弗洛伊德。

我们接着考虑,我们的状态一定要是二维的——\(dp_{i,j}\)表示在时刻\(i\)时,换了\(j\)次教室的最短距离期望。但是这样会产生状态表述不清的嫌疑——所以我们再加一维,\(dp_{i, j, 0/1}\)表示表示在时刻\(i\)时,换了\(j\)次教室,本次换/不换的最短距离期望。

所以我们的转移呢?我们考虑只能从上一个时刻转移过来,那么一共由乘法原理推出,有四种转移方式:

  • 这次换,上一次不换;
  • 这次不换,上一次不换;
  • 这次换,上一次换;
  • 这次不换,上一次换;

那么转移的时候,换的概率自然是\(p_i\),而不换的概率我们可以形式化地记为\((1- p_i)\),那么转移就比较简单了~

值得注意的是,我们求的是期望,所以对于除了两次都不换的情况其他所有情况,我们都要把所有可能都考虑在内。那么转移时的方程大概是这样:

for(int i=2;i<=n;i++){
for(int j=0;j<=min(m,i);j++)
{
dp[i][j][0]=min(dp[i-1][j][0] + f[c[i-1]][c[i]], dp[i-1][j][1] + f[d[i-1]][c[i]] * p[i-1] + f[c[i-1]][c[i]] * (1-p[i-1]));
if(!j) continue ;
dp[i][j][1]=min(dp[i - 1][j - 1][0] + f[c[i-1]][d[i]] * p[i] + f[c[i - 1]][c[i]] * (1 - p[i]),
dp[i - 1][j - 1][1] + f[c[i-1]][c[i]] *(1 - p[i - 1]) * (1 - p[i]) + f[c[i-1]][d[i]] * (1 - p[i-1]) * p[i] + f[d[i-1]][c[i]] * (1 - p[i]) * p[i - 1] + f[d[i-1]][d[i]] * p[i-1] * p[i]) ; //四种情况
}
}

虽然很麻烦的样子,但是思路是很清晰的233

不知道我什么时候可以有这样清晰的头脑啊233

\(\rm{Code}\)

#include<iostream>
#include<cstdio>
#define qwq register
#define MAXN 3010 using namespace std; int a[MAXN][MAXN], c[MAXN], d[MAXN] ;
double p[MAXN], f[MAXN][MAXN], dp[MAXN][MAXN][2] ; inline double min(double a,double b){ return a < b ? a : b ;}
inline int qread(){
int k = 0 ; char c = getchar() ;
while(!isdigit(c)) c = getchar() ;
while(isdigit(c)) k = (k<<1)+(k<<3)+c-48, c = getchar();
return k ;
}
inline double qread_double(){
double k=0;char c=getchar();
while(!isdigit(c))c=getchar();
while(isdigit(c))k=k*10+(c-48),c=getchar();
if(c=='.'){
double base=0.1;c=getchar();
while(isdigit(c))k=k+(c-48)*base,base/10,c=getchar();
}
return k ;
}
int main(){
int n,m,v,e,a1,b1,c1;
cin >> n >> m >> v >> e ;
for(qwq int i = 1 ; i <= n ; ++ i) c[i]=qread();
for(qwq int i = 1 ; i <= n ; ++ i) d[i]=qread();
for(qwq int i = 1 ; i <= n ; ++ i) cin>>p[i] ; for(qwq int i=1;i<=v;i++)
for(qwq int j=1;j<i;j++)
f[i][j] = f[j][i] = 1926081700 ;
for(qwq int i=1;i<=e;i++){
a1=qread(),b1=qread(),c1=qread();
f[a1][b1] = f[b1][a1] = min(f[a1][b1],c1) ;
}
for(qwq int k=1;k<=v;k++)
for(qwq int i=1;i<=v;i++)
for(qwq int j=1;j<i;j++)
if(f[i][k]+f[k][j]<f[i][j])
f[i][j]=f[j][i]=f[i][k]+f[k][j]; for(qwq int i=1;i<=n;i++)
for(qwq int j=0;j<=m;j++)
dp[i][j][0]=dp[i][j][1]=999999999; dp[1][0][0]=dp[1][1][1]=0;
for(qwq int i=2;i<=n;i++){
double add1=f[c[i-1]][c[i]];
for(qwq int j=0;j<=min(m,i);j++)
{
dp[i][j][0]=min(dp[i-1][j][0]+add1,dp[i-1][j][1]+f[d[i-1]][c[i]]*p[i-1]+f[c[i-1]][c[i]]*(1-p[i-1]));
if(j!=0)
dp[i][j][1]=min(dp[i-1][j-1][0]+f[c[i-1]][d[i]]*p[i]+f[c[i-1]][c[i]]*(1-p[i]),dp[i-1][j-1][1]+f[c[i-1]][c[i]]*(1-p[i-1])*(1-p[i])+f[c[i-1]][d[i]]*(1-p[i-1])*p[i]+f[d[i-1]][c[i]]*(1-p[i])*p[i-1]+f[d[i-1]][d[i]]*p[i-1]*p[i]);
}
} double hahaha=9999999999;
for(int i=0;i<=m;i++){
hahaha=min(dp[n][i][0],min(dp[n][i][1],hahaha));}
printf("%.2lf",hahaha);
}

\(\rm{Task ~5}\) 不是很可做的期望题目

这个这个……我好像只做过一道不可做的……

\(\rm{\color{red}{Description \cdot 6}}\)

\(Link\)

康娜的线段树……是个好题……

大概题意就是,康娜有一棵线段树,支持区间加,但是她的区间加并不是打标记的区间加,而是暴力区间加:

	for(int i=l;i<=r;i++)T.change(1,1,n,i,addv);

看上去不是很耐撕,但是康娜想知道,如果每次在线段树区间加操作做完后,从根节点开始等概率的选择一个子节点进入,直到进入叶子结点为止,将一路经过的节点权值累加,最后能得到的期望值是多少?

\(\rm{\color{red}{Solution \cdot 6}}\)

此题思路借鉴\(Link\),在此表示敬意。

比较难的一道题了

每个叶子结点对答案的贡献值,其实就是从根结点到达这个叶子结点路径经过的所有点权值和,乘上\(\frac{1}{2^{dep}}\)。这是比较显然的。

那么我们求个和即可,那怎么求和呢?我们考虑利用线段树的性质:算完再除

我们考虑一个小的转化:$$\sum \frac{v_i}{2^{dep_i}}-> 2^D \cdot \sum v_i \cdot {2^{D - {dep_i}}} \text{【1】}$$

我们发现一个叶节点值增加\(x\),答案就会增加\(x \times \sum_{i=1}^{dep}{\frac{1}{2^{i-1}}}\), 发现这个东西就是个等比数列,很容易地化简为\(1 + 1 - \frac{1}{2^{dep-1}} = 2 - \frac{1}{2^{dep-1}} = \frac{2^{dep}-1}{2^{dep-1}}\)。所以我们最后再前缀和一下就好。

所以就可以最后再除。

而我们知道,单点修改时,我们是对一条链进行操作。所以这个东西可以直接前缀和,前缀和出链长,维护一个永久\(Ans\)记录答案即可。

代码:

#include <cstdio>
#include <iostream>
#include <algorithm>
#define MAXN 3000010
#define ll long long
#define ls(x) x << 1
#define rs(x) x << 1 | 1
#define mid ((l + r) >> 1) using namespace std ;
ll qwq, L, R, D, Ans, Len, N, M, i ;
ll base[MAXN], dep[MAXN << 2], High, T[MAXN << 2], S[MAXN] ; inline ll qr(){
ll k = 0 , f = 1 ; char c = getchar() ;
while (c < '0' || c > '9') {if (c == '-') f = -1 ; c = getchar() ;}
while (c <= '9' && c >= '0') k = (k << 1) + (k << 3) + c - 48, c = getchar() ;
return k * f ;
}
inline void _build(ll rt, ll l, ll r, ll deep){
if(l == r){
dep[l] = deep ; T[rt] = base[l] ;
High = max(High, dep[l]) ; return ;
}
_build(ls(rt), l, mid, deep + 1) ;
_build(rs(rt), mid + 1, r, deep + 1) ;
T[rt] = T[ls(rt)] + T[rs(rt)] ;
}
inline ll query(ll rt, ll l, ll r, ll deep, ll Tot){
if(l == r) return 1LL * ((T[rt] + Tot) * (1LL << deep)) ;
else return query(ls(rt), l, mid, deep - 1, Tot + T[rt])
+ query(rs(rt), mid + 1, r, deep - 1, Tot + T[rt]) ; // 简化运算,最后同时除以。
}
int main(){
cin >> N >> M >> qwq ;
for(i = 1 ; i <= N ; ++ i) base[i] = qr() ;
_build(1, 1, N, 1) ;
Ans += query(1, 1, N, High - 1, 0) ; Len = 1LL << (High - 1) ;
ll _Temp = __gcd(Len, qwq) ; qwq /= _Temp, Len /= _Temp ;
for(i = 1 ; i <= N ; ++ i)
S[i] = S[i - 1] + (1 << dep[i]) - 1 + (dep[i] != High) * ((1 << dep[i]) - 1) ;
//此处为预处理深度,因为是完全二叉树,所以要么深度等于树高 ,要么深度等于树高减一
//但其实,我们预处理的是有关于我们的优化【1】式的。所以我们在每一层还要再乘上一个$2^{dep}$。
for(i = 1 ; i <= M ; ++ i){
L = qr(), R = qr(), D = qr(), Ans += (S[R] - S[L - 1]) * D, printf("%lld\n", Ans / Len * qwq ) ;
return 0 ;
}

最后撒花花吧~~

学记笔记 $\times$ 巩固 · 期望泛做$Junior$