概率期望学习笔记
xtx1092515503
·
·
个人记录
我 爱 数 学
突然发现我不会概率期望,就来补题了。
I.[CTSC2018]假面
期望第一题,居然能独立做出来。
首先这个数据范围明显是暗示我们一个O(Qm+Cn^2)的算法可以过去。
我们设pos_{i,j}表示敌人i剩余血量为j的概率。
则当使用一个“锁定”技能后,就相当于对pos_i做了一个背包,单次复杂度O(m_i)。
再考虑“结界”技能。
我们设一个f_i,表示该技能使用时,恰有i个人有血的概率。这个可以O(n^2)背包搞出来。
然后,再对于每个i,O(n)地从这个背包中“删掉”它。
需要注意的是,当i一定有血的时候,这个“删掉”操作与其它情况不同,需要特判一下。
代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
return z;
}
int n,lim[210],q,pos[210][110],p[210],f[210],g[210],inv[210];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&lim[i]),pos[i][lim[i]]=1;
for(int i=1;i<=n;i++)inv[i]=ksm(i,mod-2);
scanf("%d",&q);
for(int qwq=1,a,b,c,d;qwq<=q;qwq++){
scanf("%d",&a);
if(a==0){
scanf("%d%d%d",&b,&c,&d),c=1ll*c*ksm(d,mod-2)%mod,d=(mod+1-c)%mod;
(pos[b][0]+=1ll*pos[b][1]*c%mod)%=mod;
for(int i=1;i<=lim[b];i++)pos[b][i]=(1ll*pos[b][i]*d+1ll*pos[b][i+1]*c)%mod;
}else{
scanf("%d",&b);
for(int i=1;i<=b;i++)scanf("%d",&p[i]);
for(int i=0;i<=b;i++)f[i]=0;
f[0]=1;
int zero=0;
for(int i=1;i<=b;i++){
c=pos[p[i]][0],d=(mod+1-c)%mod;
if(!c){zero++;continue;}
for(int j=i-zero;j>=0;j--){
f[j]=1ll*f[j]*c%mod;
if(j)(f[j]+=1ll*f[j-1]*d%mod)%=mod;
}
}
// printf("P:");for(int i=1;i<=b;i++)printf("%d ",pos[p[i]][0]);puts("");
// printf("F:");for(int i=0;i<=b;i++)printf("%d ",f[i]);puts("");
for(int i=1;i<=b;i++){
c=pos[p[i]][0],d=(mod+1-c)%mod;
c=ksm(c,mod-2);
for(int j=0;j<=b-zero;j++)g[j]=f[j];
if(c){
for(int j=0;j<b-zero;j++){
g[j]=1ll*g[j]*c%mod;
(g[j+1]+=mod-1ll*g[j]*d%mod)%=mod;
}
}
// printf("%d:GGG",i);for(int j=0;j<b;j++)printf("%d ",g[j]);puts("");
int res=0;
for(int j=0;j<=b-zero;j++)(res+=1ll*inv[j+zero+(c!=0)]*g[j]%mod)%=mod;
printf("%d ",1ll*res*d%mod);
}puts("");
}
}
for(int i=1;i<=n;i++){
int res=0;
for(int j=0;j<=lim[i];j++)(res+=1ll*pos[i][j]*j%mod)%=mod;
printf("%d ",res);
}
return 0;
}
II.[HAOI2012]高速公路
本题已经在我的任务列表里挂了1年多了
我们将这里的“期望”转成“\dfrac{\text{区间中任意两点间距离之和}}{\text{区间中选取两点的方案数}}。然后,我们考虑使用线段树维护区间中任意两点间距离之和。
在每个节点上,我们维护如下东西:
$len$:区间长度
$tag$:区间懒标记
$pre$:区间中每个点到区间中第一个点的距离之和
$suf$:区间中每个点到区间中最后一个点的距离之和
$all$:区间中任意两点间距离之和
这个可以通过简单拼凑得到。
时间复杂度$O(n\log n)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m;
ll gcd(ll x,ll y){return y?gcd(y,x%y):x;}
#define lson x<<1
#define rson x<<1|1
#define mid ((l+r)>>1)
#define C(x) (1ll*((x)+1)*(x)/2)
struct SegTree{
int sum,len,tag;
ll all,pre,suf;
SegTree(){sum=len=tag=all=pre=suf=0;}
friend SegTree operator +(const SegTree &x,const SegTree &y){
SegTree z;
z.len=x.len+y.len;
z.sum=x.sum+y.sum;
z.pre=x.pre+y.pre+1ll*x.sum*y.len;
z.suf=y.suf+x.suf+1ll*y.sum*x.len;
z.all=x.all+y.all+x.suf*y.len+y.pre*x.len;
return z;
}
void ADD(int x){
sum+=x*len;
tag+=x;
pre+=C(len)*x;
suf+=C(len)*x;
all+=1ll*len*(len+1)*(len+2)/6*x;
}
}seg[400100];
#define pushdown(x) seg[lson].ADD(seg[x].tag),seg[rson].ADD(seg[x].tag),seg[x].tag=0
void build(int x,int l,int r){
seg[x].len=r-l;
if(l+1==r)return;
build(lson,l,mid),build(rson,mid,r);
}
void modify(int x,int l,int r,int L,int R,int val){
if(l>=R||r<=L)return;
if(L<=l&&r<=R){seg[x].ADD(val);return;}
pushdown(x),modify(lson,l,mid,L,R,val),modify(rson,mid,r,L,R,val),seg[x]=seg[lson]+seg[rson];
}
SegTree query(int x,int l,int r,int L,int R){
if(l>=R||r<=L)return SegTree();
if(L<=l&&r<=R)return seg[x];
pushdown(x);
return query(lson,l,mid,L,R)+query(rson,mid,r,L,R);
}
void iterate(int x,int l,int r){
printf("[%d,%d):%d,%lld,%lld,%lld\n",l,r,seg[x].sum,seg[x].pre,seg[x].suf,seg[x].all);
if(l+1<r)pushdown(x),iterate(lson,l,mid),iterate(rson,mid,r),seg[x]=seg[lson]+seg[rson];
printf("[%d,%d):%d,%lld,%lld,%lld\n",l,r,seg[x].sum,seg[x].pre,seg[x].suf,seg[x].all);
}
char c[10];
int main(){
scanf("%d%d",&n,&m),build(1,1,n);
for(int i=1,l,r,v;i<=m;i++){
scanf("%s%d%d",c,&l,&r);
if(c[0]=='C')scanf("%d",&v),modify(1,1,n,l,r,v);
else{
ll x=query(1,1,n,l,r).all,y=C(r-l);
ll GCD=gcd(y,x);
printf("%lld/%lld\n",x/GCD,y/GCD);
}
// iterate(1,1,n);
}
return 0;
}
```
# III.[[HNOI2015]亚瑟王](https://www.luogu.com.cn/problem/P3239)
观察题目,我们会发现两个性质:
1. 一张卡片最多只能在一轮游戏中被成功使用。
2. 一轮游戏最多只能成功使用一张卡片。
这样,我们纵向考虑每一张卡片,判断它在某局游戏中被成功使用的概率。
设我们当前有$t$轮游戏,且该卡片成功概率是$p$。则我们有$(1-p)^t$的概率在这局游戏中没有使用它,其余情况则在某局游戏中成功使用。
现在考虑这个$t$是怎么来的。显然,假如前面的卡片中有$s$张被使用了,这就相当于占用了$s$轮,在剩下的$r-s$轮里该卡片才有可能被使用。故此时我们有$t=r-s$。
我们设$f[i][j]$表示前$i$张卡片里,恰有$j$张卡片被成功施放的概率。则我们有
$$f[i][j]=f[i-1][j]\times(1-p_i)^{r-j}+f[i-1][j-1]\times\Big(1-(1-p_i)^{r-j+1}\Big)$$
我们有
$$\sum\limits_{j}f[i][j]\times\Big(1-(1-p_i)^{r-j}\Big)$$
的概率在所有游戏中成功施放该卡片。故直接用此概率与造成的伤害求积即可得到期望。
复杂度$O(Tnr\log r)$,假如你用快速幂的话。尽管倒着计算可以将复杂度优化至$O(Tnr)$,但是上述复杂度已经可以通过。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
double ksm(double p,int q){
double r=1;
for(;q;q>>=1,p=p*p)if(q&1)r=p*r;
return r;
}
int T,n,m,a[300];
double p[300],res,f[300][300];
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);
for(int i=0;i<=n;i++)for(int j=0;j<=m;j++)f[i][j]=0;
for(int i=1;i<=n;i++)scanf("%lf%d",&p[i],&a[i]);
f[0][0]=1,res=0;
for(int i=1;i<=n;i++)for(int j=0;j<=min(m,i-1);j++){
double pos=ksm(1-p[i],m-j);
f[i][j]+=f[i-1][j]*pos;
f[i][j+1]+=f[i-1][j]*(1-pos);
res+=(1-pos)*f[i-1][j]*a[i];
}
printf("%.10lf\n",res);
}
return 0;
}
```
# IV.[[六省联考2017]分手是祝愿](https://www.luogu.com.cn/problem/P3750)
首先,本题的基础是想到一种求解的方式:
当前第$n$盏灯只能被第$n$个开关控制,故我们只能操纵第$n$个开关将其搞灭。当其熄灭后,又相当于进入了$n-1$的游戏——
因此,我们可以发现(或者瞎猜出来),任意局面都有唯一的最优方法,它操作在一组特定位置上。假如一次操作作用在应该作用的位置上,则总还需要的操作次数减一;否则,加一。
(详细的证明是因为我们如果把每个开关看作一个$n$维向量,则$n$个开关所对应的向量是线性无关的;换句话说,任意一个开关都是不可替代的)
于是我们可以先通过上面的方法,在$O(n\sqrt{n})$或者$O(n\ln n)$的时间内——这取决于你使用的算法——求出初始状态需要多少次操作,设为$P$。
则如果$P\leq k$,直接按照$P$次操作输出即可;否则,我们考虑它期望多少次操作才能够只剩$k$次操作。
一组naive的想法是设$f_i$表示当剩下$i$次操作时,消减到$k$的期望操作次数。则我们有
$$f_i=\dfrac{i}{n}\times f_{i-1}+\dfrac{n-i}{n}\times f_{i+1}+1$$
特别地,我们令对于所有$i\leq k$,有$f_i=i$。
则我们就可以暴力高斯消元得出答案。详情可见某道[经典老题](https://www.luogu.com.cn/problem/CF24D)。
当然,这种方法有一个坏处,就是写起来太麻烦了,因为你开不下$n^2$的数组,只能记录对角线周围几行,这就导致操作极其恶心,特别是当主对角线上元素为$0$,需要手动交换相邻两行时。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=100003;
int n,m,P;
bool a[100100];
int g[100100][7],f[100100];
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod;
return z;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=n;i;i--)if(a[i]){
P++;
for(int j=1;j*j<=i;j++){
if(i%j)continue;
a[j]^=1;
if(j*j<i)a[i/j]^=1;
}
}
if(P<=m){for(int i=1;i<=n;i++)P=1ll*P*i%mod;printf("%d\n",P);return 0;}
int invn=ksm(n,mod-2);
for(int i=1;i<=n;i++){
g[i][6]=i;
if(i<=m){g[i][2]=1,g[i][5]=i;continue;}
int p=1ll*i*invn%mod,q=(mod+1-p)%mod;
g[i][1]=p;
g[i][2]=mod-1;
g[i][3]=q;
g[i][5]=mod-1;
}
for(int i=1;i<n;i++){
if(!g[i][2])swap(g[i][2],g[i+1][1]),swap(g[i][3],g[i+1][2]),swap(g[i][4],g[i+1][3]),swap(g[i][5],g[i+1][5]),swap(g[i][6],g[i+1][6]);
int delta=1ll*g[i+1][1]*ksm(g[i][2],mod-2)%mod;
(g[i+1][1]+=mod-1ll*delta*g[i][2]%mod)%=mod;
(g[i+1][2]+=mod-1ll*delta*g[i][3]%mod)%=mod;
(g[i+1][3]+=mod-1ll*delta*g[i][4]%mod)%=mod;
(g[i+1][5]+=mod-1ll*delta*g[i][5]%mod)%=mod;
}
f[n]=1ll*g[n][5]*ksm(g[n][2],mod-2)%mod;
for(int i=n-1;i;i--){
(g[i][5]+=mod-1ll*g[i][4]*f[i+2]%mod)%=mod;
(g[i][5]+=mod-1ll*g[i][3]*f[i+1]%mod)%=mod;
f[i]=1ll*g[i][5]*ksm(g[i][2],mod-2)%mod;
}
int res=f[P];
for(int i=1;i<=n;i++)res=1ll*res*i%mod;printf("%d\n",res);
return 0;
}
```
还有一种做法就比较小清新了。
我们设$f_i$表示当前还剩$i$次操作时,削减掉一次操作的期望时间。
则有
$$f_i=\dfrac{i}{n}\times 1+\dfrac{n-i}{n}\times(f_{i}+f_{i+1}+1)$$
理解:前一半是抽到一个需要的位置的概率;后一半是抽到一个不需要的位置时,你需要将次数从$i+1$先消减到$i$再消减到$i-1$。
将其化简,最终得到
$$f_i=\dfrac{f_{i+1}(n-i)+n}{i}$$
故直接递推即可。最终结果只要对$f$求和即可。(别忘记最后还得加上一个$k$!)
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=100003;
int n,m,P;
bool a[100100];
int f[100100];
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod;
return z;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=n;i;i--)if(a[i]){
P++;
for(int j=1;j*j<=i;j++){
if(i%j)continue;
a[j]^=1;
if(j*j<i)a[i/j]^=1;
}
}
if(P<=m){for(int i=1;i<=n;i++)P=1ll*P*i%mod;printf("%d\n",P);return 0;}
for(int i=n;i>=0;i--)f[i]=1ll*(1ll*(n-i)*f[i+1]%mod+n)%mod*ksm(i,mod-2)%mod;
for(int i=n;i>=0;i--)(f[i]+=f[i+1])%=mod;
int res=(f[m+1]-f[P+1]+m+mod)%mod;
for(int i=1;i<=n;i++)res=1ll*res*i%mod;printf("%d\n",res);
return 0;
}
```
# V.[[SHOI2014]概率充电器](https://www.luogu.com.cn/problem/P4284)
这题实际上很简单,但是我却想歪了……
我们我们可以设$f_i$表示$i$节点**熄灭**的概率。之所以不设为亮起的概率,是因为熄灭当且仅当周边节点没有一个连得到它,但是亮起却是周边至少有一个能连到它——用脚趾头想都知道哪个容易求。
设$p_i$表示$i$节点本身通电的概率,$p_{x,y}$表示$(x,y)$边通电的概率,则我们有方程
$$f_i=(1-p_i)\prod\limits_{(i,j)\in\mathbb{E}}(1-p_{i,j})+p_{i,j}\times f_j$$
方程很好理解。但是,注意的是,此处的$f_j$中**不应考虑**$f_i$——因为我们此处是**以$i$为中心**,因此这里的$f_j$并不是真实的$f_j$。
所以,我们还要再二次扫描一下,对每个位置求出以它为根时的真实的$f_i$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,head[500100],cnt;
struct node{
int to,next;
double prob;
}edge[1001000];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].prob=0.01*w,head[u]=cnt++;
edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].prob=0.01*w,head[v]=cnt++;
}
double p[500100],f[500100],res;
void dfs1(int x,int fa){
f[x]=(1-p[x]);
for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)dfs1(edge[i].to,x),f[x]*=(1-edge[i].prob)+edge[i].prob*f[edge[i].to];
}
void dfs2(int x,int fa){
for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa)f[edge[i].to]*=(1-edge[i].prob)+edge[i].prob*f[x]/((1-edge[i].prob)+edge[i].prob*f[edge[i].to]),dfs2(edge[i].to,x);
}
int main(){
scanf("%d",&n),memset(head,-1,sizeof(head));
for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
for(int i=1,x;i<=n;i++)scanf("%d",&x),p[i]=0.01*x;
dfs1(1,0),dfs2(1,0);
for(int i=1;i<=n;i++)res+=1-f[i];
printf("%lf\n",res);
return 0;
}
```
# VI.[[LnOI2019]加特林轮盘赌](https://www.luogu.com.cn/problem/P5249)
我们考虑设$f[i][j]$表示$i$个人中,第$j$个人最终存活的概率。
我们先考虑$j>1$的情况。此时,有$p$的概率排在首位的人挂掉,局面变为$f[i-1][j-1]$;反之,有$1-p$的概率首位存活,这就相当于所有人向前进一格,局面变为$f[i][j-1]$。
于是我们有
$$f[i][j]=p\times f[i-1][j-1]+(1-p)\times f[i][j-1]\qquad(1<j\leq i)$$
下面我们考虑特殊情况$f[i][1]$。显然,只有这一枪他幸存下来了,他最终才有可能存活。故我们有
$$f[i][1]=(1-p)f[i][i]$$
到这里我们就可以DP了。我们初始有$f[1][1]=1$;接着,我们从小往大枚举$i$,更新DP状态。
我们设$f[i][1]=a\times f[i][i]+b$。显然,此时有$a=1-p,b=0$。这样,我们就可以把$f[i][2]$表示成$c\times f[i][i]+d$的形式,接着把$f[i][3]$表示成$e\times f[i][i]+f$的形式……
最终,我们得到一个方程:
$$f[i][i]=x\times f[i][i]+y$$
直接解该方程即可得出$f[i][i]$。再DP一圈即可得出所有的$f[i][j]$。
时间复杂度$O(n^2)$。(注意特判$p=0$时的情况)
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const long double eps=1e-9;
int n,m;
long double p,f[2][10100];
int main(){
cin>>p>>n>>m;
if(p<eps){if(m==1)puts("1");else puts("0");return 0;}
f[1][1]=1;
for(int i=2;i<=n;i++){
double a=1,b=0;
for(int j=1;j<=i;j++)a*=1-p,b=(1-p)*b+p*f[!(i&1)][j-1];
f[i&1][i]=b/(1-a);
f[i&1][1]=(1-p)*f[i&1][i];
for(int j=2;j<i;j++)f[i&1][j]=(1-p)*f[i&1][j-1]+p*f[!(i&1)][j-1];
}
printf("%.10Lf\n",f[n&1][m]);
return 0;
}
```
# VII.[[NOI2012]迷失游乐园](https://www.luogu.com.cn/problem/P2081)
[题解戳这儿](https://www.luogu.com.cn/blog/Troverld/solution-p2081)
# VII.[[ZJOI2015]地震后的幻想乡](https://www.luogu.com.cn/problem/P3343)
本题有两种思路。
一种思路是从暴力入手并优化状态。
我们考虑边的一组排列$\{p_1,\dots,p_m\}$。它是将边按照边权从小到大排列的结果。则我们在这组排列上跑Kruskal,设在加入排名为$i$的边时跑出了一棵生成树,则这组排列的答案就是排名为$i$的边权期望,按照题面中的提示,它就是$\dfrac{i}{m+1}$。
枚举每一种排列——它们都是等概率出现的——就能求出总概率。则这个算法的时间复杂度是$O(m\times m!)$。
考虑优化。因为我们要求的是**生成树中最大边**,所以多加入一些边并不会影响最大边的大小,于是我们实际上只要找出一个使得**之前图不连通,在加入这条边后图联通了**的位置$i$即可。
于是,我们发现在我们Kruskal加入一条新边之时,我们只考虑**之前放了多少条边**以及**图的连通性**即可。
发现单独求出加入多少条边时刚好联通不好求;于是我们干脆做个后缀和,设$f[i][j]$表示加入$i$条边后集合$j$联通的方案数,最后做一个差分即可求出所有刚好联通的方案数。
发现$f[i][j]$不好求。于是我们采取正难则反,考虑设$g[i][j]$表示集合$j$不连通的方案数。
我们考虑如何求出$j$。一个显然的想法是$O(3^n)$枚举子集,将$j$集合切成两半处理。为了不重不漏地计数,我们就强制令一半联通,另一半随便连,同时两半间不连边。这里我们采取计数问题中的经典策略,找出$j$中的lowbit位,设为$p$,并且强制令联通的集合中必须包含$p$(这就相当于枚举$p$所在的连通块)。
我们设$|j|$表示$j$集合内部共有多少条边。则我们有
$$g_{i,j}=\sum\limits_{k\subset j,p\in k}\sum\limits_{l=0}^if_{l,k}\times\dbinom{\Big|j\setminus k\Big|}{i-l}$$
其中,$k$枚举子集,$l$枚举$k$中有多少条边,二项式系数的意义是从集合$j\setminus k$(其中$\setminus$是集合减符号)中选出$i-l$条边的方案数。
另,我们又有
$$f_{i,j}+g_{i,j}=\dbinom{|j|}{i}$$
这很好理解,因为联通数加上不连通数必定等于总方案数。故我们就可以直接通过该式求出$f_{i,j}$。
则我们设$h_i$表示加入$i$条边时整张图联通的概率。于是有
$$h_i=\dfrac{f_{i,\mathbb{V}}}{\binom{m}{i}}$$
其中$\mathbb{V}$是全体点集。
对$h_i$做差分就得到位置$i$刚好联通的概率;概率再乘上权值($\dfrac{i}{m+1}$)就得到了期望。
时间复杂度$O(m^23^n)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,lim;
bool s[20][20];
ll f[100][1<<10],g[100][1<<10],C[100][100],sz[1<<10];
double h[100],res;
int main(){
scanf("%d%d",&n,&m),lim=1<<n;
for(int i=0;i<=m;i++)C[i][0]=1;
for(int i=1;i<=m;i++)for(int j=1;j<=i;j++)C[i][j]=C[i-1][j-1]+C[i-1][j];
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),x--,y--,s[x][y]=s[y][x]=true;
for(int i=0;i<lim;i++)g[0][i]=1;
for(int i=0;i<n;i++)f[0][1<<i]=1,g[0][1<<i]=0;
for(int i=1;i<lim;i++){
sz[i]=sz[i^(i&-i)];
int p=__builtin_ctz(i);
for(int j=0;j<n;j++)if(i&(1<<j))sz[i]+=s[j][p];
}
for(int i=1;i<=m;i++)for(int j=1;j<lim;j++){
int p=__builtin_ctz(j);
for(int k=(j-1)&j;k;k=(k-1)&j)if(k&(1<<p))for(int l=0;l<=i;l++)g[i][j]+=f[l][k]*C[sz[j^k]][i-l];
f[i][j]=C[sz[j]][i]-g[i][j];
}
for(int i=0;i<=m;i++)h[i]=1.0*f[i][lim-1]/C[m][i];
for(int i=m;i>=1;i--)h[i]-=h[i-1];
for(int i=0;i<=m;i++)res+=h[i]*i;
printf("%lf\n",res/(m+1));
return 0;
}
```
另一种做法是从积分角度分析。
我们设$p(x)$为最大边为$x$的概率。则我们要求的期望则为
$$\mathrm{E}X=\int_0^1p(x)x\mathrm{d}x$$
仿照前一种思路,我们设 $P(X)=\int_X^1p(x)\mathrm{d}x$,则有
$$\begin{aligned}\mathrm{E}X=&\int_0^1p(x)x\mathrm{d}x\\=&\int_0^1p(x)\Big(\int_0^x1\mathrm{d}t\Big)\mathrm{d}x\\=&\int_0^1\Big(\int_t^1p(x)\mathrm{d}x\Big)\mathrm{d}t\\=&\int_0^1P(t)\mathrm{d}t\end{aligned}$$
下面我们考虑如何求出上式。观察到$P(X)$的实际意义是最大边大于等于$X$的概率;于是我们设$P_{\mathbb{S}}(X)$表示$\mathbb{S}$集合中最大边大于等于$X$的概率,则就有$P(X)=P_{\mathbb{V}}(X)$,其中$\mathbb{V}$是全部节点集合。
我们考虑转移出$P_{\mathbb{S}}(X)$:我们这次考虑枚举$1$所在的那个连通块,设此连通块为$\mathbb{T}$,则$\mathbb{T}$联通的概率是$1-P_{\mathbb{T}}(X)$,$\mathbb{T}$不与其它部分联通的概率是$(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}$,其中$e(\mathbb{T},\mathbb{S}-\mathbb{T})$是$\mathbb{S}$与其补集间边数。
于是就有
$$P_{\mathbb{S}}(X)=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\Big(1-P_{\mathbb{T}}(X)\Big)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}$$
考虑到我们最终要求的是$\int_0^1P_{\mathbb{V}}(X)\mathrm{d}X$;于是开始推式子:
$$\begin{aligned}\int_0^1P_{\mathbb{S}}(X)\mathrm{d}X&=\int_0^1\Bigg(\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\Big(1-P_{\mathbb{T}}(X)\Big)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\Bigg)\mathrm{d}X\\&=\int_0^1\Bigg(\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}-P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\Bigg)\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\int_0^1(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\int_0^1X^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\dfrac{0^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\\&=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-\int_0^1P_{\mathbb{T}}(X)(1-X)^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}\mathrm{d}X\end{aligned}$$
到这里,我们停一下,设一个
$$F^k(\mathbb{S})=\int_0^1(1-X)^kP_{\mathbb{S}}(X)\mathrm{d}X$$
则代入上面的式子,就有
$$F^0(\mathbb{S})=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-F^{e(\mathbb{T},\mathbb{S}-\mathbb{T})}(\mathbb{T})$$
于是现在的目标就是求出任意的$F^k(\mathbb{S})$;这很简单,只需要类似地代入式子中强推即可得到
$$F^k(\mathbb{S})=\sum\limits_{1\in\mathbb{T}\subset\mathbb{S}}\dfrac{1}{1+k+e(\mathbb{T},\mathbb{S}-\mathbb{T})}-F^{k+e(\mathbb{T},\mathbb{S}-\mathbb{T})}(\mathbb{T})$$
显然这就可以$O(m3^n)$地DP了。最终答案即为$F^0(\mathbb{V})$。
代码(极致压行版——它甚至没有我一个推导的式子长):
```cpp
#include<stdio.h>
int n,m,in[1<<10],lim;
double f[1<<10][100];
int main(){
scanf("%d%d",&n,&m),lim=1<<n;
for(int i=1,x,y;i<=m;i++){scanf("%d%d",&x,&y),x--,y--;for(int j=0;j<lim;j++)if((j&(1<<x))&&(j&(1<<y)))in[j]++;}
for(int i=0;i<lim;i++)if(i&1)for(int j=(i-1)&i;j;j=(j-1)&i)if(j&1)for(int k=0,l=in[i]-in[j]-in[i^j];k+l<=m;k++)f[i][k]+=1.0/(k+l+1)-f[j][k+l];
printf("%.6lf\n",f[lim-1][0]);
return 0;
}
```
# VIII.[随机数生成器](https://www.luogu.com.cn/problem/P3600)
这题能自己做出来(虽然想了整整3天),我已经满足了。
我们设$p(x)$表示最大值刚好为$x$的概率。则答案为$\sum\limits_{i=1}^mp(i)i$。
有了上一题的经验,我们很容易想到刚好为$x$的概率不好求,必须做一个前缀和/后缀和才好求。那么到底是用前缀和还是后缀和呢?
假如你用后缀和的话,~~运用人类智慧,发现很不好求~~;于是我们采取前缀和。
我们设$g(x)$表示最大值$\leq x$的概率。则,每一个区间里都至少有一个位置是$\leq x$的。
我们考虑DP求出$g(x)$。我们设$pos_i$表示所有右端点在$i$之前的区间的左端点的最大值。我们再设$f(i)$表示强制位置$i$出现一个$\leq x$的数,且前$i$个位置的所有区间里都至少存在一个$\leq x$的数的概率;则有
$$f(i)=\dfrac{x}{m}\sum\limits_{j=pos_{i-1}}^{i-1}f(j)\Big(\dfrac{m-x}{m}\Big)^{i-1-j}$$
其中,$j$是枚举$i$之前最后一个$\leq x$的数所在的位置。
则有
$$g(x)=\sum\limits_{j=pos_n}^nf(j)\Big(\dfrac{x}{m}\Big)^{n-j}$$
对于$g(x)$差分即可得到$p(x)$,并进一步求出期望出来。
明显$pos_i$是单调不降的;故我们可以直接使用双针$O(n)$求出$f(i)$。另外还要$O(m)$枚举$x$,故总复杂度$O(nm)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=666623333;
int n,m,invm,q,pos[2010],f[2010],g[2010],res;
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
return z;
}
int main(){
scanf("%d%d%d",&n,&m,&q),invm=ksm(m,mod-2);
for(int i=1,x,y;i<=q;i++)scanf("%d%d",&x,&y),pos[y]=max(pos[y],x);
for(int i=1;i<=n;i++)pos[i]=max(pos[i-1],pos[i]);
for(int i=1;i<=m;i++){
int p=1ll*i*invm%mod,q=1ll*(m-i)*invm%mod;
f[0]=1;
for(int j=1,k=0;j<=n;j++){
if(j>=1)for(int l=pos[j-2];l<pos[j-1];l++)(k+=mod-1ll*f[l]*ksm(q,j-2-l)%mod)%=mod;
k=1ll*k*q%mod;
(k+=f[j-1])%=mod;
f[j]=1ll*k*p%mod;
}
for(int j=pos[n];j<=n;j++)(g[i]+=1ll*f[j]*ksm(q,n-j)%mod)%=mod;
}
for(int i=m;i>=1;i--)g[i]=(g[i]-g[i-1]+mod)%mod;
for(int i=1;i<=m;i++)(res+=1ll*i*g[i]%mod)%=mod;
printf("%d\n",res);
return 0;
}
```
# IX.[[TJOI2015]概率论](https://www.luogu.com.cn/problem/P3978)
~~O E I S 大 法 好~~
我们设$f(x)$表示$x$个节点的二叉树的叶子节点个数之和,$g(x)$表示$x$个节点的二叉树总数。则答案就是$\dfrac{f(n)}{g(n)}$。
显然$g$就是卡特兰数;$f$通过$O(n^4)$暴力DP可以打出表来,发现是
```1, 2, 6, 20, 70, 252, 924, 3432, 12870...```
丢进OEIS一看,发现它就是$\dbinom{2n-2}{n-1}$。然后$g$的递推公式就是$\dfrac{\binom{2n}{n}}{n+1}$;两个一除,推推式子就得到了答案$\dfrac{n(n+1)}{2(2n-1)}$。
考虑它的实际意义——
一棵$n$个点的二叉树,假设它又长出了一片叶子,则这就对应了一棵$n+1$个节点的二叉树。显然这片叶子有$2n-(n-1)=n+1$种长法。
我们再来考虑一棵$n+1$个节点的二叉树。删去它的每个叶子,都会得到一棵$n$个点的二叉树。显然这两种情况是一一对应的。
故我们就有$(n+1)g(n)=f(n+1)$。代入$g$的递推式,就能得到$f$。
代码(注释部分是$O(n^4)$的DP):
```cpp
#include<bits/stdc++.h>
using namespace std;
int n;
//int f[110][110],g[110],p[110];
int main(){
scanf("%d",&n);
printf("%.10Lf",(long double)(n+1)*n/2/(2*n-1));
// f[0][0]=f[1][1]=1;
// for(int i=2;i<=n;i++)for(int j=0;j<=i;j++)for(int k=0;k<i;k++)for(int l=0;l<=j;l++)f[i][j]+=f[k][l]*f[i-1-k][j-l];
// for(int i=1;i<=n;i++)for(int j=0;j<=i;j++)p[i]+=f[i][j]*j;
// g[0]=1;
// for(int i=1;i<=n;i++)for(int j=0;j<i;j++)g[i]+=g[j]*g[i-j-1];
// for(int i=1;i<=n;i++)printf("(%lld/%lld)",p[i],g[i]);
return 0;
}
```
# X.[[SDOI2012]走迷宫](https://www.luogu.com.cn/problem/P6030)
这题本来是一个SCC+高斯消元的模板题来着的……但关键是DP状态的设计。
首先先判一下无解。显然,如果从起点出发能够走到一个**走不到终点的点**,则为无解。这很好想——只要答案有为无穷大的可能,无论概率多小,最终答案都会为无穷大。
然后就是DP设计了。我们无论设什么**从起点出发**的状态都是不太好的——所以,我们应该反过来,设**从终点出发**的状态。即,设$f_x$表示从节点$x$出发到终点的期望长度,则有
$$f_x=\dfrac{\sum\limits_{(x,y)\in\mathbb{E}}f_y}{deg_x}+1$$
因为我们发现在不同SCC间转移是有序的,就可以直接按照拓扑序DP;只有在同一个SCC内部才会出现环,于是有环就直接用高斯消元暴力消掉即可。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,S,T,col[10100],in[10100],ord[10100],out[10100],sz[10100],c;
vector<int>ele[10100];
namespace SCC{
int dfn[10100],low[10100],tot;
stack<int>stk;
vector<int>v[10100],u[10100];
bool vis1[10100],vis2[10100];
void dfs1(int x){
vis1[x]=true;
for(auto y:u[x])if(!vis1[y])dfs1(y);
}
void dfs2(int x){
if(!vis1[x]){puts("INF");exit(0);}
vis2[x]=true;
for(auto y:v[x])if(!vis2[y])dfs2(y);
}
void Tarjan(int x){
dfn[x]=low[x]=++tot,stk.push(x);
for(auto y:v[x]){
if(!dfn[y])Tarjan(y),low[x]=min(low[x],low[y]);
else if(!col[y])low[x]=min(low[x],dfn[y]);
}
if(dfn[x]!=low[x])return;
c++;
int y;
do{y=stk.top(),col[y]=c,ord[y]=sz[c]++,ele[c].push_back(y),stk.pop();}while(y!=x);
}
}
namespace DAG{
double f[10100],g[110][110];
vector<int>v[10100],u[10100];
void Gauss(int x){
for(auto i:ele[x]){
if(i==T){g[ord[i]][ord[i]]=1,g[ord[i]][sz[x]]=0;continue;}
for(auto j:SCC::v[i])if(col[i]==col[j])g[ord[i]][ord[j]]+=1.0/out[i];else g[ord[i]][sz[x]]-=f[j]/out[i];
g[ord[i]][sz[x]]-=1;
g[ord[i]][ord[i]]+=-1;
}
for(int i=0;i<sz[x];i++){
int mx=i;
for(int j=i+1;j<sz[x];j++)if(abs(g[mx][i])<abs(g[j][i]))mx=j;
if(i!=mx)for(int j=i;j<=sz[x];j++)swap(g[mx][j],g[i][j]);
for(int j=0;j<sz[x];j++){
if(i==j)continue;
double tmp=g[j][i]/g[i][i];
for(int k=i;k<=sz[x];k++)g[j][k]-=tmp*g[i][k];
}
}
for(auto i:ele[x])f[i]=g[ord[i]][sz[x]]/g[ord[i]][ord[i]];
for(int i=0;i<sz[x];i++)for(int j=0;j<=sz[x];j++)g[i][j]=0;
}
queue<int>q;
void Topo(){
q.push(col[T]);
while(!q.empty()){
int x=q.front();q.pop();
Gauss(x);
for(auto y:v[x]){
in[y]--;
if(!in[y])q.push(y);
}
}
}
}
int main(){
scanf("%d%d%d%d",&n,&m,&S,&T);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),SCC::v[x].push_back(y),SCC::u[y].push_back(x),out[x]++;
SCC::dfs1(T),SCC::dfs2(S);
for(int i=1;i<=n;i++)if(!SCC::dfn[i])SCC::Tarjan(i);
for(int i=1;i<=n;i++)for(auto j:SCC::v[i])if(col[i]!=col[j])DAG::v[col[j]].push_back(col[i]),in[col[i]]++;
DAG::Topo();
printf("%.3lf\n",DAG::f[S]);
return 0;
}
```
# XI.[[HNOI2011]XOR和路径](https://www.luogu.com.cn/problem/P3211)
同上题一样,本题采用倒序DP的方式。
我们考虑按位处理。设当前处理到第$p$位,再设$f_i$表示从位置$i$出发,到达终点时的期望结果。
则对于一条边$(x,y,z)$,如果$z$在第$p$位上是$1$,则有$f_x\leftarrow 1-f_y$;否则,则有$f_x\leftarrow f_y$。
对于一个节点$x$,它可以从所有与它有边的节点转移过来;故直接高斯消元跑一下即可。
时间复杂度$O(n^3\log n)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,head[110],out[110],cnt;
struct node{
int to,next,val;
}edge[20100];
void ae(int u,int v,int w){
edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++,out[u]++;
if(u!=v)edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++,out[v]++;
}
long double g[110][110],res;
void Gauss(int x){
for(int i=1;i<=n;i++){
if(i==n){g[i][i]=1;continue;}
for(int j=head[i];j!=-1;j=edge[j].next){
if(edge[j].val&(1<<x))g[i][edge[j].to]-=1.0/out[i],g[i][n+1]-=1.0/out[i];
else g[i][edge[j].to]+=1.0/out[i];
}
g[i][i]-=1;
}
for(int i=1;i<=n;i++){
int mx=i;
for(int j=i+1;j<=n;j++)if(abs(g[mx][i])<abs(g[j][i]))mx=j;
if(i!=mx)for(int j=i;j<=n+1;j++)swap(g[mx][j],g[i][j]);
for(int j=1;j<=n;j++){
if(i==j)continue;
double tmp=g[j][i]/g[i][i];
for(int k=i;k<=n+1;k++)g[j][k]-=tmp*g[i][k];
}
}
res+=(g[1][n+1]/g[1][1])*(1<<x);
for(int i=1;i<=n;i++)for(int j=1;j<=n+1;j++)g[i][j]=0;
}
int main(){
scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
for(int i=0;i<=30;i++)Gauss(i);
printf("%.3Lf\n",res);
return 0;
}
```
# XII.[[NOI2005]聪聪与可可](https://www.luogu.com.cn/problem/P4206)
这题一个naive的思路是设$p_{i,j}$表示$i$时刻老鼠在位置$j$的概率,然后求出$f_i$表示猫$i$时刻前抓到老鼠的概率(因为如果$i$时刻猫可以抓到老鼠,则$i+1$时刻猫一定仍可以抓到老鼠;而$i$时刻猫能抓到老鼠的位置只有可能距猫的起点$\leq 2i$),最后差个分就能得出答案。
但是我们发现猫并不能保证所有$\leq 2i$的位置都能抓到——举例子来说,我们有这样一个样例:

