【ZJOI2015】地震后的幻想乡
题面
https://www.luogu.org/problem/P3343
题解
一个几乎显然的暴力做法,枚举每一条边的大小关系,跑$Kruskal$,算出最长的边是第几小的,然后利用“对于$n$个$[0..1]$之间的随机变量$x_1,x_2,...,x_n$,第$k$小的那个的期望值是$\frac{k}{n+1}$”的结论就可以知道这种情况下的时间期望是多少。(虽然这个方法也不是我自己想出来的),这个做法也照应了题面中“当然幽香会先使用一个更加神奇的大魔法来观察出每条边e_i的值,然后再选择完成时间最小的方案”这句话,即当知道$e_i$分布的情况后,就可以跑$Kruskal$算最小生成树。可以说这句话是对题解有暗示作用的。
其实这道题很难,自己看了很多篇题解都没有看懂,最后看了https://blog.csdn.net/qq_41357771/article/details/88732154才搞懂,发现简洁的题解最适合我(我宁愿看简洁但是说不清楚的题解,也不愿看冗杂但讲的很清楚的题解)。想起去年在$qbxt$写过另外一道题,好像叫苹果树,是$P$大的李昊出的($orz\ lhz$),和这道题思路类似。记得当时就没有弄懂。
总结一下这几天刚概率期望的经验,总是把概率期望当成计数问题做,用合法方案除以总方案数,这样思考的方程也更严谨一些,因为如果一步就直接写关于概率/期望的方程就全凭感觉了。(尤其是总方案数显然的题,比如随机数生成器)
对于要取膜的题而言,可以把方案无损转化成概率期望。对于这样只要求一个笼统的数字的题,当数据范围很大时,精度是不够的,要求直接写出概率/期望的式子,还有一种可能是高斯消元和矩阵快速幂(概率即期望),尤其是在$n \le 500$时。
另一个很好的思路是期望转概率,这个思路最初是$aysn$告诉我的,最后把期望化成几个概率的合式,这几道题应该都是这样。
如果只从期望的角度考虑的话,可能利用到期望的线性性,然后考虑贡献(某$CF$题)。
回到这道题。
这个方法自然是对暴力的优化。
令$f[i][s]$为考虑了前$i$条边,$s$集合里面的元素联通的方案数。
令$g[i][s]$为考虑了前$i$条边,$s$集合里面的元素不联通的方案数。
显然,有$$f[i][s]+g[i][s]=C_{cnt[s]}^{i}$$
你可能会问,前$i$条是哪$i$条,事实上,既然是对暴力算法的优化,前$i$条就是权值最小的$i$条边,我们并不知道是哪$i$条,并且我们也不需要知道,因为已经知道了这个联通块的状态,答案只取决于最后加进去的一条边,冗余的边显然是没有贡献的,所以我们不需要知道。我们可以把它理解成任意$i$条边,进一步,更神奇的,转移的时候,我们也不需要枚举当前的边是哪一条,但是它一定是$s$集合中元素与元素间的。
对于转移的情况,有
$$g[i][s]=\sum_{s0 \subset s,0\le k<i}{f[k][s0]\cdot C_{cnt[s\oplus s0]}^{i-k}}$$
我们可以这么理解,就是我们让一个子集一定联通,然后另一部分只能互相联通,即两部分之间一定不连通。
但是这样写很$fake$,因为会多情况,所以我们强制选一个点$x$(随机的,当年,李昊老师说直接选$1$号点),让$x \in s0$恒成立,就可以做到不重不漏了。
其实这一步我也不知道是为什么,但已经不是第一次见了,所以就当做一个定理吧。
算答案已经是老生常谈了,但是这个和其他的题不一样,直接用$g[i][U]$算,即答案$>i$的所有方案数之和,这样的话,我们连乘$i$都不需要了,直接把他们都加起来就好了。
说实话,这道题联通子图的边数竟然是有意义的,这还是我第一次见到。
记一下两个细节吧,因为取的是大于号,所以$g[i][U]$要从$0$开始加,第二是$g[0][i]=1(i.count\ge 2)$
$\mbox{update 2019.10.31 zrt}$说是联通图计数,对于边集给定的情况枚举自己,如果边集是全集,就可以直接乘组合数。
#include<cstdio> #include<cstring> #include<iostream> #include<vector> #define ri register int #define N 12 #define M 46 #define mod 666623333 #define LL long long using namespace std; inline int read() { int ret=0,f=0; char ch=getchar(); while (ch<'0' || ch>'9') f|=(ch=='-'),ch=getchar(); while (ch>='0' && ch<='9') ret*=10,ret+=ch-'0',ch=getchar(); return f?-ret:ret; } LL c[100][100]; int cnt[1<<N]; LL f[M][1<<N],g[M][1<<N]; int has[N][N]; int n,m; int u[M],v[M]; int lowbit(int x) { return x&(-x); } int bit(int x) { for (ri i=0;i<n;i++) if (x&(1<<i)) return i; return -1; } int main() { n=read(); m=read(); for (ri i=1;i<=m;i++) { u[i]=read()-1; v[i]=read()-1; has[u[i]][v[i]]=has[v[i]][u[i]]=1; } cnt[0]=0; for (ri s=1;s<(1<<n);s++) { int t=lowbit(s),t0=bit(t); cnt[s]=cnt[s-t]; for (ri j=0;j<n;j++) if ( ((s-t)&(1<<j)) && has[t0][j]) cnt[s]++; } c[0][0]=1; c[1][0]=1; c[1][1]=1; for (ri i=2;i<=m;i++) { c[i][0]=1; for (ri j=1;j<=i;j++) c[i][j]=c[i-1][j]+c[i-1][j-1]; } for (ri i=0;i<(1<<n);i++) g[0][i]=1; for (ri i=0;i<n;i++) f[0][1<<i]=1,g[0][1<<i]=0; for (ri i=1;i<=m;i++) for (ri s=1;s<(1<<n);s++) { int t=lowbit(s); for (ri s0=(s-1)&s;s0;s0=(s0-1)&s) if (s0&t) for (ri k=0;k<=i;k++) g[i][s]+=f[k][s0|t]*c[cnt[s^s0]][i-k]; f[i][s]=c[cnt[s]][i]-g[i][s]; } double ans=0; int U=(1<<n)-1; for (ri i=0;i<=m;i++) ans+=g[i][U]*1.0/c[cnt[U]][i]; printf("%.6lf\n",ans/(m+1)); return 0; }