「解题报告」XXI Open Cup, Grand Prix of Tokyo

发布时间 2023-05-24 12:20:25作者: APJifengc

猜猜为什么四五天没更博了?攒了个大的。

非常好 OpenCup,10 个 998244353,爱来自陶瓷❤

快写死我了,终于写完了。

十道题里只有三道题是自己做出来的。我好废物。

Codeforces Gym

官方题解

A. Ascending Matrix

开 幕 雷 击。

首先考虑没有限制怎么做。由于优秀的单调性,我们可以对于每一个数 \(v\) 考虑将 \([1, v]\)\([v + 1, n]\) 的数划分开,划分的形状一定是从 \((n, 0)\)\((0, m)\) 的一条路径。我们发现,每一种矩阵可以用这样的方式与 \(k-1\) 条从 \((n, 0)\)\((0, m)\) 的互不穿过的路径进行对应,那么我们相当于统计这样的路径的数量。容易想到 LGV 引理,所以考虑将路径向右下平移,将第 \(i\) 条路径平移至 \((n + i, i) \to (i, m + i)\),这样问题就转化成了互不相交的 \(k-1\) 条路径计数,直接 LGV 引理即可。

考虑 \(a_{r, c} = v\) 的限制。令点 \(p=(r+v-2, c+v-2)\),那么我们的限制相当于是第 \(v\) 条路径必须在点 \(p\) 下方,第 \(v-1\) 条路径必须在点 \(p\) 上方,且没有路径经过点 \(p\)。我们可以通过加入一个元 \(x\) 来刻画这个限制,对于在 \(p\) 上方的路径,我们给权值乘上一个 \(x\),这样我们的限制就变成了求路径权值和的 \(v-1\) 次系数。具体来讲,我们给 \(p\) 点所在的对角线上的每一个点乘上一个权值,\(p\) 上方的点乘 \(x\)\(p\)\(0\)\(p\) 下方的点乘 \(1\)。计算方案数时,我们枚举经过这条对角线上的哪个点,然后乘上相应的权值即可计算。

当然我们肯定不能直接算多项式行列式,我们可以带 \(k+1\) 个点值进去然后再把多项式插出来,复杂度 \(O(k^4)\)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 305, P = 998244353;
int n, m, k, r, c, v;
int fac[MAXN << 1], inv[MAXN << 1];
int qpow(int a, int b) {
    int ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * a % P;
        a = 1ll * a * a % P;
        b >>= 1;
    }
    return ans;
}
void init(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++)
        fac[i] = 1ll * fac[i - 1] * i % P;
    inv[n] = qpow(fac[n], P - 2);
    for (int i = n; i >= 1; i--)
        inv[i - 1] = 1ll * inv[i] * i % P;
    assert(inv[0] == 1);
}
int C(int n, int m) {
    if (n < 0 || m < 0 || n < m) return 0;
    return 1ll * fac[n] * inv[m] % P * inv[n - m] % P;
}
int calc(int x1, int y1, int x2, int y2) {
    return C(x1 - x2 + y2 - y1, x1 - x2);
}
int f[MAXN][MAXN], fx[MAXN][MAXN];
int a[MAXN][MAXN];
int y[MAXN];
int det() {
    int ans = 1;
    for (int i = 0; i < k; i++) {
        int p = -1;
        for (int j = i; j < k; j++) if (a[j][i]) {
            p = j;
            break;
        }
        if (p == -1) return 0;
        if (p != i) ans *= -1, swap(a[i], a[p]);
        for (int j = i + 1; j < k; j++) {
            int inv = 1ll * a[j][i] * qpow(a[i][i], P - 2) % P;
            for (int l = i; l < k; l++) {
                a[j][l] = (a[j][l] - 1ll * inv * a[i][l] % P + P) % P;
            }
        }
    }
    for (int i = 0; i < k; i++)
        ans = 1ll * ans * a[i][i] % P;
    return (ans + P) % P;
}
int p[MAXN], q[MAXN];
int main() {
    init(600);
    scanf("%d%d%d%d%d%d", &n, &m, &k, &r, &c, &v);
    r += v - 2, c += v - 2, k--;
    for (int i = 0; i < k; i++) {
        for (int j = 0; j < k; j++) {
            f[i][j] = calc(n + i, i, j, m + j);
            for (int d = 0; r - d >= 0 && c - d >= 0; d++) {
                int x = 1ll * calc(n + i, i, r - d, c - d) * calc(r - d, c - d, j, m + j) % P;
                f[i][j] = (f[i][j] - x + P) % P;
                if (d) fx[i][j] = (fx[i][j] + x) % P;
            }
        }
    }
    for (int i = 0; i <= k; i++) {
        memset(a, 0, sizeof a);
        for (int x = 0; x < k; x++) {
            for (int y = 0; y < k; y++) {
                a[x][y] = (f[x][y] + 1ll * fx[x][y] * i) % P;
            }
        }
        y[i] = det();
    }
    int ans = 0;
    for (int i = 0; i <= k; i++) {
        memset(p, 0, sizeof p);
        p[0] = y[i];
        for (int j = 0; j <= k; j++) if (j != i) {
            int inv = qpow(j - i, P - 2);
            memset(q, 0, sizeof q);
            for (int l = 0; l <= k; l++) {
                q[l] = (q[l] + 1ll * j * inv % P * p[l] % P + P) % P;
                if (l) q[l] = (q[l] - 1ll * inv * p[l - 1] % P + P) % P;
            }
            memcpy(p, q, sizeof q);
        }
        ans = (ans + p[v - 1]) % P;
    }
    printf("%d\n", ans);
    return 0;
}

