数学期望 DP
https://notes.sshwy.name/Math/Expectation/Classic
https://ac.nowcoder.com/acm/contest/32282
对于一组离散型随机变量,出现其中某一变量的概率乘以这一变量值,再求和,就是数学期望。
也就是:
通过这个定义,我们可以感知到,所谓期望,其实表示的是一组离散型随机变量的平均水平。 也可认为是进行某件事能得到的平均结果,或者理想代价。所以它也可以叫做一组离散型随机变量的均值。这也是期望这个概念的实际意义。
关于概率的一些性质:
- 如果事件 和 为互斥事件(不会既满足 又满足 )那么 。
- 如果事件 和 为独立事件(两个事件不相关)那么 。
关于期望的一些性质:
-
证明: -
,X,Y 相互独立。
证明:
-
-
-
前缀和技巧:对于离散变量(取值只有整数),
其中,当的成立条件是 X,Y 相互独立(两个事件互不影响),这个式子反过来也是 X,Y 相互独立的判定条件。
以上是高中数学内容。在此重新提一下。
进行期望DP的时候,这些性质有时显得至关重要,可以帮助我们理解很多递推的转移。
ABC263E
题意:
有 个格子,在第 个格子上,第 个格子上有一个骰子,面值为 ,投一次骰子,这些面值均匀出现。投到几,就往前走几格。
从 号格子出发,求到 号格子所要投的骰子个数的数学期望。
分析:
对于线性期望 DP,通常有两种 DP 方式:
- 表示从起始状态到 状态的数学期望。
- 表示从 状态到起始状态的数学期望。
转移的时候,第一种方式通常从前往后转,需要起始状态已知。第二种方式通常是从后往前转,需要终止状态已知。有的时候这两种方式可以互换,有的时候不可以,需要灵活使用。
本题先考虑使用第 种方式,因为我们知道一个点可以前往哪些点,比较方便。
设 表示从 节点开始,到达 的数学期望。
先从有穷条件入手,即如果骰子可以投到 (这样不会出现在原地打转的情况,比较符合拓扑序,方便入手),那么应该怎么算。
显然,
(投到 每一种情况的概率是 ;不论什么情况从 都要多投 粒骰子)
如果是原题的无穷条件呢?在这时,虽然有些情况需要投掷的骰子数量会趋于无穷,但这时的概率成指数型增长。数学期望依然收敛。怎么计算呢?
照算不误。(by ajh 大佬)
我们假装 已经算好了。那么有以下式子,即为等量关系。所以可以划到同一边当作递推式。
把 提到左边有:
那么,
时间复杂度 ,使用后缀和优化。
从另一种角度思考。考虑这个人投了平均 次骰子都是 之后才投到不是 的。
那么有: ,其中 为在 处投到 的期望次数。(继承上面有穷条件的思路)
考虑求 。画个树状图看看。
发现刚好为 次的概率为 (前缀和技巧,至少 次 - 至少 次),刚好为 次的概率为 ,...,刚好为 次的概率为 。
那么 。
这个东西怎么求呢?它是无穷幂级数。这东西有判敛法,改天学学。现在只要知道怎么计算即可。
错位相减法。等比数列求和中用过。这题怎么用?令 ,则有:
右边是等比数列,用前 项和公式计算:
注意到 ,化简得
也就是
得到的式子和之前是一样的。
甚至我们可以直接猜出来在一个位置上期望投出 个骰子,这是有一个定理的:
【定理】
证明和上面类似,套公式即可。
使用这个定理,“投出不是 的” 的概率是 ,那么期望第 次发生。同样可以!
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1000000;
const int mod = 998244353;
int a[200010];
int dp[200010];
int suf[200010];
int qpow(int x, int k){
int ans = 1;
while(k) {
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//think twice,code once.
//think once,debug forever.
int n; cin >> n;
f(i ,1,n)cin>>a[i];
dp[n] = 0;
for(int i = n-1;i >= 1; i--){
int up = ((suf[i+1]-suf[i+a[i]+1])+a[i]+1+inf*mod) % mod;
int down = a[i];
dp[i] = up * qpow(down, mod - 2) % mod;
suf[i] = (suf[i+1]+dp[i])%mod;
}
cout << dp[1] << endl;
return 0;
}
拿球
-
箱子里有 个球 ,你要从里面拿 次球,拿了后放回,求取出的数字之和的期望。
一眼题,用到了 的定律。
-
箱子里有 个球 ,你要从里面拿 次球,拿了后不放回,求取出的数字之和的期望。
换个思路,设 为:
那么
那么
又
这个 就等于 ,那么原式还是等于 。
设置随机变量是非常重要的!这个问题中设置的是对答案的贡献。 -
箱子里有 个球 ,你要从里面拿 次球,拿了后以 的概率放回, 的概率放回两个和这个相同的球(相当于增加一个球),求取出的数字之和的期望。
设 为标号为 的球对答案的贡献,即被拿出来的次数乘以 。设 为被拿出来的次数。
那么
我们需要求的是 。这里不仅用了期望的线性加公式,还神来之笔地用到了 :
由于 (地位均等)
所以
那么代入原式得 。
总结:这三种游戏,不管是放回还是不放回还是放回两个,都是地位均等的,结论都是 。另外我们需要巧妙寻找设置元素的方案,使得解题更快。
C
一个骰子有 面,第一个面有一个点,第二面有两个点,以此类推。Twilight Sparkle 确定投掷骰子时,每一面都是等概率出现的,即每面出现的概率为 。并且她知道每次投掷的结果是独立的。帮助她计算投掷 次骰子所能获得最大值的期望。
分析:
前缀和套路,
概率独立可加性,
代回原式,得:
如果是可以取模,算到这里就行了,时间复杂度 。
不能取模的话把 sum 拆开来,发现是 减去一堆 ,具体是:
这东西先算 直接快速幂不会溢出。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long double ld;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
int m, n;
ld qpow(ld x, int k){
ld ans=1;
while(k){
if(k&1)ans=ans*x;
x=x*x;
k>>=1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//think twice,code once.
//think once,debug forever.
cin >> m >> n;
ld ans= m;
f(i, 1, m-1){
ans -= qpow((i*1.0/m), n);
}
cout << fixed<<setprecision(10)<<ans << endl;
return 0;
}
CF1754E
题意:有一个 01 序列,每次可以选择两个元素,如果为逆序则交换,否则不变,无论是否交换都算一次操作。问排好序的期望操作次数。
分析:
两个环节,第一个环节可能需要灵感,第二个环节是套路。
状态设计,容易想到使用 DP 计算,但状态并不是很好想。首先状态必须有单向性,必须有严格的 DP 顺序,于是我们可以想到用逆序对数来记录状态。然而,思考会发现并不可行。不仅是复杂度无法接受,仅用逆序对也无法完整地表示状态。实际上,我们可以用“未归位的数字个数”来作为状态。具体地,假设当前序列为 ,该序列一共有三个 0,所以前三位应该都是 0,而实际上有两个 1,就定义此时的“未归位的数字个数”为 。这个状态的单向性显然,完整性也容易证明,因为只有“归位”的交换是有意义的,如果没有达到“归位”的效果,无论是否交换都是没有意义的。
转移,首先考虑设计向前还是向后转。如果设计 表示走到有 个未归位的期望次数,那么 需要计算无穷级数不好转移。但是如果设计 表示走到有 个未归位之后期望还要几步,那么 就很好转移了。其次考虑转移方程式。最好用经典的“自己转到自己”的套路,并且牢记基本公式 。那么也就是:
稍微化简即可得到
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
const int mod = 998244353;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int dp[3000010];
int qpow(int x, int k) {
int ans =1;
while(k){
if(k&1)ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
int a[3000010];
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
int T; cin >> T;
while(T--) {
int n; cin >> n;
f(i, 0, n - 1) cin >> a[i];
int cnt = 0;
f(i, 0, n - 1) if(a[i] == 0) cnt++;
int cw = 0;
f(i, 0, cnt - 1) if(a[i] == 1) cw++;
f(i, 1, cw) dp[i] = (dp[i - 1] + n * (n - 1) % mod * qpow(2 * i % mod * i % mod, mod - 2) % mod) % mod;
cout << dp[cw] << endl;
}
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
P8774 爬树的甲壳虫
【题意】
有一只甲壳虫想要爬上一颗高度为 的树,它一开始位于树根, 高度为 ,当它尝试从高度 爬到高度为 的位置时有 的概率会掉回树根, 求它从树根爬到树顶时, 经过的时间的期望值是多少。
【分析】
设 表示还要爬多久。
有递推式 。
推完递推式别傻愣着,看看怎么处理!
由于 ,考虑递推。
考虑递推到 ,就可以出式子了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
int x[100100], y[100010];
const int mod = 998244353;
int p[100010], q[100010], c[100010];
int qpow(int x, int k) {
int ans = 1;
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
time_t start = clock();
//think twice,code once.
//think once,debug forever.
int n; cin >> n;
f(i, 0, n - 1) {
cin >> x[i] >> y[i];
p[i] = x[i] * qpow(y[i], mod - 2) % mod;
}
q[n-1]=p[n-1];
c[n - 1] = 1;
for(int i = n - 2; i >= 0; i--) {
q[i] = ((1 - p[i] + mod) % mod) * q[i + 1] % mod + p[i];
q[i] %= mod;
c[i] = (1 - p[i] + mod) %mod*c[i+1]%mod + 1;
}
cout << c[0] * qpow((1 - q[0] + mod) % mod, mod - 2) % mod;
time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
线性递推?观察式子!寻找出路!
相互推导又怎样?完全可以一起求。
P3232
【题意】
给定一个 个点 条边的无向连通图,顶点从 编号到 ,边从 编号到 。
小 Z 在该图上进行随机游走,初始时小 Z 在 号顶点,每一步小 Z 以相等的概率随机选择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的编号的分数。当小 Z 到达 号顶点时游走结束,总分为所有获得的分数之和。 现在,请你对这 条边进行编号,使得小 Z 获得的总分的期望值最小。
【分析】
首先,求出每条边期望经过的次数,然后从大到小赋权值即可。考虑 表示 这条边期望被走到的次数。考虑转化为点的期望次数这样才好做,那么有:
为什么呢?考虑走到 之后,有 的期望会走这条边。
其中 表示 的期望经过次数, 表示 的边数。
考虑 怎么求。
为什么呢?考虑走到 之后,有 的期望会走到这个点。
但是由于 在走到之后就不会再回来了。因此 不能计入。可以考虑令 。
刚开始必须走一次,而不是从别的地方走过来的。因此 有些特殊,要加 。
然后得到了 个元的线性方程组。考虑高斯消元。(又是一种处理多个变量同时推的式子的方案!)
UVA10288
【题意】
每张彩票上有一个漂亮图案,图案一共 种,如果你集齐了这 种图案就可以兑换大奖。
现在请问,在理想(平均)情况下,你买多少张彩票才能获得大奖的?
【分析】
首先,使用刚刚学过的 min-max 容斥可以发现答案为
其次,考虑集齐 种之后还需要 次才能收集到下一种,那么答案为 。
可以证明这俩等价。
P1654
【题意】
一共有 次操作,每次操作只有成功与失败之分,成功对应 ,失败对应 , 次操作对应为 个长度为 的 串。在这个串中连续的 个 可以贡献 的分数,这 个 不能被其他连续的 所包含。
现在给出 ,以及每个操作的成功率,请你输出期望分数,输出四舍五入后保留 位小数。
【分析】
这是一道高次期望问题。为什么强调高次呢?因为期望的线性性:非独立的两个期望不可以相乘,必须转化为相加和数乘的形式。
令 表示以 结尾的 1 串长度的立方的期望。
考虑 。这个式子并不成立。要转化为什么东西相加呢?
我们回到期望的本质。先观察一下平方的期望是什么意思。
为什么呢?考虑 应该等于 。其中 的概率是 ,而 的概率是 。于是我们由定义和性质推出了递推式。
期望题一定要首先知道哪一个值是期望,期望打开是什么,而不是凭感觉随便乱推。这道题的题解里我没有使用 而是使用了 这个显式表达,就是要不忘初心,才能推的对式子。
接下来,由期望的线性性,我们继续推式子。
这里注意 。()
于是我们考虑维护 和 。这个式子是这样推出来的,而不是靠玄学。
同样地,三次方的式子我们也可以推,结果是:
CF1737E. Ela Goes Hiking
只蚂蚁站成一排,第 只蚂蚁左边和第 只蚂蚁右边各有一个挡板,相邻两只蚂蚁的距离、第 只蚂蚁与左边挡板的距离和第 只蚂蚁与右侧挡板的距离相等。初始时每只蚂蚁重量相等,每只蚂蚁有 概率向左运动, 概率向右运动,每只蚂蚁速度相同。
蚂蚁之间会互相吃,一只蚂蚁吃掉另一只蚂蚁之后质量变为两只蚂蚁的质量之和,并保持速度和方向不变。方向改变当且仅当碰到了挡板,此时立刻改变方向。若两只蚂蚁相遇,重量大的蚂蚁会把重量小的蚂蚁吃掉,如果重量相同,向左运动的蚂蚁会吃掉向右运动的蚂蚁。求对于所有 ,第 只蚂蚁成为最终的存活者的概率对 取模。
首先考虑一个弱化版的问题:对于一个特定的初始运动方向序列,结果是谁会赢?
考虑一开始向右走的所有蚂蚁都是在送,对于一串 后面一个 ,所有 都会被这个 吃掉。特别地,最后一个如果是 ,也相当于 。因此可以把序列缩成一个全部向左走的串,其中每只蚂蚁的重量等于它前面连续 的个数。例如 。
然后,最前面的两只蚂蚁会打擂台似的一直决出一只继续走下去,例如
编号为 的蚂蚁赢了,当且仅当:
- 的方向是 ,除非 ,此时 都行。
- 缩起来的序列 中,假设 ,那么 。
- 对于 ,不满足 。
好。那么如今回到原题目来。我们会想到,对于条件 ,要求 ,那么 。因此,之前 个数都必须是 。
条件 是好处理的。
那么条件 怎么办?考虑从后向前,减去后面能赢,并且这一位填 的就行了。也就是说,对于每一个后面的 赢的概率 ,如果这一位可以填 ,那么这一位计算权值应该减去 。注意是先减再乘,因为只有这样才能过样例先减再乘的话,说明减掉的概率应该在“已经保证之前 个数都是 的全集”下。
进阶期望
期望题可以出的很牛逼,例如 P8967。这时候要灵活运用条件概率和条件期望相关的东西。
这里复述一下相关公式和定理。
概念
分别表示,当 成立时 的概率、期望。例如扔出两个骰子,考虑 为两个骰子的得数之和, 为第一个骰子的得数,那么 ,。
事件 ,因为 独立的时候 ,所以也会叫做事件 。
事件 ,因为 独立的时候 ,所以也会叫做事件 。
公式
条件概率公式:。
当 时, 独立。(独立的定义)
全期望公式: 需要保证。
现在我们结合 P8967 这个题来讲讲怎么应用这些公式求解东西。
P8967 追寻
【题意】
在 维空间中有一个梦想。这梦想坐落在 的地方。而你从 开始,开启寻梦的旅程。
你的步伐轻缓,每一步只能走一个单位长度。你并不知道你的梦想位于哪里,所以你只能随机选择 个正方向中的一个,然后向这个方向走一步。也就是说,在 中均匀随机选择一个正整数 ,然后,使你在第 维的坐标变成原来的坐标加一。
然而,天有不测风云。在你走每一步的过程中,你会有 的概率散入天际,并开始一段新的旅程。你会在 个地点中的一个重新开始这段旅程,其中第 个地点的坐标是 ,从这里重新开始的概率为 。
那么,期望下,你离到达这个梦想还需要多少步呢?
由于保证了 是随机生成的,可以说明以接近 的概率答案在模意义下存在。事实上,一个当 尚不确定时以合理地高的概率给出正确答案的算法足以通过本题,考察复杂的模意义下的有理数的处理不是我们的本意。
- ,。
- ,。
- 。
- ,。此即保证了 和 。
- 保证存在一个 使得对于每个 均有 。
- 保证每个 作为空间中的点互不相同。
- 保证每个 在所有可能的组合中等概率随机生成。
【分析】
只描述正解。
关键点只有 。考虑让初始生成点为 。
考虑每段路径都是形如:
考虑第 次迷失之后,到达终点或第 次迷失之前走的期望路程叫做第 段路程。(这个已经很巧妙了,就是采用了分段求解的思想)
那么答案为
考虑分解下去做。
存在第 段路程,也就是迷失了 次,我们先考虑这个东西怎么算。
先考虑当我们在某个关键点重生(条件),走过这一段路程是迷失还是到终点的概率。显然这两个事件的概率和等于 (条件概率的好处体现了:缩小概率空间的范围,使得全概率公式能用上)。考虑在第 个关键点重生,那么记 为到终点了(注意,这里只是到终点的概率,虽然路程固定为 ,但是需要满足两个因素:这些步不迷失,并且每一个维度走的是对的步数。注意不迷失不是条件!)的概率。
那么又迷失了的概率等于:。
令 。因此迷失一次之后(条件,比上一个弱,上一个是:在 点重生。但是 显然等于 。),又迷失一次的概率 等于:
那么
接下来考虑 怎么求。显然是分两个情况。令 表示确定这一次迷失的条件下的期望步数。
当 的时候,,否则 。
只需求 即可。
利用期望的线性性或者直接手算得到 。(期望的线性性怎么用,乱走的减去到终点之后乱走一段的。这是高级思想,希望早日会用。)
那么就做完了。最后的式子是:
注意多项式定理的组合数预处理可以线性。足够通过本题。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i, a, b) for(int i = (a); i <= (b); i++)
#define cl(i, n) i.clear(),i.resize(n);
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 1e9;
#define cerr if(false)cerr
#define freopen if(false)freopen
#define watch(x) cerr << (#x) << ' '<<'i'<<'s'<<' ' << x << endl
void cmax(int &x, int y) {if(x < y) x = y;}
void cmin(int &x, int y) {if(x > y) x = y;}
const int mod = 998244353;
//调不出来给我对拍!
//n 100 k 10000 sum d_i 1e7 p = x / 10^8
int n,k; int d[110]; int a[10010][110]; int p[10010];
int A,B,ans; int q[10010]; int t[10010]; int dis[10010][110];
int jc[10000010], inv[10000010]; const int V = 1e7;
int qpow(int x,int y){
int ret = 1;
while(y) {
if(y&1) ret=ret*x%mod;
x=x*x%mod; y>>=1;
}
return ret;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(NULL);
cout.tie(NULL);
//freopen();
//freopen();
//time_t start = clock();
//think twice,code once.
//think once,debug forever.
cin>>n>>k; f(i,1,n)cin>>d[i]; int div=qpow(100000000,mod-2);
f(i,1,k){f(j,1,n){cin>>a[i][j];} cin>>p[i]; p[i]*=div; p[i]%=mod;}
f(i,0,k)f(j,1,n){dis[i][j]=d[j]-a[i][j];dis[i][0]+=dis[i][j];}
jc[0]=inv[0]=1;f(i,1,k){p[0]+=p[i]; p[0]%=mod;}int invp=1*qpow(p[0],mod-2);
f(i,1,V+1) {jc[i]=jc[i-1]*i%mod;} inv[V+1]=qpow(jc[V+1],mod-2);
for(int i=V;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
f(i,0,k){
bool keda=1; f(j,1,n){if(dis[i][j]<0)keda=0;}
if(keda){q[i]=jc[dis[i][0]];f(j,1,n){q[i]*=inv[dis[i][j]]; q[i]%=mod;}
q[i]*=qpow(n, dis[i][0] * (mod-2)); q[i]%=mod;//continue;
q[i]*=qpow(1-p[0]+mod,dis[i][0]); q[i]%=mod; }
t[i]=(invp-(q[i]*(invp+dis[i][0])%mod)+mod)%mod;
t[i]*=qpow(1-q[i]+mod,mod-2); t[i]%=mod;
}//return 0;
f(i,1,k){A+=(p[i]*qpow(p[0],mod-2)%mod)*(1-q[i]+mod)%mod; A%=mod;}
f(i,1,k){B+=(p[i]*qpow(p[0],mod-2)%mod)*(q[i]*dis[i][0]%mod+(1-q[i]+mod)*t[i]%mod)%mod;B%=mod;}
ans=(1-q[0]+mod)*B%mod*qpow(1-A+mod,mod-2)%mod+q[0]*dis[0][0]%mod+(1-q[0]+mod)*t[0]%mod;
cout<<ans%mod<<endl;
//time_t finish = clock();
//cout << "time used:" << (finish-start) * 1.0 / CLOCKS_PER_SEC <<"s"<< endl;
return 0;
}
/*
2023/x/xx
start thinking at h:mm
start coding at 11:17
finish debugging at h:mm
*/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)