概率与期望
前言
因为本人数学不是很好,且本人比较弱,所以本文部分内容不保障正确性,不过我确实是推了很久,想了很久,以我的理解呈现出来的。
注意,本文主要分析期望 DP,概率 DP 的内容较少。
基础理论
一些定义
可以先大概了解,在后续需要用到相关定义的时候再翻回来看。
- $P(X)$ 表示事件 $X$ 发生的概率。
- $P(A|B)$ 表示事件 $A$ 在事件 $B$ 已经发生的条件下发生的概率。
- $P(X=X_i)$ 表示随机变量 $X$ 取到 $X_i$ 发生的概率。
- $E(X)$ 表示随机变量 $X$ 的期望。
- 互斥事件:如果事件 $A$ 和事件 $B$ 无法同时发生,那么 $A$ 和 $B$ 是互斥事件。
- 对立事件:如果事件 $A$ 和事件 $B$ 无法同时发生,并且要么 $A$ 发生,要么 $B$ 发生,那么 $A$ 和 $B$ 是对立事件。
- 相互独立事件:如果事件 $A$ 和事件 $B$ 之间互不影响(即 $A$ 发不发生和 $B$ 发不发生无关),那么 $A$ 和 $B$ 是相互独立事件。
- 样本点:一个随机现象中可能发生的不能再细分的结果。
- 样本空间:所有样本点组成的集合称为样本空间,用 $\Omega$ 表示;一个随机事件为 $\Omega$ 的子集,由若干样本点组成,用大写字母表示。
- 互斥性:事件组 $B_1, B_2, \ldots, B_n$ 中任意两个事件不能同时发生,即对于任意 $i \neq j$,有 $B_i \cap B_j = \emptyset$。
- 完备性:事件组 $B_1, B_2, \ldots, B_n$ 覆盖整个样本空间,即 $\bigcup_{i=1}^n B_i = \Omega$,且 $\sum_{i=1}^n P(B_i) = 1$。
- 互斥完备事件组:满足互斥性和完备性的事件组。
概率的性质
假设某对 $X$ 和 $Y$ 组合的概率为 $P(X=X_i, Y=Y_j)$,这里的关系是且。
1.边缘概率公式
$$P(X=X_i)=\sum_{j} P(X=X_i, Y=Y_j)$$
还是比较显然,可以感性理解为 $Y$ 把所有可能取值的概率都计算了(且这些取值是互斥的),那不就是 $X=X_i$ 的概率吗。
2.所有概率和为 $1$
$$\sum_{i} P(X=X_i)=1$$
比较显然吧。感性理解就好,我不知道咋解释了。
3.全概率公式
若事件组 $B_1, B_2, \ldots, B_n$ 是样本空间 $\Omega$ 的一个互斥完备事件组,则对于任意事件 $A$,有:
$$P(A) = \sum_{i=1}^n P(B_i) \times P(A | B_i)$$
本质是将一个复杂事件 $A$ 的概率,分解为多个简单事件 $B_i$ 条件下的概率加权和,权重为各简单事件的概率。
正确性也比较显然,因为互斥完备事件组,保证了事件 $A$ 发生的所有可能情况都被考虑且不重复,从而能够将 $A$ 的概率正确分解。
一般的概率 DP 都需要用到此公式计算。因为此公式把一个问题拆分成若干子问题,就是 DP 的思想。还是很重要的。
一些推广:
I.连续型随机变量情况:
对于连续型随机变量 $X$ 和 $Y$,全概率公式推广为:
$P(A) = \int_{-\infty}^{+\infty} P(X = x) \cdot P(A | X = x)$
II.多层全概率,可以将事件 $A$ 的概率分解为多个层次的条件概率乘积和:
$P(A) = \sum_{i} \sum_{j} P(B_i) \cdot P(C_j | B_i) \cdot P(A | B_i \cap C_j)$
4.贝叶斯公式
是全概率公式的逆用,用于在已知事件 $A$ 发生的条件下,求事件 $B_i$ 发生的概率:
$$P(B_i | A) = \frac{P(B_i) \cdot P(A | B_i)}{\sum_{j=1}^n P(B_j) \cdot P(A | B_j)} = \frac{P(B_i) \cdot P(A | B_i)}{P(A)}$$
其中,分母 $P(A)$ 就是全概率公式计算出的结果。
(部分内容先不解释了,我也不会,鸽子了)
来点简单应用!
袋中取球问题:一个袋子中有 $3$ 个红球和 $2$ 个白球。从中随机取出一个球扔掉,再随机取出一个球。求第二次取出红球的概率。
设事件 $B_1$ 为第一次取出红球的概率,事件 $B_2$ 为第一次取出白球的概率,事件 $A$ 为第二次取出红球的概率。
显然这两个事件构成了一个互斥完备事件组,然后就能用全概率公式了,也就是有:
$$
\begin{aligned}
P(A) &= P(B_1) \times P(A|B_1) + P(B_2) \times P(A|B_2) \\
&= \frac{3}{5} \times \frac{2}{4} + \frac{2}{5} \times \frac{3}{4} \\
&= \frac{3}{5} \\
\end{aligned}
$$
其实没啥特别的应用,主要还是为之后的期望准备一些理论基础。
期望的性质
1.期望的定义式
$$E(X) = \sum_i X_i \times P(X = X_i)$$
就是每种情况出现的概率乘上这种情况的权值。
本质就是本质是考虑概率权重后的平均取值,也就是加权平均数。
比如猜硬币,正面赢 $X_1=2$ 元(概率 $0.5$),反面赢 $X_2=0$ 元(概率 $0.5$),求赢钱数 $X$ 的期望。
$$
\begin{aligned}
E(X) &= X_1 \times P(X=X_1) + X_2 \times P(X=X_2) \\
&= 2 \times 0.5 + 0 \times 0.5 \\
&= 1
\end{aligned}
$$
意思是长期猜下去,平均每次能赢 $1$ 元。注意这是一个期望值,赢 $1$ 元在实际是取不到的。
2.期望的线性性
线性指的是加法和数乘(数乘可以用矩阵理解,一个数乘矩阵的时候就是数乘)。
1.和式的期望等于和式中所有项的期望之和,即:
$$E(X + Y) = E(X) + E(Y)$$
$$E(\sum_{i=1}^{n} X_i) = \sum_{i=1}^{n} E(X_i)$$
一种感性理解:设 $X$ 是你数学成绩(期望 $80$ 分),$Y$ 是语文成绩(期望 $90$ 分),那么数学语文总成绩 $X+Y$ 的期望就是数学成绩的期望与语文成绩的期望之和 $80+90=170$ 分。
证明如下:
假设 $X$ 有 $2$ 个取值 $X_1$,$X_2$,$Y$ 有 $2$ 个取值 $Y_1,Y_2$,所有组合的概率为 $P(X=X_i, Y=Y_j)$,这里的关系是且。
$$
\begin{aligned}
E(X+Y) &= \sum_{i}\sum_{j} (X_i+Y_j) \times P(X=X_i, Y=Y_j) \\
&= \sum_{i}\sum_{j} X_i \times P(X=X_i, Y=Y_j) + \sum_{i}\sum_{j} Y_j \times P(X=X_i, Y=Y_j) \\
&= \sum_{i} X_i \times \sum_{j} P(X=X_i, Y=Y_j)+\sum_{j} Y_j \times \sum_{i} P(X=X_i, Y=Y_j) \\
&= \sum_{i} X_i \times P(X=X_i) + \sum_{j} Y_j \times P(Y=Y_j) \\
&=E(X)+E(Y)
\end{aligned}
$$
对于 $X$,$Y$ 有多个取值证明逻辑是一样的,所以就证完了。
2.常数无论作为加数还是乘数,都可以提到外面,即:
$$E(X + c) = E(X) + c$$
$$E(aX + c) = aE(X) + c$$
$$E(aX + bY) = aE(X) + bE(Y)$$
$$E[\sum_{i=1}^{n} a_i X_i] = \sum_{i=1}^{n} a_i E(X_i) (a_i 为常数)$$
一种感性理解:若数学成绩 $X$ 的期望是 $80$ 分,此时乘以 $a=1.2$ 倍后再加 $5$ 分($c=5$),则新成绩的期望是 $1.2 \times 80 + 5 = 101$ 分。
证明如下:
$$
\begin{aligned}
E(aX+c) &= \sum_{i} (aX_i + c) \times P(X=X_i) \\
&= a \times \sum_{i} X_i \times P(X=X_i) + c \times \sum_{i} P(X=X_i) \\
&= aE(X) + c \\
\end{aligned}
$$
3.变量之间相乘是非线性的。因此对于两个变量相乘,期望不可拆解(只有相互独立时等式成立)。即:
$$E(XY) \neq E(X)E(Y)$$
一种感性理解:因为变量之间会相互影响,所以直接拆开就错了,因为你无法把影响体现出来。举例子懒得举了,自行举例即可。
证明不会。
3.全期望公式
若事件组 $B_1, B_2, \ldots, B_n$ 是样本空间 $\Omega$ 的互斥完备事件组(或随机变量 $Y$ 的所有可能取值),则对任意随机变量 $X$,其期望 $E[X]$ 可通过“条件期望加权求和”计算:
$$E[X] = \sum_{i=1}^n P(B_i) \times E[X | B_i]$$
全期望公式的本质可以简洁地表示为:
$$E[X] = E[E[X | Y]]$$
即“$X$ 的期望等于 $X$ 在 $Y$ 条件下的期望的期望”,所以全期望公式也被称为“期望的期望”。
其本质是「期望的线性性质」与「全概率公式的延伸」。期望 DP 往往是根据此公式把期望拆分成若干期望,所以还是很重要的。
严谨证明的话我不会,但是我会一些感性证明,这一点在后面的例题分析中可以体现。
4.期望与方差的关系
方差 $Var(X)$ 与期望之间的关系:
$$Var(X) = E(X^2) - (E(X))^2$$
根据方差定义:偏离期望的平方的期望,直接推式子就好。这里的 $E(X)$ 其实就是一个常量。
$$
\begin{aligned}
Var(X) &= E[(X-E(X))^2] \\
&= E[X^2 - 2XE(X) + (E(X))^2] \\
&= E(X^2) - E[2XE(X)] + (E(X))^2 \\
&= E(X^2) - 2E(X) \times E(X) + (E(X))^2\\
&= E(X^2) - 2(E(X))^2 + (E(X))^2 \\
&= E(X^2) - (E(X))^2\\
\end{aligned}
$$
例题分析
有了前面的理论铺垫,终于可以进入今天的正题,期望 DP 了。主要还是分析期望 DP,因为比较难理解。
有一些常规的套路解决这类 DP,这里以例题逐步分析。
例 1 绿豆蛙的归宿
给定一个起点为 $1$ ,终点为 $n$ 的有向无环图。到达每一个顶点时,如果有 $K$ 条离开该点的道路,可以选择任意一条道路离开该点,并且走向每条路的概率为 $\frac{1}{K}$ 。问从 $1$ 出发走到 $n$ 的路径期望总长度。保证从任意点出发最终一定能走到 $n$。
方法一:初态顺推
一种很容易想到的,比较人性的方法,但同时会麻烦一点。
首先,因为是个 DAG,直接拓扑排序加 DP 就行。
定义 $f_u$ 为从 $1$ 出发走到点 $u$ 的路径期望总长度。
接下来考虑转移,令 $G_u$ 表示经过一条边后到达 $u$ 的点集,$g_u$ 表示从 $1$ 出发到点 $u$ 的概率。
根据期望定义,不妨设 $f_v=\sum_i P(x=x_i) \times x_i$。
此时从 $v$ 经过 $w$ 的长度走到 $u$,那么可以得到:
$$
\begin{aligned}
f_u &= \sum_i P(x=x_i) \times \frac{1}{|G_u|} \times (x_i+w) \\
&= \frac{1}{|G_u|} \sum_i P(x=x_i) \times x_i + \frac{w}{|G_u|} \sum_i P(x=x_i) \\
&= \frac{1}{|G_u|} f_v + \frac{g_v}{|G_u|}w\\
\end{aligned}
$$
因为 $f$ 实际上就是期望,也就是根据期望和概率的各种性质推一推式子。
上述是考虑了转移一个点的情况,此时再到一般情况,可以得到转移式:
$$f_u=\sum\limits_{v\in G_u} \frac{1}{|G_u|}f_v + \frac{g_v}{|G_u|}w$$
然后对于 $g$ 用全概率公式算就行。
你还会发现这个转移式,是完全符合全期望公式的。所以我们在一种特定背景下证明了全期望公式。
那么其实下次直接考虑套用全期望公式求转移式就行了,不用再重推导一遍。
在实现时,为了方便可以直接刷表法,都一样的。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct node
{
int v, w;
};
int n, m, u1, v1, w1, d[N];
double f[N], g[N];
vector<node> a[N];
queue<int> q;
void top()
{
for (int i=1; i<=n; i++) if (!d[i]) q.push(i), g[i]=1.0;
while (!q.empty())
{
int u=q.front();
q.pop();
// printf("%d %.2lf %.2lf\n", u, f[u], g[u]);
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i].v, w=a[u][i].w;
f[v]+=1.0/a[u].size()*f[u]+1.0/a[u].size()*g[u]*1.0*w;
g[v]+=1.0/a[u].size()*g[u];
d[v]--;
if (!d[v]) q.push(v);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=m; i++)
{
scanf("%d%d%d", &u1, &v1, &w1);
a[u1].push_back({v1, w1}), d[v1]++;
}
top();
printf("%.2lf", f[n]);
return 0;
}
方法二:终态逆推
比较反直觉,但是很方便,主流的期望 DP 方法。
如果我们考虑把状态定义逆着来,定义 $f_u$ 为从 $u$ 出发走到点 $n$ 的路径期望总长度,也是可以做的。
这里推导过程和方法一是一样的。令 $G_v$ 表示 $v$ 经过一条边后可到的点的点集,$g_u$ 表示从 $u$ 出发到点 $n$ 的概率。
可以得到转移式:
$$f_v=\sum\limits_{u\in G_v} \frac{1}{|G_v|}f_u + \frac{g_u}{|G_v|}w$$
但是仔细观察可以发现,对于每个 $u$ 都满足 $g_u=1$,因为题目保证了从任意点出发最终一定能走到 $n$。
于是我们可以省去 $g_u$,省去了对于概率的处理,更加方便。
但是在方法一中,$g_u$ 就不一定为 $1$ 了。举个简单的例子,在样例中 $g_2=0.5$,原因就是从 $1$ 出发后会到达多个中间点,而这些中间点不一定能到 $u$,于是 $g_u$ 不一定为 $1$。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct node
{
int v, w;
};
int n, m, u1, v1, w1, d[N], cd[N];
double f[N];
vector<node> a[N];
queue<int> q;
void top()
{
for (int i=1; i<=n; i++) if (!d[i]) q.push(i);
while (!q.empty())
{
int u=q.front();
q.pop();
// printf("%d %.2lf\n", u, f[u]);
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i].v, w=a[u][i].w;
f[v]+=1.0/cd[v]*f[u]+1.0/cd[v]*1.0*w;
d[v]--;
if (!d[v]) q.push(v);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=m; i++)
{
scanf("%d%d%d", &u1, &v1, &w1);
a[v1].push_back({u1, w1}), d[u1]++, cd[u1]++;
}
top();
printf("%.2lf", f[1]);
return 0;
}
方法三:期望的线性性质
利用期望的线性性质,$E(X + Y) = E(X) + E(Y)$,所以另外一种求期望的方式是分别求出每一种代价产生的期望贡献,然后相加得到答案。
在本题中,路径期望总长度等于每条边产生的期望贡献之和。每条边产生的期望又等于经过这条边的概率乘这条边的代价。所以,只需要算出每条边的概率即可。
相当于直接把期望 DP 转化为了概率 DP。
这里边 $(u,v,w)$ 的经过概率不好直接计算,但如果能算出点 $u$ 的经过概率是 $f_u$ ,那么边 $(u,v,w)$ 的经过概率是 $f_u \times \frac{1}{|G_u|}$ ,对答案的贡献是 $w \times f_u \times \frac{1}{|G_u|}$ 。
算 $f$ 就是相当于全概率公式算。实现的时候可以刷表,然后又因为是刷表,边刷的时候边考虑这条边对答案的贡献就好,非常方便。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct node
{
int v, w;
};
int n, m, u1, v1, w1, d[N];
double f[N], ans=0;
vector<node> a[N];
queue<int> q;
void top()
{
for (int i=1; i<=n; i++) if (!d[i]) q.push(i), f[i]=1.0;
while (!q.empty())
{
int u=q.front();
q.pop();
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i].v, w=a[u][i].w;
f[v]+=f[u]/(1.0*a[u].size());
ans+=1.0*w*f[u]*1.0/a[u].size();
d[v]--;
if (!d[v]) q.push(v);
}
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=m; i++)
{
scanf("%d%d%d", &u1, &v1, &w1);
a[u1].push_back({v1, w1}), d[v1]++;
}
top();
printf("%.2lf", ans);
return 0;
}
方法四:期望的定义式
这题不太适合用此方法,不太好讲,具体见下面的例题。
有 $n$ 个人排成一列,每秒中队伍最前面的人有 $p$ 的概率走上电梯(一旦走上就不会下电梯),否则不动。问 $T$ 秒后,在电梯上的人数的期望。
方法一:初态顺推
考虑直接 DP,定 $f_{i,j}$ 表示一共有 $i$ 个人,过了 $j$ 秒电梯上的期望人数。
然后用全期望公式,考虑第 $i$ 个人上不上电梯,可以得出转移:
$$ f_{i,j}=p \times (f_{i-1, j-1}+1) + (1-p) \times f_{i, j-1}$$
就做完了,很简便。
我们思考一件事,为什么这题用初态顺推的方法,却不用额外记录一个概率呢?因为这里的概率 $p$ 是题目中直接给出的,于是我们并不需要额外记录。
#include <bits/stdc++.h>
using namespace std;
const int N=2005;
int n, t;
double p, f[N][N];
int main()
{
scanf("%d%lf%d", &n, &p, &t);
for (int i=1; i<=n; i++)
for (int j=1; j<=t; j++)
f[i][j]=p*(f[i-1][j-1]+1.0)+(1.0-p)*f[i][j-1];
printf("%.6lf", f[n][t]);
return 0;
}
方法二:终态逆推
定 $f_{i, j}$:前 $i$ 秒电梯外有 $j$ 个人,$i+1$ 到 $t$ 秒电梯上的期望人数。
因为我们只是想对期望进行逆推,所以只有计算期望的状态是逆过来的,其余状态都可以正着定义。
如何考虑 $i$ 上不上电梯,然后得出转移:
$$f_{i, j}=p \times f_{i+1, j-1}+(1-p) \times f_{i+1, j}$$
就做完了。
当然,不管是方法一还是方法二,状态定义成电梯上有多少人也是可行的。这里已经实践了,发现是可行的,不展开细说了。
#include <bits/stdc++.h>
using namespace std;
const int N=2005;
int n, t;
double p, f[N][N];
int main()
{
scanf("%d%lf%d", &n, &p, &t);
for (int i=t-1; i>=0; i--)
{
f[i][0]=f[i+1][0];
for (int j=1; j<=n; j++)
f[i][j]=p*(f[i+1][j-1]+1)+(1.0-p)*f[i+1][j];
}
printf("%.6lf", f[0][n]);
return 0;
}
/*
f_{i, j}:前 i 秒电梯外有 j 个人,i+1 到 t 秒电梯上期望人数。
f_{i, j}=p*f_{i+1, j-1}+(1-p)*f_{i+1, j}
*/
我们思考一件事,为什么终态逆推的时候概率和一定为 $1$ 呢?这个问题也困扰了我很久,现在给出一个感性理解。
得先明白初态,终态的详细定义到底是什么。
可以类比 DP 理解,初态就是 DP 初始化时的某一状态。终态就是 DP 最后取答案时的某一状态。
或者类比树理解,可以把转移的情况当成树,树上每一个点对应着期望,即状态;每条边对应着概率,即各种权值。
根就是初态,叶子结点就是终态,转移的方向就可以是从终态往起点推,也可以是起点往终态推,其实也对应着 DP 和记忆化搜索两种方向。
当然转移不一定能抽象成一棵树。有时候会是有向图,还有可能带环,会复杂一点,此时一般需要高斯消元。
一般而言初态唯一,而终态可能唯一,可能不唯一,形象的理解就是当成一颗树的时候,多个叶子节点即多个终态。
以下内容可以挂到树上理解:
全部终态的概率和为才 $1$,正推时,概率和并没有考虑到全部终态,所以不为 $1$。
但倒推时,终态最终一定能推回到初态,即所有终态都能回到所有初态,然后全部终态概率和为 $1$,于是所有初态的概率总和也为 $1$,但是因为只有一个初态,所以对于概率就是 $1$。
于是初态唯一,不管终态唯不唯一,都可以考虑终态逆推,如果要初态顺推,需要多记一个概率,当然如果题目已经给出就不必记录。
这件事确实很抽象,我花了不下 3 天来理解这件事,真的是需要刷多点题,再慢慢体悟。
方法三:期望的线性性质
并不适用,因为结束状态并不明确,不好拆分成若干状态的贡献。
方法四:期望的定义式
根据期望的定义式,$E(X) = \sum_i X_i \times P(X = X_i)$,相当于所有可能取值乘上其对应概率。然后直接算其可能的取值,和对应的概率就好。
这题中,$E(t)$ 表示表示过了 $t$ 秒后在电梯上的人数的期望,$P(i)$ 表示表示过了 $t$ 秒后在电梯上有 $i$ 人的概率。
那么有:
$$E(t) = \sum_{i=1}^{n} i \times P(i)$$
对于 $P(i)$,可以用全概率公式计算,即拆分成上一秒的若干情况,这其实就是类似方法一中的 DP。
定 $f_{i,j}$ 表示过了 $j$ 秒时有 $i$ 个人在电梯上的概率。有转移:
$$ f_{i,j}=(1-p) \times f_{i, j-1} + p \times f_{i-1, j-1}$$
就做完了,比较无脑。
回过头来分析下上一题为何不适用此方法。很简单,直接枚举所有路径,复杂度不能接受。
#include <bits/stdc++.h>
using namespace std;
const int N=2005;
int n, t;
double p, f[N][N], ans=0;
int main()
{
scanf("%d%lf%d", &n, &p, &t);
f[0][0]=1.0;
for (int i=1; i<=t; i++) f[0][i]=(1.0-p)*f[0][i-1];
for (int j=1; j<=t; j++)
{
for (int i=1; i<n; i++) f[i][j]=p*f[i-1][j-1]+(1.0-p)*f[i][j-1];
f[n][j]=f[n][j-1]+p*f[n-1][j-1];
}
for (int i=1; i<=n; i++) ans+=i*1.0*f[i][t];
printf("%.6lf", ans);
return 0;
}
练习
给定一个序列,一些位置未确定(o 与 x 的几率各占 $50\%$ )。对于一个 ox 序列,连续 $x$ 长度的 o 会得到 $x^2$ 的收益,求最终得到的序列的期望收益。
定 $f_i$ 表示序列前 $i$ 个位置的期望收益。然后从 $i$ 往前找到第一个为 x 的位置 $j$,考虑 $[j+1, i]$ 这样连续一段 o? 序列的期望收益,转移给 $f_i$ 即可。
但是复杂度不接受。
当可以维护期望的变化量时,可以用到一个期望常用 trick:把状态拆成类似前缀和的形式,然后转化为算期望的变化量,具体的就是设 $f_{st}$,表示从当前状态 $st$ 变成下一状态 $st'$ 的期望。原理就是期望的线性性质。
回到此题,就是设 $g_i$,表示从序列第 $i-1$ 位到序列第 $i$ 位的期望长度变化量。最后做一遍前缀和就算完了,即设 $f_i=\sum_{j=1}^{i} g_j$。
所以我们需要记录 $l_i$ 表示为以 $i$ 结尾的 comb 的期望长度。然后分类讨论即可。
根据期望的线性性质,全期望公式可以得出:
1. $s_i=$ o,$g_i=(l_{i-1}+1)^2-l_{i-1}^{2}=2l_{i-1}+1$ , $l_i=l_{i-1}+1$
2. $s_i=$ x,$g_i=0$ , $l_i=0$
3. $s_i=$ ?,$g_i=\frac{2l_{i-1}+1}{2}$ , $l_i=\frac{l_{i-1}+1}{2}+\frac{0}{2}$
#include <bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n;
char s[N];
double f[N], len[N];
int main()
{
scanf("%d%s", &n, s+1);
for (int i=1; i<=n; i++)
{
if (s[i]=='o') len[i]=len[i-1]+1, f[i]=f[i-1]+2*len[i-1]+1;
else if (s[i]=='x') len[i]=0, f[i]=f[i-1];
else len[i]=(len[i-1]+1)/2, f[i]=f[i-1]+(2*len[i-1]+1)/2;
}
printf("%.4lf", f[n]);
return 0;
}
/*
5
o?xoo
*/
给定一个序列,每个位置为 o 的几率为 $p_i$ ,为 x 的几率为 $1-p_i$。对于一个 ox 序列,连续 $x$ 长度的 o 会得到 $x^3$ 的收益,求最终得到的序列的期望收益。
上题加强版,也是考虑计算变化量。此题变化量形如 $(x+1)^3-x^3=3x^2+3x+1$,于是只需要维护 $l_i$,表示以 $i$ 结尾的 comb 的期望长度,$l2_i$,表示以 $i$ 结尾的 comb 的期望长度的平方。
注意 $E(X^2)\neq E^2(X)$ ,即 $l_i^2 \neq l2_i$,所以维护 $l2_i$ 是必要的。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n;
double p[N], f[N], len2[N], len[N];
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) scanf("%lf", &p[i]);
for (int i=1; i<=n; i++)
{
f[i]=f[i-1]+p[i]*(3*len2[i-1]+3*len[i-1]+1);
len2[i]=p[i]*(len2[i-1]+2*len[i-1]+1);
len[i]=p[i]*(len[i-1]+1);
}
printf("%.1lf", f[n]);
return 0;
}
给定一条链,链上存在若干条返祖边 $u_i \rightarrow v_i (u_i \ge v_i)$,求从 $1$ 到 $n$ 期望步数。
因为概率题目并没直接给出,如果初态顺推需要额外记录概率。所以我们直接考虑终态逆推。
定 $f_i$ 表示从 $i$ 走到 $n$ 的期望步数。但有返祖边的存在,这很难转移。
发现终态唯一,仍然沿用上题的 trick,计算变化量。
重新定 $f_i$ 表示 $i$ 走到 $i+1$ 的期望步数。因为转化成算变化量,那么正推倒推是无所谓的,但是有返祖边,于是考虑顺推去做。
然后考虑 $f_i$ 的转移式,就是考虑这一次走返祖边还是走链上的边。
我们方便推导,令 $E_{x\to y}$ 表示 $x$ 到 $y$ 的期望步数,$du_x$ 表示 $x$ 的返祖边的条数,$E$ 表示 $x$ 的返祖边的边集。
$$
\begin{aligned}
E_{x\to x+1} &= \frac{1}{du_x+1}\times1+\frac{1}{du_x+1}\sum_{(x,y)\in E} (E_{y\to x+1}+1) \\
&= \frac{1}{du_x+1}+\frac{1}{du_x+1}\sum_{(x,y)\in E}\sum\limits_{i=y}^{x}E_{i\to i+1}+\frac{1}{du_x+1}du_x\\
&= 1+\frac{1}{du_x+1}\sum_{(x,y)\in E}\sum\limits_{i=y}^{x}E_{i\to i+1}\\
\end{aligned}
$$
然后根据期望的线性性质,可以对变化量求前缀和优化。记 $sum_x=\sum\limits_{i=0}^x f_i$。
$$
\begin{aligned}
E_{x\to x+1} &= 1+\frac{1}{du_x+1}\sum_{(x,y)\in E} sum_x-sum_{y-1} \\
E_{x\to x+1} &= 1+\frac{1}{du_x+1}\sum_{(x,y)\in E} sum_{x-1}-sum_{y-1}+\frac{du_x}{du_x+1} E_{x \to x+1} \\
\frac{1}{du_x+1} E_{x\to x+1} &= 1+\frac{1}{du_x+1}\sum_{(x,y)\in E} sum_{x-1}-sum_{y-1} \\
E_{x\to x+1} &= du_x+1 +\sum_{(x,y)\in E} sum_{x-1}-sum_{y-1}
\end{aligned}
$$
推导一下式子,发现左右都含未知量,那就移项处理,最后整理即可。
甚至没有一个除法,所以直接大力取模没问题。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+5, Mod=998244353;
int id, n, m, u1, v1;
vector<int> a[N];
ll f[N], sum[N];
int main()
{
scanf("%d%d%d", &id, &n, &m);
for (int i=1; i<=m; i++)
{
scanf("%d%d", &u1, &v1);
a[u1].push_back(v1);
}
for (int i=1; i<=n; i++)
{
f[i]=a[i].size()+1;
for (int j=0; j<a[i].size(); j++) f[i]=(f[i]+sum[i-1]-sum[a[i][j]-1]+Mod)%Mod;
sum[i]=(sum[i-1]+f[i])%Mod;
}
printf("%lld", sum[n]);
return 0;
}
共有 $K$ 轮操作,一共有 $n$ 种物品,每一轮随机出现每一种物品,概率是 $\frac{1}{n}$ ,物品可选可不选,对于选每一种物品,必须要在前面的轮先选给定的部分物品,每一种物品的价格可正可负。求 $K$ 轮后按最优方案选择的期望价格。
发现物品数量很少可以状压,然后需要知道当前轮数。基本 DP 状态就确定了。然后思考正推还是倒推。
就算到了第 $K$ 轮选的物品具体选择也不唯一,也是多个终态。可以考虑倒序逆推。
定 $f_{i, s}$ 表示,前 $i$ 个物品选或不选构成集合 $s$,第 $i+1$ 到第 $K$ 轮的最优期望值。
也是和例题 2 同理,只需要考虑对期望进行逆着定义就好。
考虑转移,就是枚举当前随机出物品 $k$,然后考虑这个物品选不选。
但这题又有决策,又有期望,如何转移呢?对于有随机性的事情,算期望的需要累加。而对于可以自行决策的事情,我们算期望时可以取最值。
因为取最值就相当于最优决策,而随机事件我们无法决策,所以对于期望而言就是直接累加。
令 $mask_k$ 表示选 $k$ 这个物品必须先选 $mask_k$ 这些物品,根据上面我们推出转移式:
$$f_{i, s}= \sum\frac{1}{n}\begin{cases}max(f_{i+1, s}, f_{i+1, s|(1<<k)}+val_k) & (s \& mask_k)=mask_k \\
f_{i+1, s} & \text{otherwise} \\
\end{cases} $$
这里的概率就是 $\frac{1}{n}$,因为是倒推所以概率和为 $1$。
我们思考下如果是正推呢?那就不一定是 $\frac{1}{n}$ 了。
因为正推过程中,对于一个 $i$,其 $s$ 可能是不合法的,但这个状态又算一个终态数,转移过程中必须得减去那些不合法的终态数,那概率就不为 $1$ 了。
举个例子,正推可能会出现 $\frac{1}{3} \times f_{1, 100}+\frac{1}{3} \times f_{1, 001}+\frac{1}{3} \times f_{1, 101}\to f_{2, 101}$ 的情况,这并不是一个合法的转移。而这样 $\frac{1}{2} \times f_{1, 100}+\frac{1}{2} \times f_{1, 001} \to f_{2, 101}$ 才是一个合法的转移。也就是需要把不合法的终态数减去。但这个无法直接得到,需要额外记录,所以才会说正推过程中需要额外记录一个概率。
而倒推允许类似第一种转移的情况。所以概率和就是 $1$。这里在一种特定背景下解释了例题 2 中比较抽象的结论。
#include <bits/stdc++.h>
using namespace std;
const int N=105, M=50005;
int m, n, x, p[N], mask[N];
double f[N][M];
int main()
{
scanf("%d%d", &m, &n);
for (int i=1; i<=n; i++)
{
scanf("%d", &p[i]);
while (1)
{
scanf("%d", &x);
if (!x) break;
mask[i]|=(1<<x-1);
}
}
for (int i=m-1; i>=0; i--)
for (int j=0; j<(1<<n); j++)
{
for (int k=1; k<=n; k++)
{
if ((j&mask[k])==mask[k]) f[i][j]+=max(f[i+1][j|(1<<k-1)]+p[k], f[i+1][j]);
else f[i][j]+=f[i+1][j];
}
f[i][j]/=n;
}
printf("%.6lf", f[0][0]);
return 0;
}
/*
f[i][j]:前 i 个物品选或不选构成集合 s,第 i+1 到 k 轮的最优期望值。
*/
有 $n$ 个班级,班级间有距离,你要依次到每个班级,你有最多 $m$ 次换课机会让你从到班级 $c_i$ 转换到 $d_i$,但每次换课只有 $k_i\%$ 的概率能够成功,求班级 $1$ 到班级 $n$ 的距离总和的期望最小是多少?
首先这题图论部分和期望部分是独立的,可以先求个最短路,接着考虑期望 DP。
考虑你需要知道什么信息。显然需要记录到了哪个班级,用了几次换课机会。
还得记录你当前处于哪个班级中,这样你就能从上一个班级走过来。不过换到哪个班级是随机事件,无法直接记录,这样记录会很奇怪。但可以考虑记成在这个班级是否进行了换课操作。后续再考虑概率。
然后考虑正推还是逆推。发现题目把概率已经给你了,无需额外记录概率,直接考虑顺推。
$f_{i,j,0/1}$ 表示前 $i$ 个班级,用了 $j$ 次换课操作,第 $i$ 个班级用或没用换课操作,距离总和的期望最小值。
考虑转移,就是一个大力分讨,考虑当前班级用不用换课操作,上一个班级用不用换课操作即可,以及讨论换课是否成功。
这题和上题一样,期望中带有操作。对于你可以决策的,取最值算概率,随机事件,相加算概率即可。
具体一些就是:
考虑第 $i$ 次不换课,即 $f_{i, j, 0}$:
- 第 $i-1$ 次不换课,即 $f_{i-1,j,0}$,代价分为:
- $dis_{c_{i-1}, c_i}$
- 第 $i-1$ 次换课,即 $f_{i-1,j,1}$,代价分为:
- 第 $i-1$ 次申请成功:$p_{i-1} \times dis_{d_{i-1}, c_i}$
- 第 $i-1$ 次申请失败:$(1-p_{i-1}) \times dis_{c_{i-1}, c_i}$
综合两种情况,转移方程为:
$$f_{i,j,0} = \min\left( f_{i-1,j,0} + dis_{c_{i-1}, c_i},\ f_{i-1,j,1} + p_{i-1} \times dis_{d_{i-1}, c_i} + (1-p_{i-1}) \times dis_{c_{i-1}, c_i} \right)$$
考虑第 $i$ 次换课,即 $f_{i, j, 1}$:
- 第 $i-1$ 次不换课,即 $f_{i-1,j-1,0}$,代价分为:
- 第 $i$ 次申请成功:$p_i \times dis_{c_{i-1}, d_i}$
- 第 $i$ 次申请失败:$(1-p_i) \times dis_{c_{i-1}, c_i}$
- 第 $i-1$ 次换课,即 $f_{i-1,j-1,1}$,代价分为:
- $i-1$ 次成功且 $i$ 次失败:$ (1-p_i) \times p_{i-1} \times dis_{d_{i-1}, c_i}$
- $i-1$ 次成功且 $i$ 次成功:$p_i \times p_{i-1} \times dis_{d_{i-1}, d_i}$
- $i-1$ 次失败且 $i$ 次成功:$ p_i \times (1-p_{i-1}) \times dis_{c_{i-1}, d_i}$
- $i-1$ 次失败且 $i$ 次失败:$(1-p_i) \times (1-p_{i-1}) \times dis_{c_{i-1}, c_i}$
综合两种情况,转移方程为:
$$\begin{aligned}
f_{i,j,1} = \min\bigg( &f_{i-1,j-1,0} + p_i \times dis_{c_{i-1}, d_i} + (1-p_i) \times dis_{c_{i-1}, c_i}, \\
&f_{i-1,j-1,1} + (1-p_i) \times p_{i-1} \times dis_{d_{i-1}, c_i} + p_i \times p_{i-1} \times dis_{d_{i-1}, d_i} \\
&\quad + p_i \times (1-p_{i-1}) \times dis_{c_{i-1}, d_i} + (1-p_i) \times (1-p_{i-1}) \times dis_{c_{i-1}, c_i} \bigg)
\end{aligned}$$
#include <bits/stdc++.h>
using namespace std;
const int N=2005, M=305;
const double INF=1e18;
int n, m, tim, cnt, u1, v1, w1, dis[M][M], c[N], d[N];
double p[N], f[N][N][2], ans=0;
void floyd()
{
for (int k=1; k<=n; k++)
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
dis[i][j]=min(dis[i][j], dis[i][k]+dis[k][j]);
}
int main()
{
memset(dis, 63, sizeof dis);
scanf("%d%d%d%d", &tim, &cnt, &n, &m);
for (int i=1; i<=tim; i++) scanf("%d", &c[i]);
for (int i=1; i<=tim; i++) scanf("%d", &d[i]);
for (int i=1; i<=tim; i++) scanf("%lf", &p[i]);
for (int i=1; i<=n; i++) dis[i][i]=0;
for (int i=1; i<=m; i++)
{
scanf("%d%d%d", &u1, &v1, &w1);
dis[u1][v1]=min(dis[u1][v1], w1);
dis[v1][u1]=min(dis[v1][u1], w1);
}
floyd();
for (int i=0; i<=tim; i++) for (int j=0; j<=cnt; j++) f[i][j][0]=f[i][j][1]=INF;
f[1][0][0]=f[1][1][1]=0;
for (int i=2; i<=tim; i++)
for (int j=0; j<=cnt; j++)
{
double a=0, b=0;
//[i 换不换,i-1 换不换]
a=f[i-1][j][0]+dis[c[i-1]][c[i]]; //[0, 0]
b=f[i-1][j][1]+p[i-1]*dis[d[i-1]][c[i]]+(1-p[i-1])*dis[c[i-1]][c[i]]; //[0, 1]
f[i][j][0]=min(a, b);
if (j>=1)
{
a=f[i-1][j-1][0]+p[i]*dis[c[i-1]][d[i]]+(1-p[i])*dis[c[i-1]][c[i]]; //[1, 0]
b=f[i-1][j-1][1]+p[i]*p[i-1]*dis[d[i-1]][d[i]]+p[i]*(1-p[i-1])*dis[c[i-1]][d[i]]+(1-p[i])*p[i-1]*dis[d[i-1]][c[i]]+(1-p[i])*(1-p[i-1])*dis[c[i-1]][c[i]]; //[1, 1]
f[i][j][1]=min(a, b);
}
}
ans=INF;
for (int i=0; i<=cnt; i++) ans=min(ans, min(f[tim][i][0], f[tim][i][1]));
printf("%.2lf", ans);
return 0;
}
/*
f_{i, j, 0/1}:前 i 个教室,一共换了 j 次,第 i 次换不换。
*/
给定无向图 $G(V,E)$,每次操作为等概率随机添加一条非自环边 $(u,v)$,求出使得 $G$ 连通的期望操作次数。
$|V| \le 30,|E| \le 1000$
由于每次加边是等概率的,且每次加边要么不改变连通性,要么把两个连通块合并,就是说我们只关心图的连通状况。
注意到连通块大小和数量都很小,于是,用类似状压的技巧,暴力记录集合作为状态。
即状态记为每个连通块 $C_i$ 的大小的集合。设有 $k$ 个连通块,那么状态为 $\{|C_1|,|C_2|,\cdots,|C_k|\} $。
记当前状态 $S=\{|C_1|,|C_2|,\cdots,|C_k|\}$,为避免重复,规定 $|C_1|\le|C_2|\le\cdots\le|C_k|$。
发现初态唯一,终态不唯一,直接倒序 DP。
设 $g(S)$ 表示从当前状态开始到 $G$ 连通的期望次数,显然图 $G$ 连通就是 $\{n\}$,且有 $g(\{n\})=0$。
为了方便,我们记 $T=\dbinom{n}{2}$。
考虑转移,就是考虑每次随机从 $T$ 种可能的边中选出一条边进行加边操作,我们分类讨论:
- 边的两端在同一连通块内。
此时连通性不变,连通块大小不变,状态仍为 $S$。
这种边 $P$ 的数量为:每个连通块内部的边数之和,即:
$$P = \sum_{i=1}^k \dbinom{s_i}{2} = \sum_{i=1}^k \frac{s_i(s_i-1)}{2}$$
- 边的两端在不同连通块内。
此时两个连通块会合并,不妨直接枚举是哪两个连通块合并。记 $S'_{i,j}$ 为 $S$ 中选出两个连通块 $C_i,C_j$ 合并。即:
$$S'_{i,j}=\{|C_1|,|C_2|,\cdots,|C_{i-1}|,|C_{i+1}|,\cdots,|C_{j-1}|,|C_{j+1}|,\cdots,|C_k|,|C_i|+|C_j|\}(1\le i < j \le k) $$
这种边 $Q$ 的数量为:两连通块之间的边数,即:
$$Q=s_i s_j$$
再根据期望的线性性质,就可以得到下面的转移式:
$$
g(S) = 1 + \frac{P}{T} \cdot g(S) + \frac{Q}{T} \cdot g(S'_{i,j})
$$
上面的式子包含 $g(S)$ 自身,需要移项化简:
$$\begin{aligned}g(S) - \frac{P}{T} g(S) &= 1 + \frac{Q}{T} \cdot g(S'_{i,j}) \\
\frac{T-P}{T} g(S) &= 1 + \frac{Q}{T} \cdot g(S'_{i,j}) \\
g(S) &= \frac{T}{T-P} + \frac{Q}{T-P} \cdot g(S'_{i,j})
\end{aligned}$$
然后就做完了。具体实现的时候可以考虑记忆化搜索,相当于先从初态递归下去到终态,这个过程就是不断合并的过程,然后再递归回来计算。
直接 DP 需要倒序进行,不过也行,相当于不断拆分,就是先枚举拆分哪个数,再枚举拆分成的值,是一样的。不过没有记忆化搜索直观。
然后可以用 map 映射一个 vector,非常牛的实现方法。
关于复杂度分析:
状态 $S=\{|C_1|,|C_2|,\cdots,|C_k|\}$ 的数量等价于将 $n$ 拆分成 $k$ 个正整数之和的方案数 $f(n,k)$,其满足递推关系:
$$f(n,k)=\begin{cases}0, & n<k \\ 1, & k=1 \text{ or } n=k \\ f(n-1,k-1)+f(n-k,k), &\text{otherwise} \end{cases} $$
根据这个玩意算一下,会发现将 $n$ 拆成若干个正整数之和的方案数为 $p(n)=5604$,复杂度就是 $O(n^2p(n))$。
#include <bits/stdc++.h>
using namespace std;
const int N=35, M=50005;
int n, m, f[N], sz[N], u1, v1, T=0;
map<vector<int>, double> mp;
vector<int> st;
int find(int x)
{
if (f[x]==x) return x;
return f[x]=find(f[x]);
}
void merge(int x, int y)
{
int fx=find(x), fy=find(y);
if (f[fx]!=fy) f[fx]=fy, sz[fy]+=sz[fx];
}
double dfs(vector<int> vec)
{
if (vec.size()==1) return 0;
if (mp.find(vec)!=mp.end()) return mp[vec];
int P=0;
double ret=0;
for (int i=0; i<vec.size(); i++) P+=vec[i]*(vec[i]-1)/2;
ret=1.0*T/(T-P);
for (int i=0; i<vec.size(); i++)
for (int j=0; j<i; j++)
{
vector<int> v;
v=vec, v[i]+=v[j], v.erase(v.begin()+j);
sort(v.begin(), v.end());
ret+=1.0*vec[i]*vec[j]/(T-P)*dfs(v);
}
mp[vec]=ret;
return ret;
}
int main()
{
while (~scanf("%d%d", &n, &m))
{
st.clear(), mp.clear();
T=n*(n-1)/2;
for (int i=1; i<=n; i++) f[i]=i, sz[i]=1;
for (int i=1; i<=m; i++)
{
scanf("%d%d", &u1, &v1);
merge(u1, v1);
}
for (int i=1; i<=n; i++) if (f[i]==i) st.push_back(sz[i]);
sort(st.begin(), st.end());
printf("%.6lf\n", dfs(st));
}
return 0;
}
给定 $n \times m$ 的网格,每天随机往没有糖果的地方放一个糖果,每一行每一列都至少有一个糖果的时候结束放糖果,求结束放糖果经过天数的期望值。
容易发现我们不关心每行每列到底是如何摆糖果,只关心有几行,几列放了糖果,以及当前是是第几天。
按照此状态,发现终态不唯一。考虑倒推,比较容易。
也可以考虑拆成变化量算,因为存在一种情况是:在某一天就已经让每一行每一列都至少有一个糖果了,那么应该在此打住,不应该继续往后推。所以考虑算变化量可以方便做到。
也可以考虑方法四,从期望的定义式出发,期望 DP 转化成概率 DP。这里详细讲讲此做法,其余的做法如果能理解前面的例题应该不算难事。
可以定 $f_{i, a, b}$ 表示经过 $i$ 天,有 $a$ 行 $b$ 列上至少有一个糖果的概率。答案就是:
$$ \sum_{i=0}^{n\times m} i \times f_{i, n, m}$$
然后考虑刷表法会容易很多,因为在某一天就已经让每一行每一列都至少有一个糖果了,应该在此打住,不用当前概率往后更新。
考虑转移,就是考虑每天放糖果的操作,可能会多一行或多一列至少有一个糖果,这里分讨一下就好。具体如下:
$$f_{i+1,a,b} \leftarrow f_{i,a,b} \times \frac{a \times b-i}{n \times m-i}$$
$$f_{i+1,a+1,b} \leftarrow f_{i,a,b} \times \frac{(n-a) \times b}{n \times m-i}$$
$$f_{i+1,a,b+1} \leftarrow f_{i,a,b} \times \frac{a \times (m-b)}{n \times m-i}$$
$$f_{i+1,a+1,b+1} \leftarrow f_{i,a,b} \times \frac{(n-a) \times (m-b)}{n \times m-i}$$
然后就做完了。
#include <bits/stdc++.h>
using namespace std;
const int N=55, M=2505;
int n, m;
double f[M][N][N], ans=0;
int main()
{
scanf("%d%d", &n, &m);
f[0][0][0]=1;
for (int i=0; i<=n*m; i++)
for (int a=0; a<=n; a++)
for (int b=0; b<=m; b++)
{
if (a==n && b==m) continue;
if (a*b>=i) f[i+1][a][b]+=f[i][a][b]*(a*b-i)/(n*m-i);
f[i+1][a+1][b]+=f[i][a][b]*(n-a)*b/(n*m-i);
f[i+1][a][b+1]+=f[i][a][b]*a*(m-b)/(n*m-i);
f[i+1][a+1][b+1]+=f[i][a][b]*(n-a)*(m-b)/(n*m-i);
}
for (int i=1; i<=n*m; i++) ans+=i*f[i][n][m];
printf("%.9lf", ans);
return 0;
}
/*
f_{i, a, b}:前 i 天,有 a 行 b 列上有至少一个糖果的概率。
\sum_{i}^{nm} i \times f_{i, n, m}
*/
后记 & 总结
先放这么多例题吧。之后遇到好题再补。感觉已经能入门了,嘿嘿。
可能这只算是期望的冰山一角吧。其实还有很多套路没讲到,例如如果有后效性,需要高斯消元等技巧,贴个例题:P3232 [HNOI2013] 游走。不过我还没学高斯消元啥的,就先鸽子了。
稍微总结下,期望 DP 主要分 $4$ 种方法。
1. 正序顺推,在某些时候需要多记录一个概率,才能转移,比较麻烦,但胜在状态比较自然,不过不太推荐用,除非题目把概率已经给你了。
2. 倒序逆推,把跟期望有关的那一个状态逆过来定义,可以少记录一个概率,更方便,更推荐此方法,且通常的期望 DP 都用倒推解决。
3. 期望的线性性质,可以相当于以贡献的角度思考问题,也可以利用此性质计算变化量,把状态拆成类似前缀和的形式。
4. 期望的定义式,直接枚举一些量,然后用定义式计算,胜在无脑,不过复杂度需要考虑下。可以把期望 DP 转换成概率 DP 处理。
参考文献
本文很大一部分内容参考了网上各种资料,这里贴出链接:
https://www.cnblogs.com/jjjxs/p/18703208
https://www.zhihu.com/question/464263211
https://www.cnblogs.com/RioTian/p/15053904.html
https://www.cnblogs.com/shiranui/p/16858780.html#41-%E7%BB%BF%E8%B1%86%E8%9B%99%E7%9A%84%E5%BD%92%E5%AE%BF
https://www.cnblogs.com/toorean/articles/18597974
https://www.cnblogs.com/4BboIkm7h/articles/19164055
https://www.cnblogs.com/Cat-litter/articles/18378615
https://www.cnblogs.com/RioTian/p/14117154.html
以及洛谷上的各种题解。这里不一一列出。

浙公网安备 33010602011771号