B. Bit Operation

好像是签到题。

分析操作性质,发现假如有两个 \(0\),最后得到的一定是 \(0\),假如两个 \(1\),最后得到的一定是 \(1\),否则得到的是 \(0\) 或者 \(1\)。发现这其实等价于从两个数中选一个删除。

那么我们就可以枚举最后剩下的那个 \(1\),然后统计方案数。注意到只有两端的数有一种选择方案,其它数都有两种选择方案,所以答案形如 \(1 \times 3 \times 5 \times \cdots\),两边组合数合并起来即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005, P = 998244353;
int n, a[MAXN];
int fac[MAXN], inv[MAXN];
int qpow(int a, int b) {
    int ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * a % P;
        a = 1ll * a * a % P;
        b >>= 1;
    }
    return ans;
}
void init(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++)
        fac[i] = 1ll * fac[i - 1] * i % P;
    inv[n] = qpow(fac[n], P - 2);
    for (int i = n; i >= 1; i--)
        inv[i - 1] = 1ll * inv[i] * i % P;
    assert(inv[0] == 1);
}
int f[MAXN];
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
    }
    init(n);
    f[0] = 1;
    for (int i = 1; i <= n; i++) {
        f[i] = 1ll * f[i - 1] * (2 * i - 1) % P;
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) if (a[i] == 1) {
        ans = (ans + 1ll * fac[n - 1] * inv[i - 1] % P * inv[n - i] % P * f[i - 1] % P * f[n - i]) % P;
    }
    printf("%d\n", ans);
    return 0;
}

C. Count Min Ratio

solution version 2. 第一版因为停电被吃掉了。气死了。

首先考虑题目要求的式子其实就是:

\[\sum_{i=0}^b \sum_{j=0}^r \binom{i+j}{i} \binom{b-i+r-j}{b-i} \min\left(\left\lfloor\frac{j}{i}\right\rfloor, \left\lfloor\frac{r-j}{b-i}\right\rfloor\right) \]

考虑把后面那个东西拆了。底函数有经典的将值转为条件的方法,具体来讲:

\[\begin{aligned} &\min\left(\left\lfloor\frac{j}{i}\right\rfloor, \left\lfloor\frac{r-j}{b-i}\right\rfloor\right)\\ =&\sum_{x=1}^{\infty} \left[x \le \left\lfloor\frac{j}{i}\right\rfloor, \left\lfloor\frac{r-j}{b-i}\right\rfloor\right]\\ =&\sum_{x=1}^{\infty} \left[xi \le j \land x(b-i) \le r - j\right]\\ =&\sum_{x=1}^{\infty} \left[xi \le j \le r - xb + xi\right]\\ \end{aligned} \]

容易发现 \(x\) 有一个上界 \(\left\lfloor\dfrac{r}{b}\right\rfloor\),我们设它为 \(n\)。现在右面变成了一个关于 \(j\) 的条件,我们考虑调换求和顺序:

\[\begin{aligned} & \sum_{i=0}^b \sum_{j=0}^r \binom{i+j}{i} \binom{b-i+r-j}{b-i} \min\left(\left\lfloor\frac{j}{i}\right\rfloor, \left\lfloor\frac{r-j}{b-i}\right\rfloor\right)\\ =& \sum_{x=1}^n \sum_{i=0}^b \sum_{j = xi}^{r - xb + xi} \binom{i+j}{i} \binom{b-i+r-j}{b-i} \\ =& \sum_{x=1}^n \sum_{p = 0}^{r - xb}\sum_{i=0}^b \binom{i+j}{i} \binom{b-i+r-j}{b-i} & (j := xi + p)\\ \end{aligned} \]

可以使用广义二项级数化简,然后做完了。 代数推导太过于困难,考虑组合意义。

我们发现,这个东西本质上在枚举从 \((0, 0) \to (b, r)\) 的每条路径上的每一个点。而 \(j=xi+p\) 的条件相当于求所有路径中经过 \(y=xi+p\) 这条直线的点数的和。

我们先考虑一个问题:从 \((0, 0) \to (n, kn+p)\) 的路径上,不穿过 \(y=kn+p\) 这条直线,路径数有多少?我们记这个东西的答案为 \(f(n, k, p)\)。发现当 \(k=1\) 时这是经典的卡特兰数,所以我们用类似的方法去计数。考虑 \((0, 0) \to (n, kn + p)\)\((0, 0) \to (n - 1, kn + p + 1)\) 的两种路径,后者一定穿过直线。

先考虑第一种路径,枚举是否穿过直线,以及第一次穿过直线的位置,那么下一步一定是向上走,可以得到:

\[\binom{(k + 1) n + p}{n} = f(n, k, p) + \sum_{i=0}^{n - 1} f(i, k, p) \binom{n - i + (kn + p) - (ki - p) - 1}{n - i} \]

