[ABC253Ex] We Love Forest

[ABC253Ex] We Love Forest Solution

更好的阅读体验戳此进入

题面

给定 $ n $ 个点无边的图,给定 $ m $ 条待选的无向边。每次等概率地从 $ m $ 条边中抽取一条边加入图中。 $ n - 1 $ 次询问求加 $ 1, 2, \cdots, n - 1 $ 次边后原图形成一个森林(一棵树亦为森林)的概率为多少。对 $ 998244353 $ 取模。

Solution

考虑 FZT,即子集计数,具体可参考 FZT - 子集计数学习笔记[ABC213G] Connectivity 2 Solution,发现本题与 ABC213G 相似。

因为本题存在选的边数的限制,所以不难想到需要在一般 FZT 的状态中额外添加一维记录。

状态类比 ABC213G,较为显然,令 $ F(i, S) $ 表示选了 $ i $ 条边,点集状态为 $ S $ 形成的森林数。令 $ G(S) $ 表示点集状态为 $ S $ 生成树的总方案数。

首先考虑如何处理 $ G(S) $,发现这个东西就是个裸的矩阵树定理。不过也可以通过枚举子集设计转移来实现,令 $ cnt(S, T) $ 表示点集 $ S $ 和点集 $ T $ 之间的边数,方程不难想到为:

\[G(S) = \sum_{T \subsetneq S}G(T) \times G(S \oplus T) \times cnt(T, S \oplus T) \]

然后发现这个东西有重复,不难想到重复分为两种,一种是因为 $ T $ 和 $ S \oplus T $ 对调之后属于相同方案而被重复统计了,这个可以通过钦定一个点解决,或者在最终答案乘一个 $ \dfrac{1}{2} $ 解决。另一种是因为我们的转移,是通过将两个树形结构通过之间的边拼接在一起来实现的,而这种情况也可以认为是将一棵完整的树通过断开某一条边然后分别计算子树,这样的话对于一棵确定的树断开每条边都是等价的,所以会重复边数次,也就是答案需要乘一个 $ \dfrac{1}{\lvert S \rvert - 1} $,故可以考虑将转移改为:

\[G(S) = \dfrac{1}{2(\lvert S \rvert - 1)} \sum_{T \subsetneq S}G(T) \times G(S \oplus T) \times cnt(T, S \oplus T) \]

或:

\[G(S) = \dfrac{1}{\lvert S \rvert - 1} \sum_{\operatorname{lowbit}(S) \in T \subsetneq S}G(T) \times G(S \oplus T) \times cnt(T, S \oplus T) \]

然后初始值大概是 $ G(2^k) = 1 $。

不难发现如果无脑处理 $ cnt $ 的话复杂度是 $ O(2^{2n}m) $,显然寄,于是考虑优化,令 $ cnte(S) $ 表示点集 $ S $ 中的边数,不难想到对于 $ cnt $ 可以如下容斥:

\[cnt(S, T) = cnte(S \vee T) - cnte(S) - cnte(T) \]

则预处理 $ cnte $ 的复杂度为 $ O(m2^n) \(,\) cnt $ 可以 $ O(1) $ 求解,$ G $ 的处理则是 $ O(3^n) $ 的。

然后矩阵树定理的结论大概就是首先对这个图建出拉普拉斯矩阵 $ L $,然后答案就是 $ \det(L_0) $,其中 $ L_0 $ 表示 $ L $ 去掉第 $ i $ 行第 $ i $ 列的子矩阵,其中 $ i $ 任意。具体证明参考 矩阵树定理(Matrix-tree Theorem)笔记。所以只需要矩阵建出来无脑求一下行列式即可。

于是考虑如何转移 $ F $,依然类比 ABC213G,我们令 $ \lvert S \rvert $ 表示点集 $ S $ 中点的数量,也就是 __builtin_popcount(S),不难发现转移:

\[F(i, S) = \sum_{1 \in T \subsetneq S} G(T) \times F(i - (\lvert T \rvert - 1), S \oplus T) \]

也就是钦定一个子集为一棵树,然后剩下的作为森林,钦定 $ 1 $ 节点在树中可以去重,具体在 ABC213G 也有说明。

不过很遗憾这个又是错的。我们在 ABC213G 中钦定 $ 1 $ 是因为 $ 1 $ 一定被选,但是在本题里并没有这个限制,所以我们要换一种写法,不难想到使用 $ S $ 中的最后一个 $ 1 $ 是易于实现且正确的,即 $ \operatorname{lowbit}(S) $,或者说 $ S \land -S $。于是转移改为:

