zoukankan      html  css  js  c++  java
  • 「ZJU Summer Training 2020

    补题地址:https://zjusummer.contest.codeforces.com/

    Content

    • ZJU-ICPC Summer 2020 Contest 7 by Group A
      • Problem B. Card Game
      • Problem D. Cafeterias
      • Problem E. Lifelong Learning
    • ZJU-ICPC Summer 2020 Contest 8 by Group B
      • Problem C. Thresholding
      • Problem D. Mysterious Machine
      • Problem E. Magic
      • Problem F. Can you find B
    • ZJU-ICPC Summer 2020 Contest 9 by Group C
      • Problem A. An easy problem
      • Problem B. Ball
      • Problem F. FFT

    ZJU-ICPC Summer 2020 Contest 7 by Group A

    Problem B. Card Game

    你正在与 (p(le 10^5))(包括自己)个人玩一个游戏,每个人手上有一张手牌,手牌上有数字 (b_1, b_2, cdots b_n(in[1, c(le 10^5)]))。牌堆大小为 (n),其上的数字 (in[1, c])。每个人轮流将桌上的一张卡片换成自己手上的卡片,你是第一个操作的。如果最后的桌子上留下的卡片中 (h(in [1, c]))唯一 的众数那么视为你成功。求有多少种可行的操作使你必胜,并输出必胜策略中交换的牌。

    我们不妨从最坏情况考虑——每个人都不希望你赢,于是纷纷把你的牌 (h) 换掉。

    因此我们令自己 最后操作。枚举一遍牌堆的牌并验证。维护全局众数可以直接线段树。

    时间复杂度 (O(nlog c))。注意 (n = 1) 时特判,具体见代码。

    #include <iostream>
    #include <vector>
    
    using namespace std;
    const int N = 1e5 + 5;
    
    int n, p, maxc, h;
    int a[N], c[N];
    
    #define mid ((l + r) >> 1)
    int val[N << 2], cnt[N << 2];
    void pushup(int x) {
        val[x] = max(val[x << 1], val[x << 1 | 1]);
        cnt[x] = 0;
        if (val[x] == val[x << 1]) cnt[x] += cnt[x << 1];
        if (val[x] == val[x << 1 | 1]) cnt[x] += cnt[x << 1 | 1];
    }
    void build(int l, int r, int* dat, int x = 1) {
        if (l == r) { val[x] = dat[l], cnt[x] = 1; return; }
        build(l, mid, dat, x << 1);
        build(mid + 1, r, dat, x << 1 | 1);
        pushup(x);
    }
    void update(int p, int v, int l = 1, int r = maxc, int x = 1) {
        if (l == r) { val[x] += v; return; }
        if (p <= mid) update(p, v, l, mid, x << 1);
        else update(p, v, mid + 1, r, x << 1 | 1);
        pushup(x);
    }
    
    signed main() {
        ios::sync_with_stdio(false);
        cin >> n >> p >> maxc >> h;
        for (int i = 1; i <= n; i++)
            cin >> a[i], c[a[i]]++;
    
        
        if (n == 1) {
            int t = 0;
            for (int i = 1; i <= p; i++)
                cin >> t;
            if (t == h) printf("1
    1");
            else printf("0");
            return 0;
        }
        
        int cur; cin >> cur;
        for (int i = 2; i <= p; i++) {
            int t; cin >> t;
            --c[h], ++c[t];
        }
        
        build(1, maxc, c);
    
        vector<int> ans;
        for (int i = 1; i <= n; i++) {
            update(a[i], -1);
            --c[a[i]], ++c[cur];
            update(cur, 1);
    
            if (val[1] == c[h] && cnt[1] == 1)
                ans.push_back(i);
    
            update(a[i], 1);
            ++c[a[i]], --c[cur];
            update(cur, -1);
        }
    
        cout << ans.size() << endl;
        for (auto i : ans)
            cout << i << ' ';
        return 0;
    }
    

    Problem D. Cafeterias

    给定一个 (n(le 10^5)) 个点, (m(le 2 imes 10^5)) 条边的无向图,并标有 (k(le n)) 个特殊点。现给定 (T(le 10^5)) 个询问,每次给出整数 (t(le 10^9)),求有多少点存在到达特殊点,长度为 (t-1)不一定 简单的路径。

    显然一跳路径可以通过“反复横跳”来加长路径:(1 o 3 o 4) 可以变为 (1 o 3 o 4 o 3 o 4)。而且一次增加 (2)

    于是求出两种最短路——长度分别为奇数和偶数时的最短路径长。求出最短路可以通过“反复横跳”来构造出新的路径。由于图不带边权,于是直接 Bfs。

    然后将所有最短距离排序(无法到达记为无限大)。

    对于询问的 (t),我们先判断其奇偶性,再在最短路长数组上二分即可。

    时间复杂度 (O(nlog n))

    /*
     * Author : _Wallace_
     * Source : https://www.cnblogs.com/-Wallace-/
     * Article : ZJU Summer Training 2020
    */
    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <queue>
    #include <vector>
    
    using namespace std;
    const int N = 1e5 + 5;
    
    int n, m, k, T;
    bool spec[N];
    vector<int> G[N];
    long long dist[2][N];
    
    struct queNode { int pos; long long dis; };
    void solve() {
        queue<queNode> que;
        memset(dist, 0x3f, sizeof dist);
    
        for (int i = 1; i <= n; i++)
            if (spec[i]) que.push(queNode{i, 0});
        
        while (!que.empty()) {
            queNode x = que.front();
            que.pop();
    
            if (dist[x.dis & 1][x.pos] <= x.dis)
                continue;
            dist[x.dis & 1][x.pos] = x.dis;
    
            for (auto y : G[x.pos])
                que.push(queNode{y, x.dis + 1});
        }
    }
    
    vector<long long> res[2];
    
    signed main() {
        scanf("%d%d%d", &n, &m, &k);
        for (int i = 1; i <= k; i++) {
            int p; scanf("%d", &p);
            spec[p] = true;
        }
        
        for (int i = 1; i <= m; i++) {
            int u, v; scanf("%d%d", &u, &v);
            G[u].push_back(v);
            G[v].push_back(u);
        }
    
        for (int i = 1; i <= n; i++)
            if (G[i].size() == 0) spec[i] = false;
        solve();
        
        res[0] = vector<long long>(dist[0] + 1, dist[0] + 1 + n);
        res[1] = vector<long long>(dist[1] + 1, dist[1] + 1 + n);
        sort(res[0].begin(), res[0].end());
        sort(res[1].begin(), res[1].end());
    
        scanf("%d", &T);
        while (T--) {
            long long t;
            scanf("%lld", &t), --t;
            if (t == 0) printf("%d
    ", k);
            else printf("%d
    ", int(upper_bound(res[t & 1].begin(), res[t & 1].end(), t) - res[t & 1].begin()));
        }
    }
    

    Problem E. Lifelong Learning

    给定一个长为 (n(le 10^6)) 的数列 ({a_1, a_2, cdots, a_n}(a_i le 10^6))。每次操作可以改变数列中的一个元素的值。求最少的操作次数,使最终的数列满足:前半部分满足 (a_i + 1 = a_{i + 1}),后半部分满足 (a_i - 1 = a_{i + 1})。若 (n) 为偶数则 (a_{frac n 2} = a_{frac n 2+1})

    我们先将每一个数,以中间数 ( ext{mid} = lfloor dfrac{n+1}{2} floor) 为基准,对于每一个位置 (i),进行操作 (a_i leftarrow a_i + |i - ext{mid}|)

    接下来我们在操作后的 (a) 数列上贪心地取众数,记众数的出现次数为 (k)。显然这样可以最大化不改的数的个数。

    答案即为 (n - k)

    #include <cstdio>
    #include <algorithm>
    
    using namespace std;
    const int N = 1e6 + 5;
    
    int n, a[N];
    int buc[N << 1];
    
    signed main() {
        scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%d", a + i);
        
        int mid = (n + 1) / 2;
        for (int i = 1; i < mid; i++)
            a[i] += mid - i;
        if (!(n & 1)) ++mid;
        for (int i = mid + 1; i <= n; i++)
            a[i] += i - mid;
        
        for (int i = 1; i <= n; i++)
            buc[a[i]]++;
        
        int mode = *max_element(buc + 1, buc + (N << 1));
        printf("%d
    ", n - mode);
        return 0;
    }
    

    ZJU-ICPC Summer 2020 Contest 8 by Group B

    Problem C. Thresholding

    给定一个 (w(le 640) imes h(le 640)) 的矩阵,矩阵中的元素为整数且 (in [0, 2^{16}))。你可以设一个间值 (k),将矩阵中所有 (le k) 的元素归入集合 (0),其他的归入集合 (1)。设集合 (S) 占总数大小的比例为 (omega_S),集合 (S) 的方差为 (sigma_S^2)。求 (sigma_w^2 = omega_0sigma_0^2 + omega_1sigma_1^2) 的最小值。

    注意到本题中的“矩阵”是假象,实质上就是一堆数字。

    于是显然可以枚举数字的分布情况,这可以通过排序完成。枚举时需要动态维护左右两部分的方差,可以使用公式 (sigma = sum x^2 - (sum x)^2)。预处理一个前缀和以及前缀

    但理论上不能直接扫,因为我们不能让两个相同的元素在不同的集合里,于是这里要特判。然而好像不判也过了

    复杂度 (O(whlog (wh)))。直接三分应该也行。

    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    
    using namespace std;
    const int N = 640 * 640 + 5;
    
    inline double sqr(double x) {
        return x * x;
    }
    double val[N], s[N], ss[N];
    int w, h, n;
    
    inline double getvar(int l, int r) {
        if (l > r) return 0;
        return (ss[r] - ss[l - 1]) / (r - l + 1) - sqr((s[r] - s[l - 1]) / (r - l + 1));
    }
    
    signed main() {
        scanf("%d%d", &w, &h), n = w * h;
        for (int t, i = 1; i <= n; i++)
            scanf("%d", &t), val[i] = t;
        
        sort(val + 1, val + 1 + n);
        
        for (int i = 1; i <= n; i++)
            s[i] = s[i - 1] + val[i], ss[i] = ss[i - 1] + sqr(val[i]);
    
        double ans = getvar(1, n);
    
        for (int i = 1; i < n; i++)
            if (val[i] != val[i + 1])
                ans = min(ans, getvar(1, i) * (i * 1.0 / n) + getvar(i + 1, n) * ((n - i) * 1.0 / n));
    
        printf("%.10f
    ", ans);
        return 0;
    }
    

    Problem D. Mysterious Machine

    给定三个正整数 (x, y, z(le 5 imes 10^3))。构造三个 01 串 (A, B, C),满足 ( ext{LCS}(A, B) = x, ext{LCS}(B, C) = y, ext{LCS}(A, C) = z)。其中 ( ext{LCS}(S, T)) 表示字符串 (S, T) 的最长公共子序列长度。构造出的长度不超过 (10^4)

    构造题。我们钦定 (x le y le z),那么我们先将在字符串 (A) 中填充 (z)( exttt 1),在 (B) 中填充 (x)( exttt 1)。这样保证了 ( ext{LCS}(A, B) = x)

    然后再向 (B) 后面添加 (y - x)( exttt 0),最后在 (C) 中加 (5000)( exttt 1)(5000)( exttt 0)。这样显然满足题意,因为我们可以近似的认为 ( ext{LCS}(B, C) = |B| = y, ext{LCS}(A, C) = |A| = z)

    #include <algorithm>
    #include <iostream>
    
    using namespace std;
    const int N = 1e4 + 5;
    
    char str[3][N];
    int x, y, z, a, b, c;
    
    signed main() {
        cin >> x >> y >> z;
        a = 0, b = 1, c = 2;
    
        if (x > z) swap(x, z), swap(b, c);
        if (x > y) swap(x, y), swap(a, c);
        if (y > z) swap(y, z), swap(b, a);
    
        fill(str[a] + 1, str[a] + 1 + z, '1');
        fill(str[b] + 1, str[b] + 1 + x, '1');
        fill(str[b] + x + 1, str[b] + y + 1, '0');
        fill(str[c] + 1, str[c] + 5000 + 1, '1');
        fill(str[c] + 5001, str[c] + 10000 + 1, '0');
    
        cout << (str[0] + 1) << endl;
        cout << (str[1] + 1) << endl;
        cout << (str[2] + 1) << endl;
        return 0;
    }
    

    Problem E. Magic

    给定 (n(le 20)) 个字符串 (S_1, S_2, cdots , S_n(sum|S_i| le 10^5)),任意排列这些字符串并拼接,可以计算出得到的新串的 本质不同 的子序列的个数,空串也算。求有多少种排列的方式,使得得到的新串的本质不同的子序列的个数为偶数。字符集 (Sigma) 为全体小写英文字母。

    若只计算单串 (S) 的本质不同的子序列的个数,我们可以设计一个动态规划算法:设 (f(i, c)) 为子串 (S[1, i]) 以字符 (c) 结尾的本质不同的子序列的个数,且令 (s(i) = sumlimits_{cin Sigma} f(i, c)),即当前的答案。那么有状态转移方程:

    [f(i, c) = egin{cases} f(i - 1, c) & c e s[i] \ s(i-1) & c = s[i] end{cases} \ s(i) = 2s(i-1) - f(i, s[i]) ]

    由于本题只与奇偶性有关,我们不妨对 (2) 取模:

    [s(i) = (2s(i - 1) - f(i, s[i]))mod 2 = f(i, s[i]) ]

    这相当于 (f)(s) 之间的 交换


    我们注意以下初始状态:(f = 0),只有 (s(0) = 1)(空串也算),而上式只存在交换,因此不难断定 为 1 的字符位置只有一个

    那么我们只需抓住这个位置即可。我们使用上述思想预处理出 (t)(代码中为 (f) 数组) —— (t(S, c)) 表示 1 的位置为 (c) 开始,“通过”在字符串 (S) 后 1 的位置。


    根据这个 1 的位置,我们再考虑一种 dp:(g(S, c)) 表示,字符串集合(编号) (S) 中的字符串任意拼接,当 1 的位置为字符 (c) 时,存在几种满足题意的方案。

    那么对于每一个状态 (g(S, c)),有:

    [g(S cup {i}, t(i, c)) leftarrow g(S cup {i}, t(i, c)) + g(S, c) ]

    显然需要满足条件 (i ot in S)

    状态压缩一下,可以有 (O(2^n imes n|Sigma|))

    #include <cstdio>
    #include <cstring>
    
    using namespace std;
    const int N = 21;
    const int S = 26;
    const int L = 1e5 + 5;
    
    int n;
    char str[L];
    
    int f[N][S + 1];
    long long g[1 << N][S + 1];
    
    signed main() {
        scanf("%d", &n);
        for (int i = 0; i < n; i++) {
            scanf("%s", str + 1);
            int l = strlen(str + 1);
            for (int j = 0; j <= S; j++) {
                f[i][j] = j;
                for (int p = 1; p <= l; p++)
                    if (f[i][j] && str[p] - 'a' + 1 == f[i][j]) f[i][j] = 0;
                    else if (!f[i][j]) f[i][j] = str[p] - 'a' + 1;
            }
        }
    
        g[0][0] = 1;
        for (int i = 0; i < (1 << n); i++)
            for (int k = 0; k < n; k++)
                if (!(i & (1 << k)))
                    for (int j = 0; j <= S; j++)
                        g[i | (1 << k)][f[k][j]] += g[i][j];
    
        long long ans = 0;
        for (int i = 1; i <= S; i++)
            ans += g[(1 << n) - 1][i];
        
        printf("%lld
    ", ans);
        return 0;
    }
    

    F. Can you find B

    给定长度为 (n(le 100)) 的数列 ({a_1, a_2, cdots , a_n}(a_i le 50))。试构造出数列 ({b}) ,使 (sumlimits_{i = 1} ^n |a_i - b_i|) 最小且 ({b}) 中的任意两个元素 (a, b) 满足 (gcd(x, y) = 1)。求出这个最小值。

    (gcd = 1) 的条件是比较苛刻的,这导致了 ({b}) 中会存在大量的 (1)

    又因为 (a_i le 50),所以 (b_i le 100),原因是选择超过 (100) 的数显然不如直接选 (1)

    于是我们有这样一个 dp:设 (f(i, S)) 表示对于 (a) 的前 (i) 个数,选定数的 质因子 集合为 (S),得到 (a, b) 的最小总差值。

    那么有转移方程:

    [f(i, S) = min_{ ext{div}(j) otsubseteq T } { f(i-1, Tcup ext{div}(j)) + |a_i - j| } ]

    然而直接状压计算量高达 (2^{25} cdot 100 cdot n),原地爆炸。因为 ([1, 100]) 内的质数有 (25) 个。


    我们又发现一个性质:对于一个 (>50) 的质数,我们只可能选择其本身而非其倍数。原因见上,显然。

    于是我们只枚举 (50) 以内的质数即可——范围缩小至 (15) 个。对于 (a) 数列,我们贪心地将其从小到大排序,小的对应小的。

    我们计算答案时,我们取 左半部分的 dp,右半部分的较大质数

    dp 仍然是那个 dp,复杂的直降至 (O(2^p imes n imes max {a})),其中 (p = 15)

    #include <algorithm>
    #include <cstring>
    #include <iostream>
     
    using namespace std;
     
    const int inf = 0x3f3f3f3f;
    const int N = 100;
    const int P = 25; // <= 100
    const int T = 15; // <= 50
    const int prime[P + 5] = {
        2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
        31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
        73, 79, 83, 89, 97
    };
    
    int ds[N + 5]; // set of prime divisions
    int n, a[N + 5];
     
    int f[N + 5][1 << T];
    void dynamicPrograming() {
        memset(f, 0x3f, sizeof f);
        f[0][0] = 0;
        for (int i = 1; i <= n; i++)
            for (int S = 0; S < (1 << T); S++) {
                f[i][S] = min(f[i][S], f[i - 1][S] + a[i] - 1);
                for (int j = 2; j <= N; j++) {
                    if (ds[j] == -1 || (S & ds[j])) continue;
                    int R = S | ds[j];
                    f[i][R] = min(f[i][R], f[i - 1][S] + abs(j - a[i]));
                }
            }
    }
     
    signed main() {
        ios::sync_with_stdio(false);
     
        for (int i = 2; i <= N; i++)
            for (int j = 0; j < T; j++)
                if (i % prime[j] == 0)
                    ds[i] |= (1 << j);
        for (int i = T; i < P; i++)
            ds[prime[i]] = -1;
    
        cin >> n;
        for (int i = 1; i <= n; i++)
            cin >> a[i];
        sort(a + 1, a + 1 + n);
        dynamicPrograming();
     
        int ans = inf;
        for (int i = n; i && n - i <= 9; --i) {
            int cur = inf;
            cur = min(cur, *min_element(f[i], f[i] + (1 << T)));
            for (int j = i + 1; j <= n; j++)
                cur += abs(a[j] - prime[T + j - i - 1]);
            ans = min(ans, cur);
        }
     
        cout << ans << endl;
        return 0;
    }
    

    ZJU-ICPC Summer 2020 Contest 9 by Group C

    Problem A. An easy problem

    给定 (n(le 10)) 个互不相同数字 ((in[0, 9]))。选择若干个数字拼接成整数 (a),剩下拼成整数 (b),拼成的数不应含前导零。求 (min |a - b|)

    真·签到。(n) 很小,可以暴力枚举 next_permutation

    但是有一个技巧——要最小化差值,一定是从中间分割数列。

    一个小优化:(n=10) 时答案固定,直接输出。

    #include <algorithm>
    #include <climits>
    #include <cmath>
    #include <iostream>
    
    using namespace std;
    const int N = 10;
    
    int T, n;
    int a[N];
    
    void solve() {
        cin >> n;
        for (int i = 0; i < n; i++)
            cin >> a[i];
        
        if (n == 10) {
            cout << 247 << endl;
            return;
        }
        
        int ans = INT_MAX;
        sort(a, a + n);
        do {
            int left = 0, right = 0;
            int mid = n / 2;
    
            if ((a[0] == 0 || a[mid] == 0) && n >= 3)
                continue;
    
            for (int i = 0; i < mid; i++)
                left = left * 10 + a[i];
            for (int i = mid; i < n; i++)
                right = right * 10 + a[i];
            
            ans = min(ans, abs(left - right));
        } while(next_permutation(a, a + n));
    
        cout << ans << endl;
    }
    
    signed main() {
        for (cin >> T; T; --T) solve();
        return 0;
    }
    

    Problem B. Ball

    给定两个球体,坐标大小,半径 (le 10^2),求两个球体的体积并。

    计算几何,分类讨论。

    • 不相交,答案为 (V_1 + V_2)
    • 包含,答案为 (max(V_1, V_2))
    • 否则,答案为 ((V_1 + V_2) - (r_1 + r_2 - d)^2pi imes (d^2 + 2d(r_1 + r_2) - 3(r_1^2 + r_2^2) + 6r_1r_2)/(12d)),其中 (r_1, r_2) 为两圆半径,(d) 为两圆心距离。
    #include <cmath>
    #include <cstdio>
    #include <iostream>
    #include <tuple>
    
    using namespace std;
    const double pi = acos(-1);
    const double eps = 1e-10;
    typedef tuple<double, double, double> Point;
    
    double sqr(double x) {
        return x * x;
    }
    double dist(Point& A, Point& B) {
        return sqrt(sqr(get<0>(A) - get<0>(B))
                  + sqr(get<1>(A) - get<1>(B))
                  + sqr(get<2>(A) - get<2>(B)));
    }
    double volume(double r) {
        return pi * pow(r, 3) * 4.0 / 3.0;
    }
    bool included(Point& A, double r1, Point& B, double r2) {
        return dist(A, B) + min(r1, r2) < max(r1, r2);
    }
    double intersection(Point& A, double r1, Point& B, double r2) {
        double d = dist(A, B);
        return pi * sqr(r1 + r2 - d)
             * (sqr(d) + d * 2 * (r1 + r2) - 3 * (sqr(r1) + sqr(r2)) + 6 * r1 * r2) / (12 * d);
    }
    
    signed main() {
        Point A, B;
        double r1, r2;
    
        cin >> get<0>(A) >> get<1>(A) >> get<2>(A) >> r1;
        cin >> get<0>(B) >> get<1>(B) >> get<2>(B) >> r2;
    
        if (r1 + r2 - dist(A, B) < eps) {
            printf("%.10f
    ", volume(r1) + volume(r2));
            return 0;
        }
    
        if (included(A, r1, B, r2)) {
            printf("%.10f
    ", max(volume(r1), volume(r2)));
            return 0;
        }
    
        double ans = volume(r1) + volume(r2) - intersection(A, r1, B, r2);
        printf("%.10f
    ", ans);
        return 0;
    }
    

    Problem F. FFT

    给定 (n(le 10^2)) 个(有标号的)点,你可以在其间任意连边使之成为连通图。现在限定只允许连 (m) 条边,试问连成连通图的方案数。你需要根据所有的 (m in [0, frac{n(n - 1)}{2}]) 输出答案。

    本题实质上是 限定边数有标号连通图计数 问题。

    以下用 (c(x)) 表示 (frac{x(x - 1)}{2})

    考虑一种 dp:设 (F(m, k)) 为通过 (m) 条边连出 (k) 个连通块的方案数,(G(m, k)) 表示通过 (m) 条边,连成 (k) 个“假块”(人为划分的块,块间可能有连边)的方案数。

    那么我们可以用 (F) 来表示 (G)

    [G(m, k) = sumlimits_{j = k}^{n} F(m, j) egin{Bmatrix} j \ k end{Bmatrix} ]

    我们可以选择个数大于等于 (k) 的块数进行“合并”,其中 (egin{Bmatrix} j \ k end{Bmatrix}) 表示 (j) 个连通块并入 (k) 个“假块”的方案数,即 第二类斯特林数

    但如果要反过来用 (G) 表示 (F) 呢?于是我们使用 斯特林反演 点击学习

    [F(m, k) = sumlimits_{j=k}^n G(m, j)egin{bmatrix}j\kend{bmatrix}(-1)^{j-k} ]

    方案数那么答案就是当 (k = 1) 时,代入得 (sumlimits_{j=1}^n G(m, j)(j-1)!(-1)^{j-1}) 即为所求。


    但我们发现这个直接求居然比暴力 (O(n^6)) 还慢。于是我们把这个 (G) 拆成 (H)——(H(i, j, m)) 表示选取 (i) 个点,构成 (j) 个块(仍然是“假块”),余下可用的边数(注意这里定义反转了,不过这样好写)为 (m) 的方案数。有关系:

    [G(m, j) = sumlimits_{k=m}^{c(n)} H(n, j, k) egin{pmatrix} k \ mend{pmatrix} ]

    这个组合数表示在剩余的 (k) 条边中选出 (m) 条的方案数。

    但我们又发现直接求 (H) 仍然爆炸,于是我们把 块数那一维缩掉,只保留两维。

    如何加入系数 ((j-1)!(-1)^{j-1}) 的影响?只要在转移时乘上 (-1) 即可。

    组合数系数的含义详见代码。

    #include <iostream>
    
    using namespace std;
    const int N = 105;
    const int mod = 998244353;
    typedef long long LL;
    
    int n;
    LL fact[N * N];
    LL invf[N * N];
    LL f[N][N * N]; /* 上文的 H */
    LL g[N]; /* 答案 */
    
    inline int calc(int x) { /* 上文中的 c(x) */
        return x * (x - 1) / 2;
    }
    
    LL fp(LL a, LL b, LL c) { /* 快速幂 */
        if (!b) return 1ll;
        LL t = fp(a, b / 2, c);
        return t * t % c * ((b & 1) ? a : 1) % c;
    }
    LL C(int n, int m) { /* 求组合数 */
        return fact[n] * invf[n - m] % mod * invf[m] % mod;
    }
    
    signed main() {
        cin >> n;
    
        fact[0] = 1ll;
        for (int i = 1; i <= calc(n); i++)
            fact[i] = fact[i - 1] * i % mod;
        invf[calc(n)] = fp(fact[calc(n)], mod - 2, mod);
        for (int i = calc(n) - 1; ~i; i--)
            invf[i] = invf[i + 1] * (i + 1) % mod;
        /* 预处理组合数 */
        
        for (int i = 1; i <= n; i++)
            f[i][calc(i)] = 1ll;
        /* dp 初始化,此处 f 即为 H */
        
        for (int i = 1; i <= n; i++)
            for (int j = calc(i); ~j; --j)
                for (int k = 1; k + i <= n; k++)
                    f[i + k][j + calc(k)] = (f[i + k][j + calc(k)] - f[i][j] * C(i + k - 1, k) % mod + mod) % mod;
                    /*
                    dp 处理 H:先顺序枚举点数,倒叙枚举边数,然后枚举新块的大小并合并到这 i 个点中。
                    注意乘上 -1,记得取模。
                    C(i + k - 1, k) 指合并后的 (i + k - 1) 个点中选出 k 个为“新加的”。
                    */
    
        for (int i = 0; i <= calc(n); i++)
            for (int j = i; j <= calc(n); j++)
                g[i] = (g[i] + f[n][j] * C(j, i) % mod) % mod;
                /*
                统计答案:系数 C(j, i) 表示剩余的 j 条边中选出 i 条。
                记得取模。
                */
        for (int i = 0; i <= calc(n); i++)
            cout << g[i] << endl;
    
        return 0;
    }
    
  • 相关阅读:
    整理公共基础库子系统和系统属性组件
    鸿蒙轻内核M核源码分析系列六 任务及任务调度(1)任务栈
    HarmonyOS三方件开发指南(19)-BGABadgeView徽章组件
    鸿蒙的DFX子系统
    安卓to鸿蒙系列:ButterKnife(一)
    基于Neptune开发板的键盘蓝牙模块DIY指南
    徒手撸一个Spring Boot中的starter
    这三道最基础的java面试题,你真的答得上来吗?
    奇葩java迭代器面试题,还真有很多人踩坑
    15道类和对象面试题,快看看自己会几道
  • 原文地址:https://www.cnblogs.com/-Wallace-/p/13355012.html
Copyright © 2011-2022 走看看