「WC 2019」数树

「WC 2019」数树

一道涨姿式的EGF好题,官方题解我并无彻底看懂,尝试用指数型生成函数和组合意义的角度推了一波。考场上只得了 44 分也暴露了我在数数的一些基本套路上的不足,后面的 \(\exp\) 是真的神仙,作不出来固然很正常,并且我当时也不怎么会多项式。函数

Task0

考虑公共边组成 \(k\) 个联通块,答案就是 \(y^k\) ,并查集维护一下便可,复杂度 \(\mathcal O(n\log n)\)spa

code

namespace task0{
    map<pair<int, int>, int> mp;
    int fa[N];
    inline int ask(int x){ return x == fa[x] ? x : fa[x] = ask(fa[x]); }
    inline void solve(){ 
        for(int i = 1; i <= n; i++) fa[i] = i;
        for(int i = 1, x, y; i <= 2 * (n - 1); i++)
            read(x), read(y), mp[make_pair(min(x, y), max(x, y))]++;;
        for(map<pair<int, int>, int>::iterator it = mp.begin(); it != mp.end(); it++) if(it->second == 2)
            if(ask(it->first.first) != ask(it->first.second)) fa[ask(it->first.first)] = ask(it->first.second);
        int tot = 0;
        for(int i = 1; i <= n; i++) if(fa[i] == i) tot++;
        cout << Pow(Y, tot) << endl;
    }
}

Task1

考虑两棵树每有一条公共边,联通块个数就 \(-1\) ,不妨设一开始答案为 \(y^n\) ,每有一条公共边,其对答案的贡献就是 \(z=y^{-1}\)code

先按照官方题解说的,引入组合恒等式
\[ z^k=(z-1+1)^k=\sum_{i=1}^k {k\choose i }(z-1)^i \]
考虑红树和蓝树的最终形态若是刚好有 \(k\) 条公共边,那么对答案的贡献就是 \(z^k\) ,考虑枚举这种形态的全部公共边的每个子集,每个大小为 \(i\) 的子集贡献为 \((z-1)^i\) ,就能够获得这个式子的组合意义。get

不妨枚举一个大小为 \(i\) 的公共边集 \(S\) ,(必定是蓝树的一个边集),而后考虑全部公共边集是 \(S\) 的超集的方案,其对答案的贡献就是 \((z-1)^i\) 乘上覆盖它的红树的数量。it

假设当前有 \(i\) 条公共边,造成了 \(m=n-i\) 个联通块,其中第 \(i\) 个联通块大小为 \(a_i\) ,根据 \(prufer\) 经典结论 ,能够获得覆盖这个它的红树的数量。
\[ n^{m-2}\prod a_i \]
那么全部状况对答案的贡献和就是
\[ n^{-2}\sum_{m=1}^n(z-1)^{n-m}n^m\prod_{\sum_{i=1}^m a_i=n,a_i\geq1} a_i \]
考虑后面式子的组合意义是在每一个联通块中刚好选出一个点的方案数,因此能够令 \(dp[u][0/1]\) 表示蓝树以 \(u\) 为根的联通块是否选出一个点的总贡献,此时每一个联通块有 \(n\) 的贡献,每选一条公共边有 \(z\) 的贡献,讨论 \(u\) 的每一个儿子是否和 \(u\) 在一个联通块便可,复杂度 \(O(n)\)class

code

