VII.[ZJOI2015]地震后的幻想乡
本题有两种思路。
一种思路是从暴力入手并优化状态。
我们考虑边的一组排列\(\{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\)集合内部共有多少条边。则我们有
其中,\(k\)枚举子集,\(l\)枚举\(k\)中有多少条边,二项式系数的意义是从集合\(j\setminus k\)(其中\(\setminus\)是集合减符号)中选出\(i-l\)条边的方案数。
另,我们又有
这很好理解,因为联通数加上不连通数必定等于总方案数。故我们就可以直接通过该式求出\(f_{i,j}\)。
则我们设\(h_i\)表示加入\(i\)条边时整张图联通的概率。于是有
其中\(\mathbb{V}\)是全体点集。
对\(h_i\)做差分就得到位置\(i\)刚好联通的概率;概率再乘上权值(\(\dfrac{i}{m+1}\))就得到了期望。
时间复杂度\(O(m^23^n)\)。
代码:
#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\)的概率。则我们要求的期望则为
仿照前一种思路,我们设 \(P(X)=\int_X^1p(x)\mathrm{d}x\),则有
下面我们考虑如何求出上式。观察到\(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}\)与其补集间边数。
于是就有
考虑到我们最终要求的是\(\int_0^1P_{\mathbb{V}}(X)\mathrm{d}X\);于是开始推式子:
到这里,我们停一下,设一个
则代入上面的式子,就有
于是现在的目标就是求出任意的\(F^k(\mathbb{S})\);这很简单,只需要类似地代入式子中强推即可得到
显然这就可以\(O(m3^n)\)地DP了。最终答案即为\(F^0(\mathbb{V})\)。
代码(极致压行版——它甚至没有我一个推导的式子长):
#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;
}