题解 P3343 【[ZJOI2015]地震后的幻想乡】
command_block · · 题解
先膜拜积分大佬!
就着这道题谈一下我做概率期望题的感想吧。
这之中离不开的就是方案数+概率+期望
大家都知道
如果一道题加模数的话,三个量可以不掉精度地转化,以方便我们的分析。
(比如这道题)
回到本题的平民做法,我们知道要求的是最小生成树上边的最大值的期望。
这东西一看就很不好求,而且边的权都不是离散的,而是
题目很良心,有一个提示:
对于n个[0,1]之间的随机变量x1,x2,...,xn,第k小的那个的期望值是k/(n+1)
这让我们想到把期望转化成排名的期望,这样子就能化连续为离散(一般来讲,这可以使问题变简单,在P3600里面也有这样的细节)。
现在主要的复杂点就在于最小生成树,这是个很复杂的东西,我们想方设法挖掘本质,将它变简单。
假设你知道了边的排名,然后在跑克鲁斯卡尔。
你会一条一条地加边,然后判断是否生成了树。
答案就是最后加入的那一条边的排名。
那么我们就有了第一个算法:
枚举边的排名,然后跑克鲁斯卡尔。
得到排名的期望,再除以(m+1)得到答案。
复杂度O(m*m!)据说分数可观。
我们考虑“生成了树”的本质是什么,在克鲁斯卡尔中,就是把n个点都联通了起来。
一个最小生成树内最大的边排名为x的概率=用k条边恰好联通整个图的概率(这个很好理解吧)
根据期望的定义,排名的期望=
其中
恰好的意思是,没有第k条边的时候,不连通,加一条边就联通了。
这个限制令人头疼,我们把它前缀和一下(P3600里面也有类似套路)
得到
根据人类智慧
(仔细体味
然后因为前缀和,
我们只要想办法弄出
至此,问题已经变成了图的联通性问题(边的选取问题)。
那么问题来了,这玩意怎么求?
如果一道题加模数的话,三个量可以不掉精度地转化,以方便我们的分析。
这道题没加模数,不代表我们不能转化,我们考虑转化成方案数。
方案数会不会爆long long,因为最多只有
这里我们采用dp求方案数。
设
想了想发现不可做。
于是采用正难则反,这
我们设
一个很好理解的东西
我们考虑如何转移出
我自然地想到了枚举子集,把
(如果没听说过枚举子集的话建议上网查一下,或者去做P3959)
那么,必须令这s不连通,那好办,直接令这s1,s2之间没有边。
那么s1,s2内部就可以随便连。
这样肯定是不会算漏,但会重复。
第一次分割
1 # 2
#
### 3
根据任意连边,所有点之间都不连边的方案适合这个分割线
第二次分割
1 # 2
####
3
根据任意连边,所有点之间都不连边的方案也适合这个分割线
很明显存在重复计算
之所以会存在重复计算,是因为分割线可能会重复穿过“随便连”的部分。
我们dp出了
这样子,分割线就不会再次穿过s1(因为其内部联通)
现在还有一个问题,如果s1与s2调换,还是会存在重复算的情况(必须联通∈随便连)
所以当我们枚举过某个s1时,就不能再次枚举它的补集作为s1。
这里有个非常脑洞的操作,随便钦点一个s内的点p,令s1必须包含p,这样子,枚举过的s1的补集必然不包含p,也就被排除在外了。
理解了这些(建议结合其他题解理解这个,真的不好懂)就好办了,下面是dp的代码与详细注释:
long long f[50][1200],g[50][1200],c[50][50];
//f:联通; g:不连通; c:组合数;
int siz[1200];
//siz:某点集内边的数量;
bool e[12][12];
//e:邻接矩阵;
for (int s=0;s<=limit;s++)g[0][s]=1;
for (int i=0;i<n;i++)
{f[0][1<<i]=1;g[0][1<<i]=0;}
//根据“一个点自身是联通的”来初始化
for (int s=0;s<=limit;s++)
for (int i=0;i<n;i++)
for (int j=i+1;j<n;j++)
if (e[i][j]&&((1<<i)&s)&&((1<<j)&s))
siz[s]++;
//求解siz
for (int i=0;i<=m;i++){
c[i][0]=1;
for (int j=1;j<=i;j++)
c[i][j]=c[i-1][j]+c[i-1][j-1];
}//预处理组合数
for (int i=1;i<=m;i++)
for (int s=1;s<=limit;s++){
int p=0;
for (;((1<<p)&s)==0;p++);
//寻找一个s内的点(s必然不为空集)
for (int k=(s-1)&s;k;k=(k-1)&s)
if ((1<<p)&k){
//这里还要枚举分给两个子集各多少条边
for (int j=0;j<=i;j++)
g[i][s]+=f[j][k]*c[siz[s^k]][i-j];
//如上文,一部分随便选,一部分必须联通
}
f[i][s]=c[siz[s]][i]-g[i][s];
//根据c[siz[s]][i]=g[i][s]+f[i][s]求出f[i][s]
}
至此,dp的部分就讲完了,我们回到概率
代码
double p[50],p2[50];
double ans=0;
for (int i=0;i<=m;i++)
p2[i]=(double)f[i][limit]/(double)c[m][i];
//计算p'
for (int i=0;i<=m;i++)p[i]=p2[i]-p2[i-1];
//计算p
for (int i=0;i<=m;i++)ans+=p[i]*i;
//计算排名的期望
printf("%.6lf",ans/(m+1));
//得到最大边的期望