namespace task1{
    int dp[N][2], Z;
    vector<int> g[N];
    inline void dfs(int u, int fa){
        dp[u][0] = 1, dp[u][1] = n;
        for(int i = 0; i < (int) g[u].size(); i++){
            int v = g[u][i];
            if(v == fa) continue;
            dfs(v, u);
            dp[u][1] = (1ll * dp[v][1] * dp[u][1] % P + 1ll * (Z - 1) * (1ll * dp[v][1] * dp[u][0] % P + 1ll * dp[v][0] * dp[u][1] % P) % P) % P;
            dp[u][0] = (1ll * dp[v][1] * dp[u][0] % P + 1ll * dp[v][0] * dp[u][0] % P * (Z - 1) % P) % P;
        }
    }   
    inline void solve(){
        Z = Pow(Y);
        for(int i = 1, x, y; i < n; i++){
            read(x), read(y);
            g[x].push_back(y), g[y].push_back(x);
        }
        dfs(1, 0);
        cout << 1ll * dp[1][1] * Pow(Y, n) % P * Pow(n, P - 3) % P << endl;
    }   
}

Task 2

仍是利用以前的组合恒等式,枚举一个公共边集 \(S\) ,算出其对答案的贡献。map

这一步等价于将 \(n\) 拆分红至多 \(m=n-|S|\) 个联通块,每一个联通块内部已经固定,计算全部拆分方式对答案的贡献:
\[ \sum_{m=1}^n(z-1)^{n-m}\sum_{\sum_{i=1}^{m}a_i=n,a_i\geq1}\dfrac{n!}{m!\prod a_i!} \prod a_i^{a_i-2}\left(n^{m-2}\prod a_i\right)^2 \\ =(z-1)^nn^{-4}\sum_{m=1}^n\sum_{\sum_{i=1}^ma_i=n,a_i\geq1}\dfrac{n!}{m!\prod a_i!}\prod(z-1)^{-1}n^2a_i^{a_i} \\ \]集合

也就是说每个大小为 \(a_i\) 的联通块对答案的贡献为 \((z-1)^{-1}n^2a_i^{a_i}\) ,对这些联通块作有标号的集合拼接再乘上以前的系数能够获得答案,前面的 \(\dfrac{n!}{m!\prod a_i!}\) 的组合意义是对于当前枚举的拼接方式,去除集合内部顺序以及拼接顺序的影响后的方案数。di

其实到这一步 EGF 的形式就已经很显然了,考虑列出每一个联通块的指数型生成函数。
\[ f(x) = \sum_{i=1}^{\infty}\dfrac{(z-1)^{-1}n^2i^{i}}{i!}x^i \]
把这个生成函数 \(\exp\) 一下就天然作完了有标号的集合拼接,前面集合拼接的方案数的系数也不用考虑了。因为 \(\exp\) 后的第 \(n\) 项还有一个 \(\dfrac{x^n}{n!}\) 的形式幂级数要去掉,因此最终式子就变成:
\[ (z-1)^nn^{-4}n![x^n]\exp\left(\sum_{i=1}^{\infty}\dfrac{(z-1)^{-1}n^2i^{i}}{i!}x^i\right) \]
作一遍多项式 \(\exp\) ,注意 \(z = 1\) 也就是 \(y=1\)的时候 \((z-1)^{-1}\) 不存在,须要特判,总复杂度 \(\mathcal O(n \log n)\)make

code

namespace task2{
    int js[N], inv[N], ans[N], f[N];
    inline void solve(){
        if(Y == 1) return (void) (cout << 1ll * Pow(n, n - 2) * Pow(n, n - 2) % P << endl); 
        poly::init();
        js[0] = inv[0] = 1;
        for(int i = 1; i <= n; i++) 
            js[i] = 1ll * js[i-1] * i % P, inv[i] = Pow(js[i], P - 2);
        int Z = Pow(Y, P - 2), c = 1ll * Pow(Z - 1, n) * Pow(n, P - 5) % P * js[n] % P;
        int c2 = 1ll * n * n % P * Pow(Z - 1, P - 2) % P;
        for(int i = 1; i <= n; i++) f[i] = 1ll * c2 * Pow(i, i) % P * inv[i] % P;
        poly::getexp(f, ans, n + 1);
        cout << 1ll * ans[n] * c % P * Pow(Y, n) % P;
    }
}
相关文章
相关标签/搜索