再考虑第二种路径,一定穿过直线,直接枚举穿过直线的位置,可得:

\[\begin{aligned} &\binom{(k + 1) n + p}{n - 1} = \sum_{i=0}^{n - 1} f(i, k, p) \binom{n - 1 - i + (kn + p + 1) - (ki - p) - 1}{n - 1 - i}\\ =&\binom{(k + 1) n + p}{n - 1} = \frac{1}{k} \sum_{i=0}^{n - 1} f(i, k, p) \binom{n - i + (kn + p) - (ki - p) - 1}{n - i} \end{aligned} \]

对比两个式子,容易得出:

\[f(n, k, p) = \binom{(k + 1) n + p}{n} - k \binom{(k + 1) n + p}{n - 1} \]

于是这个问题就解决了。

再次考虑原来的问题,设从 \((0, 0) \to (w, h)\) 的所有路径中经过 \(y = kx + p\) 这条直线的点的总数为 \(g(w, h, k, p)\),那么首先有一个显然的式子,就是枚举每个点,计算经过它的方案数,可以得到:

\[g(w, h, k, p) = \sum_{i = 0}^w \binom{i + ki+p}{i} \binom{w - i + h - ki - p}{w - i} \]

考虑想办法凑出 \(f(n, k, p)\),我们计算 \(g(w, h, k, p) - k \cdot g(w - 1, h + 1, k, p)\)

\[\begin{aligned} &g(w, h, k, p) - k \cdot g(w - 1, h + 1, k, p)\\ =&\sum_{i=0}^w\binom{(k+1)i + p}{i} \left(\binom{w - i + h - ki - p}{w - i} - k \binom{w - i + h - ki - p}{w - i - 1}\right)\\ =&\sum_{i=0}^w\binom{(k+1)i + p}{i} f(w - i, k, h - wi - p)\\ \end{aligned} \]

考虑这个东西的组合意义。我们相当于在枚举一个点,前面随意走,然后从这个点开始一直在 \(y=kx+p\) 直线的上方的方案数。假如说我们钦定在这个点往上走一步,那么就变成了枚举最后一次经过 \(y=kx+p\) 的点,然后之后一直不经过 \(y=kx+p\) 的方案数,这实际上统计了所有 \((0, 0) \to (w, h + 1)\) 的路径数。于是我们可以得到答案等于 \(\dbinom{w + h + 1}{w}\)

我们知道了 \(g(w, h, k, p) - k \cdot g(w - 1, h + 1, k, p) = \dbinom{w + h + 1}{w}\),暴力展开即可得到:

\[g(w, h, k, p) = \sum_{i=0}^w \binom{w + h + 1}{i} k^{w - i} \]

这个式子与 \(p\) 都无关。那么直接代回原来的式子就能化简了。

容易得到原式等于:

\[\sum_{i=0}^b \binom{r+b+1}{i} \sum_{x=1}^n (r-bx+1) x^{b-i} \]

我们只需要预处理出自然数幂和即可快速计算这个式子。注意到上标 \(n\) 不变,所以考虑用伯努利数计算。(直接写出来 EGF 也行,就是 \(\dfrac{e^{(n + 1)x} - 1}{e^x - 1} - 1\)

int main() {
    long long r; int b;
    scanf("%lld%d", &r, &b);
    init(b + 3);
    Polynomial F(b + 1), G(b + 1);
    int n = (r / b) % P;
    for (int i = 0; i <= b + 1; i++) {
        F[i] = inv[i + 1];
        G[i] = 1ll * qpow(n + 1, i + 1) * inv[i + 1] % P;
    }
    F = (G * F.inv(b + 2) - 1).set(b + 1);
    for (int i = 0; i <= b + 1; i++) {
        F[i] = 1ll * F[i] * fac[i] % P;
    }
    int ans = 0, tmp = 1;
    for (int i = 0; i <= b; i++) {
        ans = (ans + 1ll * tmp * inv[i] % P * (
                (r + 1) % P * F[b - i] % P - 1ll * b * F[b - i + 1] % P + P
            )) % P;
        tmp = 1ll * tmp * ((r + b + 1 - i) % P) % P;
    }
    printf("%d\n", ans);
    return 0;
}

D. Do Use FFT

观察式子,发现 \(i\)\(j\) 是完全独立的,所以考虑把 \(i, j\) 分离开。简单推式子:

\[\begin{aligned} f_k &= \sum_{i = 1}^n c_i \prod_{j=1}^k (a_i + b_j)\\ &= \sum_{i = 1}^n c_i \sum_{p = 0}^k a_i^{k - p} [x^p] \prod_{j=1}^k (1 + b_j x)\\ &= \sum_{p = 0}^k \sum_{i = 1}^n c_i a_i^{k - p} [x^p] \prod_{j=1}^k (1 + b_j x)\\ \end{aligned} \]

我们令 \(\displaystyle G(x) = \sum_{k \ge 0} \sum_{i = 1}^n c_i a_i^k x^k\),显然有 \(\displaystyle G(x) = \sum_{i = 1}^n \frac{c_i}{ 1 - a_i x}\),这部分可以分治乘法 \(O(n \log^2 n)\) 求出。

然后原问题就转化成了:

\[f_k = [x^k] G(x) \prod_{i = 1}^k (1 + b_i x) \]

首先有一个很暴力的做法:直接分块重构,设一个阈值 \(B\),当后面的次数大于 \(B\) 时直接与前面的 \(G(x)\) 乘起来,这样一定是一个 \(B\) 次多项式乘一个 \(n\) 次多项式,可以在 \(O(B)\) 复杂度内求单项,总复杂度 \(O(\frac{n}{B} n \log n + nB)\),令 \(B = \sqrt{n \log n}\) 可得 \(O(n \sqrt{n \log n})\) 的复杂度,由于时限 10s,完全可以通过。

int n, a[MAXN], b[MAXN], c[MAXN];
pair<Polynomial, Polynomial> solve(int l, int r) {
    if (l == r) {
        return { Polynomial{ c[l] }, Polynomial{ 1, P - a[l] } };
    }
    int mid = (l + r) >> 1;
    auto L = solve(l, mid), R = solve(mid + 1, r);
    return { L.first * R.second + L.second * R.first, L.second * R.second };
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)
        scanf("%d", &b[i]);
    for (int i = 1; i <= n; i++)
        scanf("%d", &c[i]);
    auto p = solve(1, n);
    auto g = (p.first * p.second.inv(n + 1)).set(n);
    auto tmp = Polynomial{1};
    const int B = 2200;
    for (int i = 1; i <= n; i++) {
        if (i % B == 0) {
            g = (g * tmp).set(n);
            tmp = Polynomial{1};
        }
        tmp.set(tmp.len + 1);
        for (int j = tmp.len; j >= 1; j--) {
            tmp[j] = (tmp[j] + 1ll * b[i] * tmp[j - 1]) % P;
        }
        int ans = 0;
        for (int j = 0; j <= tmp.len && j <= i; j++) {
            ans = (ans + 1ll * tmp[j] * g[i - j]) % P;
        }
        printf("%d\n", ans);
    }
    return 0;
}

