Luogu P3343 [ZJOI2015]地震后的幻想乡

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Luogu P3343 [ZJOI2015]地震后的幻想乡相关的知识,希望对你有一定的参考价值。

首先转化一下答案:
根据提示,发现其实只需要求出 \\(e_i\\) 对应的排名 \\(rk_i\\) 就可以得出其期望值 \\(\\fracrk_im + 1\\)
所以只需要求排名的期望,最后答案除上 \\(m + 1\\) 就行了

不难想到能把期望值拆成 \\(\\sum_k = 1 ^ m P(k)\\times k\\),可以把期望值拆成对应的概率乘上其权值
发现权值很好求,“加上第 \\(k\\) 条边使原先不连通的图连通”其权值就为 \\(k\\)
那只需考虑求“加上第 \\(k\\) 条边后使原先不连通的图连通”的概率 \\(P(k)\\) 就可算出答案了

感觉 \\(P(k)\\) 不好算,因为需要满足一个条件“当有 \\(k - 1\\) 条边时图不连通”
就考虑对 \\(P(k)\\) 做个前缀和操作 \\(P\'(k)\\sum_i = 0^k P(i)\\),这样 \\(P\'(k) - P\'(k - 1)\\) 就可得到 \\(P(k)\\)
而且发现 \\(P\'(k)\\) 的定义为“用 \\(k\\) 条边使图连通的概率”,没有了“当有 \\(k - 1\\) 条边时图不连通”这个条件,那就感觉好做多了

又根据古典概率的求法,只需要算出“用 \\(k\\) 条边使图连通的方案数”和“选 \\(k\\) 条边的方案数”就可以算出对应的 \\(P\'(k)\\)
发现“选 \\(k\\) 条边方案数”好求,即为 \\(C_m^k\\)
所以只需要算出“用 \\(k\\) 条边使图连通的方案数”\\(f_k\\) 就行了

图连通,便联想到了并查集
这其实就给了一些解题的提示,这道题也可以类似的通过合并两个小集合得到大集合最终算出 \\(f_k\\)
具体来说,可以对每个集合 \\(s\\) 算出“用集合内部的边中的 \\(k\\) 条使 \\(s\\) 里的点组成的图连通”的方案数 \\(f_k, s\\)
发现好像也有点难做,考虑正难则反,设 \\(g_k, s\\) 为“用集合内部的边中的 \\(k\\) 条使 \\(s\\) 里的点组成的图不连通”的方案数,似乎好求些了

当然由于答案需要 \\(f_k, s\\),要从 \\(g_k, s\\) 推出 \\(f_k, s\\)
发现对于 \\(s\\) 里的点组成的图,要么连通,要么不连通,即任意一种情况只属于 \\(f_k, s\\)\\(g_k, s\\)
且能发现设“集合内部的边”的数量为 \\(szl_s\\),则“用集合内部的边中的 \\(k\\) 条”的方案数就是 \\(C_szl_s^k\\)
所以能得到 \\(f_k, s = C_szl_s^k - g_k, s\\)

考虑求 \\(g_k, s\\)
比较容易想到枚举子集把 \\(s\\) 分成两个集合 \\(t, s-t\\),其”内部可以任意连”,但是“两个集合之间不能有连边“
画几个图会发现这样子会算重,原因出自于“内部可以任意连”,举个例子也可以不连,所以就有了很多完全没有连边的情况
对于这个情况,可以直接定义集合 \\(t\\) 满足“集合内的点组成的图连通”,而 \\(s - t\\) 这个集合”随意连边“
感性证明一下:假设现在有 \\(t, t\'\\quad (t\\not = t\')\\)
根据前面的方法,考虑 \\(t\\) 对于 \\(t\'\\) 多出来的一部分点(反过来一样)对于 \\(t\\) 一定与 \\(t\\) 里的点有连边,而对于 \\(t\'\\) 这部分点在 \\(s - \'t\\) 部分,一定与 \\(t\'\\) 里的点没有连边,所以这两个集合绝对不重
其实能发现这个转移还是有重复,因为 \\(t\\)\\(s\\) 子集,则 \\(s - t\\) 肯定也是 \\(s\\) 子集,两部分都连通的情况算重了
考虑选一个点 \\(p\\in s\\),要求 \\(p\\in t\\) 时才能转移,因为这样子就能满足 \\(p\\notin s - t\\),两部分就不会算重了
然后就可以考虑转移了,对于 \\(t\\) 这个集合,“集合内的点组成的图连通”,显然为 \\(f_i, t\\),对于 \\(t - s\\) 这个集合,“任意连边”,因为 \\(t\\) 已经连了 \\(i\\) 条边,所以 \\(s-t\\) 只能连 \\(k - i\\) 条边,明显是一个组合数 \\(C_szl_s-t^k-i\\),因为“两个集合之间不能有连边”,直接乘法原理把两部分乘起来就行了:
\\(g_k, s = \\sum\\limits_t\\in s\\sum\\limits_i = 0^k f_i, t\\times C_szls-t^k - i\\quad (p\\in t)\\)
根据这个式子其实也能发现一个有趣的东西,就是 \\(\\textdp\\) 的顺序是不影响答案的,先枚举 \\(k\\) 和先枚举 \\(s\\) 都可以
初始化就比较简单了,若只有一个点,则没有边也是连通的,若有多个点,则没有边一定不连通
\\(f_0, s = \\begincases1& (s = 2^j)\\\\ 0& (s\\not = 2^j)\\endcases, g_0, s = \\begincases0& (s = 2^j)\\\\ 1& (s\\not = 2^j)\\endcases\\)

时间复杂度 \\(O(3^n m^2)\\)

无注释版本

#include<bits/stdc++.h>
using namespace std;
#define int64 long long
#define double64 long double
const int N = 10, M = 45 + 5;
int E[N][N];
int szl[1 << N];
int64 C[M][M];
int64 f[M][1 << N], g[M][1 << N];
double64 Ph[M], P[M];
int main() 
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) 
        int x, y;
        scanf("%d%d", &x, &y);
        x--, y--, E[x][y] = E[y][x] = 1;
    
    int lim = (1 << n) - 1;
    for (int s = 1; s <= lim; s++) 
        for (int i = 0; i < n; i++) 
            for (int j = i + 1; j < n; j++)  
                if (((s >> i) & 1) && ((s >> j) & 1) && E[i][j]) 
                    szl[s]++;
                
            
        
    
    // 预处理集合内部边的个数
    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 - 1] + C[i - 1][j];
        
    
    // 预处理组合数
    for (int s = 1; s <= lim; s++) 
        f[0][s] = 0, g[0][s] = 1;
    
    for (int i = 0; i < n; i++) 
        f[0][1 << i] = 1, g[0][1 << i] = 0;
    
    // 分只有 1 个点或多个点两种情况预处理
    for (int s = 1; s <= lim; s++) 
        for (int k = 1; k <= m; k++) 
            int mustp;
            for (mustp = 0; ! ((s >> mustp) & 1); mustp++);
            // 求出“必须包含的”p
            for (int t = (s - 1) & s; t; t = (t - 1) & s) 
                if ((t >> mustp) & 1) 
                    // 必须要包含 p
                    for (int i = 0; i <= k; i++) 
                        g[k][s] += f[i][t] * C[szl[s ^ t]][k - i];
                    
                    // 转移
                
            
            f[k][s] = C[szl[s]][k] - g[k][s];
            // 推出 f 的值
        
    
    double64 ans = 0.0;
    for (int i = 0; i <= m; i++) 
        Ph[i] = 1.0 * f[i][lim] / C[m][i];
    
    // 整个图其实就是 lim(2 ^ n - 1)
    for (int i = 1; i <= m; i++) 
        P[i] = Ph[i] - Ph[i - 1];
    
    // 由概率前缀和推回初始概率
    for (int i = 1; i <= m; i++) 
        ans += 1.0 * P[i] * i;
    
    // 概率 * 权值
    printf("%.6Lf", 1.0 * ans / (m + 1));
    // 注意求出来的是排名的期望即 rk 的期望,求出 e 的期望需 / (m + 1)
    return 0;

「ZJOI2015」地震后的幻想乡

题目

点这里看题目。

分析

首先,设原图的最小生成树的边集为 \\(T\\),则容易得到:

\\[\\beginaligned E(\\max_x\\in Te_x) &=\\int_0^1P(t<\\max_x\\in Te_x)\\mathrm dt \\endaligned \\]

而可以发现 \\(P(t<\\max_x\\in Te_x)\\) 恰好为加入了 \\(\\le t\\) 的边之后,图仍然不连通的概率。因此我们可以直接枚举一个边集 \\(E\'\\subseteq E\\),使得加入了 \\(E\'\\) 后图不连通,期望则可以被表述为:

\\[\\beginaligned \\int_0^1P(t<\\max_x\\in Te_x)\\mathrm dt &=\\sum_E\'\\subseteq E\\int_0^1t^|E\'|(1-t)^m-|E\'|\\mathrm d t\\\\ &=\\sum_E\'\\subseteq E\\int_0^1\\sum_j=0^m-|E\'|(-1)^j\\binomm-|E\'|jt^j+|E\'|\\mathrm d t\\\\ &=\\sum_j=0^m\\sum_k=0^m-j(-1)^j\\binomm-kj\\left(\\int_0^1t^j+k\\mathrm d t\\right)\\left(\\sum_E\'\\subseteq E[|E\'|=k]\\right)\\\\ &=\\sum_j=0^m\\sum_k=0^m-j(-1)^j\\binomm-kj\\frac1j+k+1\\left(\\sum_E\'\\subseteq E[|E\'|=k]\\right) \\endaligned \\]

所以,我们尝试计算出 \\(\\sum_E\'\\subseteq E[|E\'|=k]\\),整个问题也便迎刃而解了。注意 \\(E\'\\) 还需要保证不连通,这其实颇有一点麻烦——我们反之保证连通。设 \\(f_S,k\\) 表示在 \\(S\\subseteq V\\)导出子图中,能够保证 \\(S\\) 内点连通且大小恰好为 \\(k\\) 的边集的数量。设 \\(S\\) 的导出子图的边集为 \\(E(S)\\)。正难则反,我们可以想到容斥不连通的情况:

\\[f_S,k=\\binom|E(S)|k-\\sum_T\\subseteq S,T\\neq\\varnothing[\\min T=\\min S]\\sum_j=0^kf_T,j\\binom|E(S\\setminus T)|k-j \\]

如果直接暴力 DP 是 \\(O(3^nm^2)\\) 的。不过注意到,第二维可以看作是生成函数的指数,我们相当于在进行生成函数运算。而我们又知道,最终 \\(f_V\\) 的生成函数的指数一定是 \\(\\le m\\) 的,因此我们可以预先插入 \\(m+1\\) 个点值,最后再做拉格朗日插值得到每一项系数

最终我们得到了一个 \\(O(3^nm+m^2)\\) 的算法。


实现还需要一点技巧:

为了方便,由于 \\(\\binom4522\\approx4.1\\times 10^12\\) 并不是很大,long long 也还装得下,我们可以选一个略大于 \\(\\binom4522\\) 的质数,在它的模域下做运算,这样就简单了许多。

最后,由于我们需要输出浮点数结果,运算精度也是我们需要考虑的问题。注意到,答案最终一定是在 \\((0,1)\\) 范围内,因此我们可以输出它 \\(\\bmod 1\\) 之后的结果。这样的话,像 \\(\\binomm-kj\\frac1j+k+1\\bmod 1\\) 这样的运算,就可以转化为 \\(\\frac1j+k+1\\left(\\binomm-kj\\bmod (j+k+1)\\right)\\)。我们可以直接将分子对分母取模,最后算的时候再计算 \\(ans \\bmod 1\\) 即可。

如果不是不调整精度过不了完全图,我会卡吗我?

小结:

  1. 仍然需要注意常见的运算技巧。这里主要的处理期望的技巧,还是把贡献滚到前缀上,从而转化为概率

  2. 注意一下图上的子集 DP 的方法。一般来说,我们会倾向于通过容斥计算,在无向图的连通性和有向图的强连通中都可以这么做;另一方面,强连通图中也有用耳分解的算法。

  3. 注意实现中的小技巧。尤其是处理精度这一块的,平时接触不多更要注意。

    一般来说,我们会把浮点数压缩到一个较小的范围内,从而保证精度。这里进行 \\(\\bmod 1\\) 及后续运算就是为了达成这一点。

    你也可以说,我是发现所有整数部分都被抵消了才想到了这个方法

代码

#include <cmath>
#include <cstdio>

#define rep( i, a, b ) for( int i = (a) ; i <= (b) ; i ++ )
#define per( i, a, b ) for( int i = (a) ; i >= (b) ; i -- )

typedef long long LL;

const LL mod = 4116715363889;
const int MAXN = 105, MAXS = ( 1 << 10 ) + 5;

template<typename _T>
void read( _T &x ) 
    x = 0; char s = getchar(); bool f = false;
    while( s < \'0\' || \'9\' < s )  f = s == \'-\', s = getchar(); 
    while( \'0\' <= s && s <= \'9\' )  x = ( x << 3 ) + ( x << 1 ) + ( s - \'0\' ), s = getchar(); 
    if( f ) x = -x;


template<typename _T>
void write( _T x ) 
    if( x < 0 ) putchar( \'-\' ), x = -x;
    if( 9 < x ) write( x / 10 );
    putchar( x % 10 + \'0\' );


int fin[MAXN];

LL f[MAXS], g[MAXS];
int cnt[MAXS];

LL C[MAXN][MAXN];

LL ways[MAXN], coe[MAXN];

int N, M;

inline LL Mul( const LL a, const LL b ) 
    return ( a * b - ( LL ) ( ( long double ) a / mod * b ) * mod + mod ) % mod;


inline LL Qkpow( LL, LL );
inline LL Inv( const LL a )  return Qkpow( a, mod - 2 ); 
inline LL Sub( LL x, const LL v )  return ( x -= v ) < 0 ? x + mod : x; 
inline LL Add( LL x, const LL v )  return ( x += v ) >= mod ? x - mod : x; 

inline LL Qkpow( LL base, LL indx ) 
    LL ret = 1;
    while( indx ) 
        if( indx & 1 ) ret = Mul( ret, base );
        base = Mul( base, base ), indx >>= 1;
    
    return ret;


LL Query( const int x ) 
    for( int S = 0 ; S < ( 1 << N ) ; S ++ ) 
        f[S] = 0;
        per( i, M, 0 ) f[S] = Add( Mul( f[S], x ), C[cnt[S]][i] );
        g[S] = f[S];
        for( int T = ( S - 1 ) & S ; T ; T = ( T - 1 ) & S )
            if( ( T & ( - T ) ) == ( S & ( - S ) ) )
                g[S] = Sub( g[S], Mul( g[T], f[S ^ T] ) );
    
    return g[( 1 << N ) - 1];


inline long double Mod1( const long double x )  
    return x - floorl( x );


int main() 
    read( N ), read( M );
    rep( i, 1, M ) 
        int u, v; read( u ), read( v );
        cnt[( 1 << ( u - 1 ) ) | ( 1 << ( v - 1 ) )] ++;
    
    rep( i, 0, M ) 
        C[i][0] = C[i][i] = 1;
        rep( j, 1, i - 1 ) C[i][j] = Add( C[i - 1][j], C[i - 1][j - 1] );
    
    for( int i = 0 ; i < N ; i ++ )
        for( int S = 0 ; S < ( 1 << N ) ; S ++ )
            if( ! ( S >> i & 1 ) ) cnt[S | ( 1 << i )] += cnt[S];
    coe[0] = 1;
    rep( i, 1, M + 1 )
        per( j, M + 1, 0 ) 
            coe[j] = Mul( coe[j], mod - i );
            if( j ) coe[j] = Add( coe[j], coe[j - 1] );
        
    rep( i, 1, M + 1 ) 
        LL inv = Inv( i ), tmp = 1;
        rep( j, 0, M + 1 ) 
            if( j ) coe[j] = Sub( coe[j], coe[j - 1] );
            coe[j] = Mul( coe[j], mod - inv );
        
        rep( j, 1, M + 1 ) if( i ^ j )
            tmp = Mul( tmp, Sub( i, j ) );
        tmp = Mul( Query( i ), Inv( tmp ) );
        rep( j, 0, M ) ways[j] = Add( ways[j], Mul( tmp, coe[j] ) );
        per( j, M + 1, 0 ) 
            coe[j] = Mul( coe[j], mod - i );
            if( j ) coe[j] = Add( coe[j], coe[j - 1] );
        
    
    long double ans = 0;
    rep( i, 0, M ) 
        rep( j, 0, M - i ) 
            int cur = i + j + 1;
            int res = 1ll * ( C[M - i][j] % cur ) * ( Sub( C[M][i], ways[i] ) % cur ) % cur;
            fin[cur] = j & 1 ? ( ( fin[cur] - res ) % cur + cur ) % cur : ( fin[cur] + res ) % cur;
        
    rep( i, 1, M + 1 ) ans = Mod1( ans + ( long double ) fin[i] / i );
    printf( "%.6Lf\\n", ans );
    return 0;

以上是关于Luogu P3343 [ZJOI2015]地震后的幻想乡的主要内容,如果未能解决你的问题,请参考以下文章

「ZJOI2015」地震后的幻想乡

对于有关东方的题目的整理。。

[ZJOI2015]地震后的幻想乡

[ZJOI2015]地震后的幻想乡(期望+dp)

bzoj3925: [Zjoi2015]地震后的幻想乡

bzoj3925 [Zjoi2015]地震后的幻想乡