\[F(i, S) = \sum_{\operatorname{lowbit}(S) \in T \subsetneq S} G(T) \times F(i - (\lvert T \rvert - 1), S \oplus T) \]

边界条件的话就是如果 $ S $ 的点数恰好为 $ i + 1 $,那么显然就是一棵树,即 $ F(i, S) = G(S) $。

然后考虑答案,令 $ Smx = 2^n - 1 $,即全集,不难想到对于选 $ i $ 条边的答案 $ ans_i $ 有:

\[ans_i = \dfrac{F(i, Smx) \times i!}{m^i} \bmod{998244353} \]

也不难理解,概率转计数,总方案数为 $ m_i $,然后要让所有点形成森林,方案中选择的 $ i $ 条边任意排列均不同,所以需要在乘一个 $ i! $。

大概就这样了,处理 $ G $ 的复杂度为 $ O(n^3 2^n) $,处理 $ F $ 的复杂度为 $ O(n 3^n) $,处理答案复杂度作为常数,最终复杂度 $ O((m + n^3) 2^n + 3^n) $,可以通过。

Code

#define _USE_MATH_DEFINES
#include <bits/stdc++.h>

#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW(arr) void* Edge::operator new(size_t){static Edge* P = arr; return P++;}

using namespace std;

mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}

typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;

#define MAX_STATUS (20000)
#define MOD (ll)(998244353)
#define EXIST(x) (S & (1 << ((x) - 1)))

template < typename T = int >
inline T read(void);

struct Edge{
    Edge* nxt;
    int to;
    OPNEW;
}ed[1100];
ROPNEW(ed);
Edge* head[20];

int N, M;
int deg[20];
ll G[MAX_STATUS], F[20][MAX_STATUS];
int cnte[MAX_STATUS];
ll fact[30];

int lowbit(int x){return x & -x;}
int CalCnt(int S, int T){return cnte[S | T] - cnte[S] - cnte[T];}
ll qpow(ll a, ll b){
    ll ret(1), mul(a);
    while(b){
        if(b & 1)ret = ret * mul % MOD;
        b >>= 1;
        mul = mul * mul % MOD;
    }return ret;
}

int main(){fact[0] = 1;
    for(int i = 1; i <= 20; ++i)fact[i] = fact[i - 1] * i % MOD;
    N = read(), M = read();
    const int Smx = (1 << N) - 1;
    for(int i = 1; i <= M; ++i){
        int s = read(), t = read();
        head[s] = new Edge{head[s], t};
        head[t] = new Edge{head[t], s};
    }
    for(int S = Smx; S; S = (S - 1) & Smx){
        int cnt(0);
        for(int p = 1; p <= N; ++p)
            for(auto i = head[p]; i; i = i->nxt)
                if(EXIST(p) && EXIST(SON))++cnt;
        cnt >>= 1;
        cnte[S] = cnt;
    }
    for(int S = 1; S <= Smx; ++S){
        if(__builtin_popcount(S) == 1){G[S] = 1; continue;}
        for(int T = (S - 1) & S; T; T = (T - 1) & S)
            if(T & lowbit(S))
                (G[S] += G[T] * G[S ^ T] % MOD * CalCnt(T, S ^ T) % MOD) %= MOD;
        (G[S] *= qpow(__builtin_popcount(S) - 1, MOD - 2)) %= MOD;
    }
    for(int i = 0; i <= N - 1; ++i)
        for(int S = 0; S <= Smx; ++S){
            if(__builtin_popcount(S) == i + 1){F[i][S] = G[S]; continue;}
            for(int T = (S - 1) & S; T; T = (T - 1) & S)
                if(T & lowbit(S) && i - (__builtin_popcount(T) - 1) >= 0)
                    (F[i][S] += G[T] * F[i - (__builtin_popcount(T) - 1)][S ^ T]) %= MOD;
        }
    for(int i = 1; i <= N - 1; ++i)
        printf("%lld\n", F[i][Smx] * fact[i] % MOD * qpow(qpow(M, i), MOD - 2) % MOD);
    fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
    return 0;
}

template < typename T >
inline T read(void){
    T ret(0);
    int flag(1);
    char c = getchar();
    while(c != '-' && !isdigit(c))c = getchar();
    if(c == '-')flag = -1, c = getchar();
    while(isdigit(c)){
        ret *= 10;
        ret += int(c - '0');
        c = getchar();
    }
    ret *= flag;
    return ret;
}

UPD

update-2022_12_07 初稿

posted @ 2023-01-07 15:51  Tsawke  阅读(20)  评论(0编辑  收藏  举报