略微精细点:发现求的次数与后面的式子的区间是连续的,尝试分治计算这个东西,考虑先计算出左区间的乘积,然后在把当前的乘积递归给右边计算。发现,对于右区间 \([mid + 1, r]\),第 \(mid + i\) 个位置的答案是左区间的 \(G(x) \prod (1+b_i x)\) 乘上一个 \(i\) 次多项式,那么前面那个多项式有用的次数为 \([mid, mid + i]\)。也就是说,左面的答案仅有 \([mid, r]\) 这些项是有用的,我们只保留这些项然后往下递归。不难发现对于每一个区间,保留的多项式长度都是区间的长度,所以复杂度为 \(T(n) = 2T\left(\dfrac{n}{2}\right) + O(n \log n)\),即 \(T(n) = O(n \log^2 n)\)大常数导致跑的和 \(O(n \sqrt{n \log n})\) 差不多。

常数至少能砍 \(\frac{1}{3}\),不过反正能过,懒得优化了。

int n, a[MAXN], b[MAXN], c[MAXN];
pair<Polynomial, Polynomial> solve(int l, int r) {
    if (l == r) {
        return { Polynomial{ c[l] }, Polynomial{ 1, P - a[l] } };
    }
    int mid = (l + r) >> 1;
    auto L = solve(l, mid), R = solve(mid + 1, r);
    return { L.first * R.second + L.second * R.first, L.second * R.second };
}
int ans[MAXN];
Polynomial solve2(Polynomial G, int l, int r) {
    if (l == r) {
        ans[l] = (1ll * G[0] * b[l] + G[1]) % P;
        return Polynomial{ 1, b[l] };
    }
    int mid = (l + r) >> 1;
    auto L = solve2(G, l, mid);
    return L * solve2((G * L).shift(mid - l + 1).set(r - mid), mid + 1, r);
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++)
        scanf("%d", &b[i]);
    for (int i = 1; i <= n; i++)
        scanf("%d", &c[i]);
    auto p = solve(1, n);
    auto g = (p.first * p.second.inv(n + 1)).set(n);
    solve2(g, 1, n);
    for (int i = 1; i <= n; i++) {
        printf("%d ", ans[i]);
    }
    printf("\n");
    return 0;
}

题解给出了一个多点求值做法,我们直接扔掉。

E. Edge Subsets

怎么感觉见过类似的套路,但是还是一点也不会,哈哈。

\(g = \gcd(a, b)\),那么我们可以按照点 \(\bmod g\) 的值分成若干组,答案等于它们的乘积,于是就转化成了 \(\gcd(a, b) = 1\) 的情况。

考虑按照 \(b\) 再将点进行排列,根据裴蜀定理,可以得知 \(ka \bmod b\) 互不相同。于是我们将这些点排列成一个方针,令 \(p\) 的坐标为 \((x, y)\),则满足 \(x = \left\lfloor\dfrac{p}{b}\right\rfloor, yA \equiv p \pmod b\)。容易发现,这样进行排列后,\(+b\) 的边就是向下连边,\(+a\) 的边就是向右 / 向右下 / 向下一行的第一个数连边。

然后大概会连成一个这样的图:

image