猫从$1$出发,第一时刻到达$3$;但是,如果老鼠此时走到了$5$,虽然$1$到$5$的距离$\leq 2i=2$,但是猫仍不能一次抓到老鼠。
所以我们不得不考虑一种新的想法,即记录下猫和鼠的位置,求出此时它期望多少轮能够捉到鼠。
我们设$f_{i,j}$表示猫在$i$时,鼠在$j$时的期望次数;我们再设$nex2_{i,j}$表示此时猫下一步会走到哪里。$nex2$可以通过$O(nm)$地预处理出来距离数组,再$O(nm)$地预处理出来$nex1$数组表示猫走一步的目标,然后通过$nex1$预处理出来$nex2$得出。
则有:
$f_{i,j}=0$,如果$i=j$;
$f_{i,j}=1$,如果猫能直接从$i$走到$j$;
否则,设这里有一条边$(j,k)$,则有
$$f_{i,j}=\dfrac{f_{nex2_{i,j},j}\sum f_{nex2_{i,j},k}}{deg_k+1}$$
凭直觉发现这不可能有环;故直接记忆化搞掉即可。
时间复杂度$O(nm)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,dis[1010][1010],nex1[1010][1010],nex2[1010][1010],cat,mouse;
double f[1010][1010];
bool vis[1010][1010];
vector<int>v[1010];
void bfs(int S){
queue<int>q;
memset(dis[S],0x3f,sizeof(dis[S])),dis[S][S]=0;
q.push(S);
while(!q.empty()){
int x=q.front();q.pop();
for(auto y:v[x])if(dis[S][y]==0x3f3f3f3f)dis[S][y]=dis[S][x]+1,q.push(y);
}
}
double dfs(int x,int y){
if(vis[x][y])return f[x][y];
vis[x][y]=true;
double &now=f[x][y];
if(x==y)return now=0;
if(nex2[x][y]==-1)return now=1;
x=nex2[x][y];
for(auto z:v[y])now+=dfs(x,z)+1;
now+=dfs(x,y)+1;
now/=int(v[y].size()+1);
return now;
}
int main(){
scanf("%d%d",&n,&m);
scanf("%d%d",&cat,&mouse);
for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),v[x].push_back(y),v[y].push_back(x);
for(int i=1;i<=n;i++)bfs(i);
// for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",dis[i][j]);puts("");}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
if(dis[i][j]==0x3f3f3f3f)continue;
if(dis[i][j]<=1){nex1[i][j]=-1;continue;}
int mp=-1;
for(auto k:v[i])if(mp==-1||dis[mp][j]>dis[k][j]||dis[mp][j]==dis[k][j]&&mp>k)mp=k;
nex1[i][j]=mp;
}
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
if(dis[i][j]==0x3f3f3f3f)continue;
if(nex1[i][j]==-1||nex1[nex1[i][j]][j]==-1){nex2[i][j]=-1;continue;}
nex2[i][j]=nex1[nex1[i][j]][j];
}
// for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",nex2[i][j]);puts("");}
// for(int i=1;i<=n;i++)printf("%d ",dis[i]);puts("");
printf("%.3lf\n",dfs(cat,mouse));
return 0;
}
```
# XIII.[[JXOI2018]游戏](https://www.luogu.com.cn/problem/P4562)
这题好像根本不算概率期望罢……
我们考虑$[l,r]$中,如果删去了区间中所有**不是区间中其他任何数的倍数**的数,则整个区间内所有的数都会被删去;反之,假如剩下了某些不是区间中其他任何数的倍数的数,则此区间一定不会被全部删完。
于是我们考虑求出区间中上述数的个数。考虑一个数满足上述条件的充分必要条件,就是它的最大约数$<l$。一个数的最大约数,可以直接通过线性筛求出来,复杂度$O(n)$。
然后我们考虑计算答案。我们考虑最后一个满足条件的数出现在序列中第$i$个位置的情形(应有$i\geq m$):
此时,共有
$$(i-1)!\times m\times\dbinom{n-m}{i-m}\times (n-i)!$$
个排列满足上述条件。其中,$(i-1)!$是枚举前$i-1$个位置填哪些数,$m$是枚举第$i$个位置填什么数,$\dbinom{n-m}{i-m}$找出哪些不满足条件的数应被填入前$i$个位置中,$(n-i)!$枚举剩下位置的方案数。
组合数和阶乘可以通过$O(n)$预处理得到;则总复杂度$O(n)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int l,r,n,m,pri[10010000],fac[10010000],inv[10010000],res;
void Ural(){
if(l==1){m=1;return;}
for(int i=2;i<=r;i++){
if(!pri[i])pri[++pri[0]]=i,m+=(l<=i);
for(int j=1;j<=pri[0]&&i*pri[j]<=r;j++){
pri[i*pri[j]]=true;
m+=(l<=i*pri[j]&&l>i);
if(!(i%pri[j]))break;
}
}
}
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
return z;
}
#define C(x,y) (1ll*fac[(x)]*inv[(y)]%mod*inv[(x)-(y)]%mod)
int main(){
scanf("%d%d",&l,&r),n=r-l+1;
Ural();
fac[0]=1;
for(int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[n]=ksm(fac[n],mod-2);
for(int i=n-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
for(int i=m;i<=n;i++)(res+=1ll*i*m%mod*fac[i-1]%mod*C(n-m,i-m)%mod*fac[n-i]%mod)%=mod;
printf("%d\n",res);
return 0;
}
```
# XIV.[[JXOI2018]排序问题](https://www.luogu.com.cn/problem/P4561)
本题好像又不算期望罢……
根据一些简单的推理,我们发现最终答案就是
$$\dfrac{(n+m)!}{\prod\limits_{i}cnt_i!}$$
其中$cnt_i$表示有多少个数是$i$。(这很简单,因为只有每个位置一一对应才能排序成功;但是值相同的数之间两两可以相互替换,故要除掉)
我们发现,对于$i\notin[l,r]$,这个$cnt_i$就已经固定了,可以直接通过哈希表/离散化预处理出来;而对于$i\in[l,r]$,$cnt_i$并不固定,而我们希望它尽量平均,故我们先用这$m$个位置尽量补,补到对于$i\in[l,r]$,$cnt_i$全部相等,然后剩下的直接平均分即可。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
const int N=10200000;
int T,n,m,l,r,a[200100],b[200100],c[200100],fac[10200100],inv[10200100],res;
vector<int>v;
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*z*x%mod;
return z;
}
int main(){
fac[0]=1;for(int i=1;i<=N;i++)fac[i]=1ll*fac[i-1]*i%mod;
inv[N]=ksm(fac[N],mod-2);for(int i=N-1;i>=0;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
scanf("%d",&T);
while(T--){
scanf("%d%d%d%d",&n,&m,&l,&r),res=fac[n+m],v.clear();
for(int i=1;i<=n;i++)scanf("%d",&a[i]),v.push_back(a[i]);
sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin());
for(int i=1;i<=n;i++)b[lower_bound(v.begin(),v.end(),a[i])-v.begin()]++;
c[0]=r-l+1;
for(int i=0;i<v.size();b[i++]=0)if(l<=v[i]&&v[i]<=r)c[0]--,c[b[i]]++;else res=1ll*res*inv[b[i]]%mod;
int lim=0;
for(lim=0;c[lim]!=r-l+1;lim++){
if(c[lim]<=m)m-=c[lim],c[lim+1]+=c[lim],c[lim]=0;
else{c[lim+1]+=m,c[lim]-=m,m=0;break;}
}
if(c[lim]==r-l+1){
int tmp=m%c[lim],now=m/c[lim];
res=1ll*res*ksm(inv[lim+now+1],tmp)%mod*ksm(inv[lim+now],c[lim]-tmp)%mod;
}else for(int i=lim,j=0;j!=r-l+1;i++)res=1ll*res*ksm(inv[i],c[i])%mod,j+=c[i];
for(int i=0;i<=n;i++)c[i]=0;
printf("%d\n",res);
}
return 0;
}
```
# XV.[小 Y 和恐怖的奴隶主](https://www.luogu.com.cn/problem/P4007)
[题解](https://www.luogu.com.cn/blog/Troverld/solution-p4007)
# XVI.[[BJOI2018]治疗之雨](https://www.luogu.com.cn/problem/P4457)
一眼能看出这是道高斯消元题。
我们设$f_i$表示当前英雄血量为$i$时期望多少次死掉。
则我们有
$$f_i=\dfrac{1}{m+1}\times\Big(\sum\limits_{j=0}^iq_jf_{i+1-j}\Big)+\dfrac{m}{m+1}\times\Big(\sum\limits_{j=0}^{i-1}q_jf_{i-j}\Big)+1$$
其中$q_j$是打掉$j$点血的概率,有
$$q_j=\Big(\dfrac{1}{m+1}\Big)^j\Big(\dfrac{m}{m+1}\Big)^{k-j}\dbinom{k}{j}$$
特殊地,当满血(即$i=n$)时,因为必定奶不到英雄,故直接有
$$f_i=\sum\limits_{j=0}^{i-1}q_jf_{i-j}+1$$
明显这个DP有后效性,必须得高斯消元;但是$O(n^3)$你告诉我能跑$1500$?
抱歉,还真能。
我们列出转移矩阵,发现它大概长这样:
$$\begin{bmatrix}&?,?,0,0,0,\dots,0\\&?,?,?,0,0,\dots,0\\&?,?,?,?,0,\dots,0\\&\dots\\&?,?,?,?,?,\dots,?\end{bmatrix}$$
发现它十分接近倒着的上三角矩阵;故我们只需要消掉对角线上面的一行,就能把它消成一个真正的倒置上三角矩阵,然后就能$O(n^2)$代回解出了。
明显只消掉对角线上面一行的复杂度也是$O(n^2)$的;故总复杂度即为$O(n^2)$。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int T,n,m,S,p,g[2010][2010],q[2010],inv[2010],f[2010];
int ksm(int x,int y){
int z=1;
for(;y;y>>=1,x=1ll*x*x%mod)if(y&1)z=1ll*x*z%mod;
return z;
}
void Gauss(){
for(int i=n;i>1;i--){
if(!g[i][i])swap(g[i],g[i-1]);
int tmp=1ll*g[i-1][i]*ksm(g[i][i],mod-2)%mod;
for(int j=1;j<=i;j++)(g[i-1][j]+=mod-1ll*g[i][j]*tmp%mod)%=mod;
(g[i-1][n+1]+=mod-1ll*g[i][n+1]*tmp%mod)%=mod;
}
// for(int i=1;i<=n;i++){for(int j=1;j<=n+1;j++)printf("%d ",g[i][j]);puts("");}
f[1]=1ll*g[1][n+1]*ksm(g[1][1],mod-2)%mod;
for(int i=2;i<=n;i++){
f[i]=g[i][n+1];
for(int j=1;j<i;j++)(f[i]+=mod-1ll*g[i][j]*f[j]%mod)%=mod;
f[i]=1ll*f[i]*ksm(g[i][i],mod-2)%mod;
}
}
int main(){
scanf("%d",&T);
for(int i=1;i<=2000;i++)inv[i]=ksm(i,mod-2);
while(T--){
scanf("%d%d%d%d",&n,&S,&m,&p);
if(p==0||p==1&&m==0&&(S!=n||n!=1)){puts("-1");continue;}
int P=ksm(m+1,mod-2),Q=1ll*m*P%mod;
for(int i=0;i<=n;i++)q[i]=0;
for(int i=0,j=1;i<=min(n,p);j=1ll*j*(p-i)%mod,i++,j=1ll*j*inv[i]%mod)q[i]=1ll*ksm(P,i)*ksm(Q,p-i)%mod*j%mod;
for(int i=1;i<=n;i++)for(int j=1;j<=n+1;j++)g[i][j]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++)g[i][i-j]=1ll*q[j]*(i<n?mod+1-P:1)%mod;
if(i<n)for(int j=0;j<i+1;j++)(g[i][i+1-j]+=1ll*q[j]*P%mod)%=mod;
(g[i][i]+=mod-1)%=mod;
g[i][n+1]=mod-1;
}
// for(int i=1;i<=n;i++){for(int j=1;j<=n+1;j++)printf("%d ",g[i][j]);puts("");}
Gauss();
printf("%d\n",f[S]);
}
return 0;
}
```
# XVII.[[SDOI2017]龙与地下城](https://www.luogu.com.cn/problem/P3779)
本题在模意义下和实数意义下,小范围和大范围下各有几种做法。
我们此处定义有$n$个骰子,每个骰子有$m$面。
1. 小数据范围
明显发现它就是$f(x)=\frac{\sum\limits_{i=0}^{m-1}x^i}{m}$的$n$次方。
于是直接倍增计算快速幂即可。时间复杂度$O(nm\log nm)$,卡的过去我请你喝茶。
2. 大数据范围
发现这道题精度要求很小,故我们掐头去尾,设一个 $\epsilon=10^{-13}$(亲测$10^{-9}$过不去),掐掉两边$<\epsilon$的部分。这样就能过去了。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
const double pi=acos(-1);
const double eps=1e-13;
int T,n,m;
int lim,LG,rev[500100];
struct cp{
double x,y;
cp(double u=0,double v=0){x=u,y=v;}
friend cp operator +(const cp &u,const cp &v){return cp(u.x+v.x,u.y+v.y);}
friend cp operator -(const cp &u,const cp &v){return cp(u.x-v.x,u.y-v.y);}
friend cp operator *(const cp &u,const cp &v){return cp(u.x*v.x-u.y*v.y,u.x*v.y+u.y*v.x);}
}F[500100],G[500100];
void FFT(cp *a,int tp){
for(int i=0;i<lim;i++)if(i<rev[i])swap(a[i],a[rev[i]]);
for(int md=1;md<lim;md<<=1){
cp rt=cp(cos(pi/md),tp*sin(pi/md));
for(int stp=md<<1,pos=0;pos<lim;pos+=stp){
cp w=cp(1,0);
for(int i=0;i<md;i++,w=w*rt){
cp x=a[pos+i],y=w*a[pos+md+i];
a[pos+i]=x+y;
a[pos+md+i]=x-y;
}
}
}
if(tp==-1)for(int i=0;i<lim;i++)F[i].x/=lim;
}
void mul(double *a,double *b,double *c,int A,int B){
LG=0,lim=1;
while(lim<=A+B)lim<<=1,LG++;
for(int i=0;i<lim;i++)rev[i]=(rev[i>>1]>>1)|((i&1)<<(LG-1));
for(int i=0;i<lim;i++)F[i]=cp(a[i],0),G[i]=cp(b[i],0);
FFT(F,1),FFT(G,1);
for(int i=0;i<lim;i++)F[i]=F[i]*G[i];
FFT(F,-1);
for(int i=0;i<=A+B;i++)c[i]=F[i].x;
}
int cut(double *a,int &LIM){
int i=0;
while(a[i]<eps)i++;
while(a[LIM]<eps)LIM--;
for(int j=0;j+i<=LIM;j++)a[j]=a[j+i];
LIM-=i;
return i;
}
double a[500100],b[500100];
int func(int x,int &len){
if(x==1)return 0;
int tmp=func(x>>1,len);
tmp<<=1;
mul(a,a,a,len,len),len<<=1;
tmp+=cut(a,len);
if(!(x&1))return tmp;
mul(a,b,a,len,m),len+=m;
tmp+=cut(a,len);
return tmp;
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d",&m,&n);
for(int i=0;i<=500000;i++)a[i]=b[i]=0;
for(int i=0;i<m;i++)a[i]=b[i]=1.0/m;
m--;
int len=m;
int tmp=func(n,len);
for(int i=1;i<=len;i++)a[i]+=a[i-1];
for(int i=1,x,y;i<=10;i++){
scanf("%d%d",&x,&y),x--;
double X=0,Y=0;
if(x>=tmp){
x-=tmp;
if(x>=len)X=1.0;
else X=a[x];
}
if(y>=tmp){
y-=tmp;
if(y>=len)Y=1.0;
else Y=a[y];
}
printf("%lf\n",Y-X);
}
}
return 0;
}
```
3. 大数据范围
我们发现在$n$很大时最终会近似于一个正态分布,故我们在较大范围内可以直接用正态分布进行积分。可以用自适应辛普森法计算(别问我,我不会)。
4. 模意义下
我们要计算
$$\Bigg(\dfrac{\sum\limits_{i=0}^{m-1}x^i}{m}\Bigg)^n$$
中某一段的和。
我们乘上一个前缀和算子$\dfrac{1}{1-x}$,将其转成差分形式
$$\dfrac{1}{1-x}\times\dfrac{\Big(\sum\limits_{i=0}^{m-1}x^i\Big)^n}{m^n}$$
套个等差数列求和公式,得到
$$\dfrac{1}{1-x}\times\dfrac{\Big(\dfrac{1-x^m}{1-x}\Big)^n}{m^n}$$
继续化简
$$\dfrac{1}{1-x}\times\dfrac{(1-x^m)^n}{(1-x)^n\times m^n}$$
于是
$$\dfrac{(1-x^m)^n}{(1-x)^{n+1}\times m^n}$$
二项式展开
$$\dfrac{\sum\limits_{i=0}^n(-x^m)^i\dbinom{n}{i}}{(1-x)^{n+1}\times m^n}$$
把分母上的东西当作$n+1$阶前缀和
$$\dfrac{\sum\limits_{i=0}^n(-x^m)^i\dbinom{n}{i}}{m^n}\times\sum\limits_{i=0}\dbinom{n+1+i}{i}x^i$$
对于一次询问$[l,r]$,我们只需要管$x^{l-1}$和$x^r$的系数即可。通过预处理一大坨组合计数的东西可以简单计算。
# XVIII.[[AGC049A]Erasing Vertices](https://atcoder.jp/contests/agc049/tasks/agc049_a)
非常原教旨的概率题。假如想到这个point就应该非常easy罢。
我们考虑删掉一个节点的概率。则答案即为所有节点的概率之和。
对于某个节点来说,删去任何不能到达它的节点,对它都没有任何影响;而任意时刻,假如它未被删去,则所有能到达它的节点都未被删去。而在所有能到达它的点中随机删去一个,除非删到它本人,否则都不算数。
故我们发现实际上删掉一个节点的概率,就是 $\dfrac{1}{\text{所有能到达它的点的数量}}$。
于是传递闭包维护一下可达性即可。
代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
int n;
char s[110][110];
double res;
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)scanf("%s",s[i]),s[i][i]='1';
for(int k=0;k<n;k++)for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(s[i][k]=='1'&&s[k][j]=='1')s[i][j]='1';
for(int i=0;i<n;i++){
int now=0;
for(int j=0;j<n;j++)if(s[j][i]=='1')now++;
res+=1.0/now;
}
printf("%.10lf\n",res);
return 0;
}
```