[ZJOI2015]地震后的幻想乡

VII.[ZJOI2015]地震后的幻想乡

本题有两种思路。

一种思路是从暴力入手并优化状态。

我们考虑边的一组排列{p1,,pm}。它是将边按照边权从小到大排列的结果。则我们在这组排列上跑Kruskal,设在加入排名为i的边时跑出了一棵生成树,则这组排列的答案就是排名为i的边权期望,按照题面中的提示,它就是im+1

枚举每一种排列——它们都是等概率出现的——就能求出总概率。则这个算法的时间复杂度是O(m×m!)

考虑优化。因为我们要求的是生成树中最大边,所以多加入一些边并不会影响最大边的大小,于是我们实际上只要找出一个使得之前图不连通,在加入这条边后图联通了的位置i即可。

于是,我们发现在我们Kruskal加入一条新边之时,我们只考虑之前放了多少条边以及图的连通性即可。

发现单独求出加入多少条边时刚好联通不好求;于是我们干脆做个后缀和,设f[i][j]表示加入i条边后集合j联通的方案数,最后做一个差分即可求出所有刚好联通的方案数。

发现f[i][j]不好求。于是我们采取正难则反,考虑设g[i][j]表示集合j不连通的方案数。

我们考虑如何求出j。一个显然的想法是O(3n)枚举子集,将j集合切成两半处理。为了不重不漏地计数,我们就强制令一半联通,另一半随便连,同时两半间不连边。这里我们采取计数问题中的经典策略,找出j中的lowbit位,设为p,并且强制令联通的集合中必须包含p(这就相当于枚举p所在的连通块)。

我们设|j|表示j集合内部共有多少条边。则我们有

gi,j=kj,pkl=0ifl,k×(|jk|il)

其中,k枚举子集,l枚举k中有多少条边,二项式系数的意义是从集合jk(其中是集合减符号)中选出il条边的方案数。

另,我们又有

fi,j+gi,j=(|j|i)

这很好理解,因为联通数加上不连通数必定等于总方案数。故我们就可以直接通过该式求出fi,j

则我们设hi表示加入i条边时整张图联通的概率。于是有

hi=fi,V(mi)

其中V是全体点集。

hi做差分就得到位置i刚好联通的概率;概率再乘上权值(im+1)就得到了期望。

时间复杂度O(m23n)

代码:

#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的概率。则我们要求的期望则为

EX=01p(x)xdx

仿照前一种思路,我们设 P(X)=X1p(x)dx,则有

EX=01p(x)xdx=01p(x)(0x1dt)dx=01(t1p(x)dx)dt=01P(t)dt

下面我们考虑如何求出上式。观察到P(X)的实际意义是最大边大于等于X的概率;于是我们设PS(X)表示S集合中最大边大于等于X的概率,则就有P(X)=PV(X),其中V是全部节点集合。

我们考虑转移出PS(X):我们这次考虑枚举1所在的那个连通块,设此连通块为T,则T联通的概率是1PT(X)T不与其它部分联通的概率是(1X)e(T,ST),其中e(T,ST)S与其补集间边数。

于是就有

PS(X)=1TS(1PT(X))(1X)e(T,ST)

考虑到我们最终要求的是01PV(X)dX;于是开始推式子:

01PS(X)dX=01(1TS(1PT(X))(1X)e(T,ST))dX=01(1TS(1X)e(T,ST)PT(X)(1X)e(T,ST))dX=1TS01(1X)e(T,ST)dX01PT(X)(1X)e(T,ST)dX=1TS01Xe(T,ST)dX01PT(X)(1X)e(T,ST)dX=1TS1e(T,ST)1+e(T,ST)0e(T,ST)1+e(T,ST)01PT(X)(1X)e(T,ST)dX=1TS11+e(T,ST)01PT(X)(1X)e(T,ST)dX

到这里,我们停一下,设一个

Fk(S)=01(1X)kPS(X)dX

则代入上面的式子,就有

F0(S)=1TS11+e(T,ST)Fe(T,ST)(T)

于是现在的目标就是求出任意的Fk(S);这很简单,只需要类似地代入式子中强推即可得到

Fk(S)=1TS11+k+e(T,ST)Fk+e(T,ST)(T)

显然这就可以O(m3n)地DP了。最终答案即为F0(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;
} 

posted @   Troverld  阅读(76)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示