我们现在考虑统计匹配数。这种东西看起来就像是状压一类的东西。可以使用轮廓线 DP(插头 DP)统计答案,复杂度 \(O(n 2^b)\),复杂度过高。但是注意到这张图的总点数是 \(O(n)\) 的,于是可以类似很多状压的套路,每次将较小的那一边进行状压。但是如果转过来做,我们需要先枚举最后一列向第一列连的边的选择方案,然后再 DP,这样复杂度为 \(O(n 4^{\frac{n}{b}})\)。平衡一下,可以得到 \(O(n 2^{\sqrt{2n}})\),可以通过。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205, MAXM = 405, P = 998244353;
int n, m, a, b;
int x[MAXN], y[MAXN];
bool edge[MAXN][MAXN][3];
void add(int &a, int b) {
    a += b;
    if (a >= P) a -= P;
}
#define BIT(j) (1 << (j))
#define MASK(j) (S ^ BIT(j))
namespace Solution1 {
    int f[2][1 << 21];
    int solve(int n, int m) {
        int o = 0;
        int S = (1 << (m + 1)) - 1;
        memset(f[o], 0, sizeof f[o]);
        f[o][0] = 1;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
                for (int s = 0; s <= S; s++) if (f[o][s]) {
                    if (s >> (j + 1) & 1) {
                        add(f[o ^ 1][s & MASK(j + 1)], f[o][s]);
                    } else {
                        add(f[o ^ 1][s & MASK(j + 1)], f[o][s]);
                        if (edge[i][j][0]) {
                            if (!(s >> j & 1)) {
                                add(f[o ^ 1][s | BIT(j) & MASK(j + 1)], f[o][s]);
                            }
                        }
                        if (edge[i][j][1]) {
                            if (!(s >> (j + 2) & 1)) {
                                add(f[o ^ 1][s | BIT(j + 2)], f[o][s]);
                            }
                        }
                        if (edge[i][j][2]) {
                            if (j == m - 1) {
                                if (!(s & 1)) {
                                    add(f[o ^ 1][s | BIT(0)], f[o][s]);
                                }
                            } else {
                                add(f[o ^ 1][s | BIT(j + 1)], f[o][s]);
                            }
                        }
                    }
                }
                o ^= 1;
            }
            memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
            for (int s = 0; s <= S; s++) if (f[o][s]) {
                add(f[o ^ 1][(s << 1 & S)], f[o][s]);
            }
            o ^= 1;
        }
        int ans = 0;
        for (int s = 0; s <= S; s++) {
            add(ans, f[o][s]);
        }
        return ans;
    }
}
namespace Solution2 {
    int f[2][1 << 11];
    int solve(int n, int m) {
        int S = (1 << (n + 1)) - 1;
        int ans = 0;
        for (int t = 0; t < (1 << n); t++) {
            bool flag = true;
            for (int i = 0; i < n; i++) if (t >> i & 1) {
                if (!edge[i][m - 1][2]) {
                    flag = false;
                    break;
                }
            }
            if (!flag) continue;
            int o = 0;
            memset(f[o], 0, sizeof f[o]);
            f[o][t << 2] = 1;
            for (int i = 0; i < m - 1; i++) {
                for (int j = 0; j < n; j++) {
                    memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
                    for (int s = 0; s <= S; s++) if (f[o][s]) {
                        if (s >> (j + 1) & 1) {
                            add(f[o ^ 1][s & MASK(j + 1)], f[o][s]);
                        } else {
                            add(f[o ^ 1][s & MASK(j + 1)], f[o][s]);
                            if (edge[j][i][1]) {
                                if (!(s >> j & 1)) {
                                    add(f[o ^ 1][s | BIT(j) & MASK(j + 1)], f[o][s]);
                                }
                            }
                            if (edge[j][i][0]) {
                                if (!(s >> (j + 2) & 1)) {
                                    add(f[o ^ 1][s | BIT(j + 2)], f[o][s]);
                                }
                            }
                            if (edge[j][i][2]) {
                                add(f[o ^ 1][s | BIT(j + 1)], f[o][s]);
                            }
                        }
                    }
                    o ^= 1;
                }
                memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
                for (int s = 0; s <= S; s++) if (f[o][s]) {
                    add(f[o ^ 1][(s << 1 & S)], f[o][s]);
                }
                o ^= 1;
            }
            memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
            for (int s = 0; s <= S; s++) if (f[o][s]) {
                add(f[o ^ 1][s >> 1], f[o][s]);
            }
            o ^= 1;
            for (int j = 0; j < n; j++) {
                memset(f[o ^ 1], 0, sizeof f[o ^ 1]);
                for (int s = 0; s <= S; s++) if (f[o][s]) {
                    if (s >> j & 1) {
                        add(f[o ^ 1][s], f[o][s]);
                    } else {
                        add(f[o ^ 1][s], f[o][s]);
                        if (edge[j][m - 1][0]) {
                            if (!(s >> (j + 1) & 1)) {
                                add(f[o ^ 1][s | BIT(j) | BIT(j + 1)], f[o][s]);
                            }
                        }
                    }
                }
                o ^= 1;
            }
            for (int s = 0; s <= S; s++) if ((s & t) == 0) {
                add(ans, f[o][s]);
            }
        }
        return ans;
    }
}
vector<pair<int, int>> e[MAXN];
int mp[MAXN];
int solve(vector<pair<int, int>> e, int n) {
    memset(edge, 0, sizeof edge);
    for (auto p : e) {
        int u = p.first, v = p.second;
        if (x[u] == x[v]) edge[x[u]][y[u]][1] = 1;
        else if (y[u] == y[v]) edge[x[u]][y[u]][0] = 1;
        else edge[x[u]][y[u]][2] = 1;
    }
    if (b <= sqrt(2 * n)) return Solution1::solve((n + b - 1) / b, b);
    else return Solution2::solve((n + b - 1) / b, b);
}
int main() {
    scanf("%d%d%d%d", &n, &m, &a, &b);
    int g = __gcd(a, b);
    a /= g, b /= g;
    for (int i = 0; i < b; i++) {
        mp[i * a % b] = i;
    }
    for (int i = 0; i < n; i++) {
        x[i] = i / b, y[i] = mp[i % b];
    }
    for (int i = 1; i <= m; i++) {
        int u, v; scanf("%d%d", &u, &v);
        u--, v--;
        e[u % g].push_back({ u / g, v / g });
    }
    int ans = 1;
    for (int i = 0; i < g; i++) {
        ans = 1ll * ans * solve(e[i], (n + g - 1) / g) % P;
    }
    printf("%d\n", ans);
    return 0;
}

