【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;
}
posted @ 2019-10-20 15:01  HellPix  阅读(175)  评论(0编辑  收藏  举报