F. Find the LCA

首先考虑一个问题,我们尝试计算出对于某一个大小 \(k\),有多少树满足 \(\mathrm{lca}(k - 1, k) = 1\)。可以证明,当 \(n \ge 3\) 时,答案等于 \(\dfrac{(n-1)!}{2}\)。证明考虑建立每两棵树的双射关系,假设现在我们有 \(l = \mathrm{lca}(k - 1, k), l \ne 1\),那么可以考虑将 \(l\) 与除了 \(k\) 子树外的所有子树同时剥去,整个子树接在 \(1\) 上,这样一定能够构造出一棵 \(\mathrm{lca}(k - 1, k) = 1\) 的树。同理,对于任意一棵 \(\mathrm{lca}(k - 1, k) = 1\) 的树,我们可以找到 \(1\) 的子树中包含 \(k - 1\) 的那一个,将其插入至 \(1 \to k\) 的路径中,由于有堆的性质,插入的位置唯一。于是可以证明双射关系成立。

那么考虑如何计算答案。有三种情况:

  • \(\mathrm{lca}(n - 1, n) = n - 1\)
  • \(\mathrm{lca}(n - 1, n) = 1\)
  • \(\mathrm{lca}(n - 1, n) \in [2, n - 2]\)

三种情况答案分别是:

  • \((n-2)! a_{n - 1} a_n\)
  • \(\displaystyle \dfrac{(n-1)!}{2} \times \prod_{i=1}^n a_i\)
  • \(\displaystyle \sum_{i=3}^{n - 2} \dfrac{(i-1)!}{2} (n - i - 1)! \sum_{j = 2}^{n - 2} f_{i - 2, j}\)

其中 \(f_{i, j}\) 表示第 \(j\) 个节点所在子树大小为 \(i\) 的所有 \(\prod a_i\) 之和。

考虑怎么计算 \(f_{i, j}\),显然可以使用多项式刻画,发现:

\[f_{i, j} = [x^i] (j - 1) a_j x \prod_{k = j + 1}^{n - 2} (1 + a_k x) \]

那么我们只需要计算出 \(F = \sum_{j = 2}^{n - 2} (j - 1) a_j x \prod_{k = j + 1}^{n - 2} (1 + a_k x)\) 即可快速计算答案。这个东西相当于求每一个后缀积的和,显然可以分治乘法维护。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 400005, P = 998244353, G = 3, GI = 332748118;
const int INV2 = (P + 1) / 2;
int qpow(int a, int b) {
    int ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * a % P;
        a = 1ll * a * a % P;
        b >>= 1;
    }
    return ans;
}
struct Polynomial { ... };
int n;
int a[MAXN];
int fac[MAXN], inv[MAXN];
pair<Polynomial, Polynomial> solve(int l, int r) {
    if (l > r) return {{}, {}};
    if (l == r) {
        return { Polynomial{ 0, 1ll * (l - 1) * a[l] % P }, Polynomial{ 1, a[l] } };
    }
    int mid = (l + r) >> 1;
    auto L = solve(l, mid), R = solve(mid + 1, r);
    return { L.first * R.second + R.first, L.second * R.second };
}
void init(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++)
        fac[i] = 1ll * fac[i - 1] * i % P;
    inv[n] = qpow(fac[n], P - 2);
    for (int i = n; i >= 1; i--)
        inv[i - 1] = 1ll * inv[i] * i % P;
    assert(inv[0] == 1);
}
int f(int n) {
    return 1ll * fac[n - 1] * INV2 % P;
}
int main() {
    scanf("%d", &n);
    init(n);
    int prod = 1;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        if (i <= n - 2) prod = 1ll * prod * a[i] % P;
    }
    int ans = (fac[n - 2] + 1ll * f(n) * prod) % P;
    auto p = solve(2, n - 2).first;
    for (int i = 3; i < n; i++) {
        ans = (ans + 1ll * fac[n - i - 1] * f(i) % P * p[i - 2]) % P;
    }
    ans = 1ll * ans * a[n - 1] % P * a[n] % P;
    printf("%d\n", ans);
    return 0;
}

G. Games

首先题目给出的是 k - nim 游戏,结论为将所有数进行二进制表示,当每一位上 \(1\) 的个数模 \((k + 1)\) 等于 \(0\) 时先手必败。证明与 Nim 游戏类似。

这个题实际上是一个 \(6\) - nim,不难发现我们实际上要做的就是一个 \(7\) 进制异或 FWT。简单来讲,就是我们定义 \(k\) 维异或为每一位加起来模 \(k\),这东西是一个循环卷积,所以考虑直接用 DFT 转移。打个表发现模意义下的 \(7\) 次单位根存在,所以不需要写啥分圆多项式,直接跑 FWT 即可。

高维 FWT 详见 command_block's blog

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005, G = 14553391, P = 998244353;
int n;
long long k;
int qpow(int a, long long b) {
    int ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * a % P;
        a = 1ll * a * a % P;
        b >>= 1;
    }
    return ans;
}
const int GI = qpow(G, P - 2);
int a[MAXN];
int base[10];
int tmp[7];
void fwt(bool rev) {
    for (int mid = 1; mid < base[7]; mid *= 7) {
        for (int l = 0; l < base[7]; l += mid * 7) {
            for (int i = 0; i < mid; i++) {
                int step = 1;
                for (int k = 0; k < 7; k++, step = 1ll * step * (rev ? GI : G) % P) {
                    int w = 1;
                    tmp[k] = 0;
                    for (int q = 0; q < 7; q++, w = 1ll * w * step % P) {
                        tmp[k] = (tmp[k] + 1ll * a[l + i + q * mid] * w) % P;
                    }
                }
                for (int k = 0; k < 7; k++) {
                    a[l + i + k * mid] = tmp[k];
                }
            }
        }
    }
    if (rev) {
        int inv = qpow(base[7], P - 2);
        for (int i = 0; i < base[7]; i++) {
            a[i] = 1ll * a[i] * inv % P;
        }
    }
}
int main() {
    scanf("%d%lld", &n, &k);
    base[0] = 1;
    for (int i = 1; i <= 7; i++) {
        base[i] = 7 * base[i - 1];
    }
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        int y = 0;
        for (int j = 0; j < 7; j++) {
            y += (x >> j & 1) * base[j];
        }
        a[y]++;
    }
    fwt(false);
    for (int i = 0; i < base[7]; i++) {
        a[i] = qpow(a[i], k);
    }
    fwt(true);
    printf("%d\n", a[0]);
    return 0;
}

H. Harsh Comments

这个加权随机非常烦人啊!考虑把它去了。

有一个经典随机算法叫拒绝采样,我们可以用相同的方法放宽限制。考虑假如每个数被选后不删除,仍然可以继续选,如果选了之前已经选过的数就重新选,这样首先每个数被选择的概率就确定了,而且不难证明每一种选择顺序的概率是不变的。

然后就可以根据期望的线性性,我们考虑将答案转化为每一个 \(b_i\) 的选择顺序在所有 \(a_i\) 的选择顺序之后的概率之和,这些 \(b_i\) 是不需要被选的,所以用 \(n+m\) 减去这个概率之和就是答案。

看起来还是不好计算,考虑容斥,枚举一个集合一定在 \(b_i\) 之后选,其它的不变。设这个集合的概率和为 \(q\),选择 \(b_i\) 的概率为 \(p_i\),那么概率为 \(\sum_{i \ge 1} p_i (1-p_i-q)^{i - 1} = \dfrac{p_i}{p_i + q}\)。考虑到这里只关心选中的集合的大小,所以可以先用一个背包统计出每种集合和的容斥系数之和,然后直接计算答案即可。

这个做法是和 猎人杀 一模一样的,不过那个题需要写一个分治 NTT,不能直接背包。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005, MAXM = 10005, P = 998244353;
int qpow(int a, int b) {
    int ans = 1;
    while (b) {
        if (b & 1) ans = 1ll * ans * a % P;
        a = 1ll * a * a % P;
        b >>= 1;
    }
    return ans;
}
int n, m;
int a[MAXN], b[MAXN];
int f[MAXM];
int main() {
    scanf("%d%d", &n, &m);
    int sum = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        sum += a[i];
    }
    for (int i = 1; i <= m; i++) {
        scanf("%d", &b[i]);
        sum += b[i];
    }
    f[0] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 10000; j >= a[i]; j--) {
            f[j] = (f[j] - f[j - a[i]] + P) % P;
        }
    }
    int ans = 0;
    for (int i = 1; i <= m; i++) {
        for (int j = 0; j <= 10000; j++) {
            ans = (ans + 1ll * f[j] * b[i] % P * qpow(b[i] + j, P - 2)) % P;
        }
    }
    ans = (n + m - ans + P) % P;
    printf("%d\n", ans);
    return 0;
}

I. Inverse Problem

好像是签到题。

首先观察到一个性质,就是这个序列中第 \(i\) 个数一定出现在 \([1, n - m + i]\) 内,而如果序列中存在一个数比前面的数小,说明这个数一定是在 \(n - m + i\) 的位置,否则肯定优先选它。那么这个数以及后面的数的位置就全部固定了。

考虑前面的数,如果有一个数没有被固定,且没有出现在序列中,那么它肯定要在序列中比它大的数的前面。那么我们考虑从小到大依次填入这几个数,然后统计一下当前有多少位置可以放即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 250005, P = 998244353;
int n, m;
int a[MAXN];
int vis[MAXN];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d", &a[i]);
    }
    bool flag = false;
    int top;
    for (int i = 1; i <= m; i++) {
        if (a[i] < a[i - 1]) {
            flag = true;
        }
        if (flag)
            vis[a[i]] = 2;
        else
            vis[a[i]] = 1, top = a[i];
    }
    int ans = 1;
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (vis[i] == 0) 
            ans = 1ll * ans * (cnt + (i > top)) % P, cnt++;
        if (vis[i] == 1)
            cnt++;
    }
    printf("%d\n", ans);
    return 0;
}

J. Japanese Knowledge

首先有一个结论:设 \(f_k(a_1, a_2, \cdots, a_n)\)\(k\) 时的答案,\(g(a_1, a_2, \cdots, a_n)\) 为没有限制时的答案,那么可以得到 \(f_k(a_1, a_2, \cdots, a_n) = g(a_{k + 1} - 1, a_{k + 2} - 1, \cdots, a_n - 1)\)

证明考虑建立两者之间的双射:

  • 对于任意一种方案,将所有 \(x_i = a_i\) 的列删去,即可得到一个长度为 \(n - k\) 的序列,且满足每个 \(x_i < a_{i + k}\)
  • 对于一个满足每个 \(x_i < a_{i + k}\) 的长为 \(n - k\) 的序列,我们考虑从小往大填,每次将最小的一个数填入第一个 \(a_j > x_i\)\(j\) 位置。最后剩下的没有填的位置就都填 \(x_i = a_i\),即可得到一个长为 \(n\) 的序列,满足 \(x_i \le a_i\)

于是我们相当于要对每个后缀计算出答案。我们首先可以将序列转化成路径数,那么答案可以看做是从一个阶梯状格子的右边界任意一个位置进入,然后从某个下边界出去的路径数。这个可以使用多项式分治计算。

具体的,我们用 \(solve(l, r, F, d)\) 计算 \([l, r]\),底边为 \(d\),右边界进入的方案数为 \(F\) 时从下边界的每个点出去的方案数。我们从 \(mid\) 处将其分作三部分,首先计算右上角的部分,然后可以使用卷积计算到左下角的右边界的方案数,最后计算所有点到下边界的方案数即可。右下角的部分可以使用组合数直接计算,且式子可以卷积优化。所以直接分治做即可。

(图从别的博搬的,把下边界和右边界反过来了,意思差不多)

image

Polynomial solve(Polynomial F, int l, int r, int d) {
    if (l == r) {
        Polynomial way;
        for (int i = 0; i <= a[l] - d; i++) {
            way[0] = (way[0] + F[i]) % P;
        }
        return way;
    }
    int mid = (l + r) >> 1;
    int llen = mid - l + 1, rlen = r - mid;
    int x = a[mid] - d;
    if (x < 0) return solve(F, mid + 1, r, d).shift(-llen);
    auto R = solve(F.shift(x + 1), mid + 1, r, d + x + 1);
    Polynomial tmp(x);
    for (int i = 0; i <= x; i++)
        tmp[i] = C(rlen - 1 + x - i, rlen - 1);
    auto rlway = (F.set(x) * tmp).shift(x);
    tmp = Polynomial(x + rlen);
    for (int i = 0; i <= x + rlen; i++)
        tmp[i] = fac[x + rlen - i];
    auto R2 = R;
    for (int i = 0; i < rlen; i++)
        R2[i] = 1ll * R2[i] * inv[i] % P;
    auto ulway = (R2 * tmp).shift(rlen).set(x);
    for (int i = 0; i <= x; i++)
        ulway[i] = 1ll * ulway[i] * inv[x - i] % P;
    auto L = solve(rlway + ulway, l, mid, d);
    tmp = Polynomial(rlen);
    for (int i = 0; i <= rlen; i++)
        tmp[i] = C(x + rlen - i, x);
    auto udway = (R * tmp).shift(rlen);
    tmp = Polynomial(x + rlen);
    for (int i = 0; i <= x + rlen; i++)
        tmp[i] = fac[x + rlen - i];
    F.set(x);
    for (int i = 0; i <= x; i++)
        F[i] = 1ll * F[i] * inv[i] % P;
    auto rdway = (F * tmp).shift(x + 1).set(rlen - 1);
    for (int i = 0; i <= rlen - 1; i++)
        rdway[i] = 1ll * rdway[i] * inv[rlen - 1 - i] % P;
    return L + (udway + rdway).shift(-llen);
}
int main() {
    scanf("%d", &n);
    init(500000);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
    }
    Polynomial F(a[n] - 1);
    for (int i = 0; i < a[n]; i++)
        F[i] = 1;
    auto ans = solve(F, 1, n, 1);
    for (int i = 0; i < n; i++) {
        printf("%d ", ans[i]);
    }
    printf("1\n");
    return 0;
}