计数中的问题转化

计数中的问题转化

组合意义

P1758 [NOI2009] 管道取珠

给出两个栈,大小分别为 \(n, m\) ,栈内元素仅由 AB 组成。每次可以选择一个栈,将其顶部插入序列末尾并弹栈。

记最后得到的序列有 \(k\) 种,其中第 \(i\) 种序列可以由 \(a_i\) 种不同的操作序列得到。

显然有 \(\sum a_i = \binom{n + m}{m}\) ,求 \((\sum a_i^2) \bmod 1024523\)

\(n, m \le 500\)

考虑将 \(a_i^2\) 转化为组合意义:有两个人同时各自进行这个游戏,求两个人得到相同序列的方案数。

考虑 DP,设 \(f_{i, j, k}\) 表示序列长度为 \(i\) ,两个人分别在第一个栈中取了 \(j, k\) 个元素,并且两人所得序列相同的方案数,转移直接枚举两人的操作即可。

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

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1024523;
const int N = 5e2 + 7;

int f[N][N], g[N][N];
char a[N], b[N];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
    f[0][0] = 1;

    for (int i = 0; i < n + m; ++i) {
        memcpy(g, f, sizeof(f)), memset(f, 0, sizeof(f));

        for (int j = 0; j <= min(n, i); ++j)
            for (int k = 0; k <= min(n, i); ++k) {
                if (j < n && k < n && a[j + 1] == a[k + 1])
                    f[j + 1][k + 1] = add(f[j + 1][k + 1], g[j][k]);

                if (j < n && i - k < m && a[j + 1] == b[i - k + 1])
                    f[j + 1][k] = add(f[j + 1][k], g[j][k]);

                if (i - j < m && k < n && b[i - j + 1] == a[k + 1])
                    f[j][k + 1] = add(f[j][k + 1], g[j][k]);

                if (i - j < m && i - k < m && b[i - j + 1] == b[i - k + 1])
                    f[j][k] = add(f[j][k], g[j][k]);
            }
    }

    printf("%d", f[n][n]);
    return 0;
}

CF917D Stranger Trees

有一张 \(n\) 个点的完全图,给定该图的一个生成树。对于 \(i = 0, 1, \cdots, n - 1\) ,求有多少棵这个完全图的生成树,使得这些生成树与给定的生成树恰好有 \(i\) 条边重合。

\(n \le 100\)

恰好 \(i\) 条边是不好处理的,考虑二项式反演,则只需求出重合至少 \(i\) 条边的方案数。

至少 \(i\) 条边重合相当于给原树划分为 \(n - i\) 个连通块,连通块之间任意连边形成一棵树,任意连边的方案数即为 \(n^{k - 2} \prod_{i = 1}^k s_i\)

考虑组合意义,\(\prod_{i = 1}^k s_i\) 等价于给每个连通块内部任意定根的方案数,设 \(f_{u, i, 0/1}\) 表示 \(u\) 子树内划分了 \(i\) 个连通块,\(u\) 所在连通块是否定根的方案数,转移就是树形背包,不难做到 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e2 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int fac[N], inv[N], invfac[N], siz[N], f[N][N][2], g[N][2], ans[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int sgn(int n) {
    return n & 1 ? Mod - 1 : 1;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

void dfs(int u, int fa) {
    f[u][1][0] = f[u][1][1] = siz[u] = 1;

    for (int v : G.e[u]) {
        if (v == fa)
            continue;

        dfs(v, u);
        memset(g, 0, sizeof(g));

        for (int i = 1; i <= siz[u]; ++i)
            for (int j = 1; j <= siz[v]; ++j) {
                g[i + j][0] = add(g[i + j][0], 1ll * f[u][i][0] * f[v][j][1] % Mod);
                g[i + j][1] = add(g[i + j][1], 1ll * f[u][i][1] * f[v][j][1] % Mod);
                g[i + j - 1][0] = add(g[i + j - 1][0], 1ll * f[u][i][0] * f[v][j][0] % Mod);
                g[i + j - 1][1] = add(g[i + j - 1][1], 
                    add(1ll * f[u][i][0] * f[v][j][1] % Mod, 1ll * f[u][i][1] * f[v][j][0] % Mod));
            }

        memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs(1, 0);

    for (int i = 2, mul = 1; i <= n; ++i, mul = 1ll * mul * n % Mod)
        ans[n - i] = 1ll * f[1][i][1] * mul % Mod;

    ans[n - 1] = 1;
    prework(n);

    for (int i = 0; i < n; ++i)
        for (int j = i + 1; j < n; ++j)
            ans[i] = add(ans[i], 1ll * sgn(j - i) * C(j, i) % Mod * ans[j] % Mod);

    for (int i = 0; i < n; ++i)
        printf("%d ", ans[i]);

    return 0;
}

Chef Cuts Tree

给定一棵树,进行 \(n - 1\) 次操作,每次等概率断掉一条边,每次操作后求出所有连通块大小的平方和的期望,答案对 \(10^9 + 7\) 取模。

\(n \le 10^5\)

考虑组合意义,连通块大小的平方和即为所有连通点对的数目。对于一对距离为 \(d\) 的点对,\(i\) 轮后仍连通则说明中间的边都被没断掉,概率为:

\[\frac{(n - d - 1)^{\underline{i}}}{(n - 1)^{\underline{i}}} \]

\(cnt_i\) 表示距离为 \(i\) 的点对数量,第 \(i\) 天的答案为:

\[\frac{(n - i - 1)!}{(n - 1)!} \sum_{d = 0}^{n - 1} \frac{(n - d - 1)!}{(n - d - i - 1)!} \times cnt_d \]

发现后者是一个卷积的形式,但是给出的模数不是 NTT 模数,使用 MTT 即可。

下面考虑求 \(cnt\) ,考虑点分治,枚举分治重心,只要记录一下分治子树内到其距离为 \(i\) 的点对数量,对其做卷积,然后减去各个子树内部卷积的答案即可,时间复杂度 \(O(n \log^2 n)\)

#include <bits/stdc++.h>
typedef long double ld;
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int fac[N], inv[N], invfac[N];
int siz[N], mxsiz[N], f[N], g[N];
bool vis[N];

int n, root;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

namespace Poly {
const ld Pi = acos(-1);
const int N = 5e5 + 7, B = 1 << 15;

struct Complex {
    ld x, y;
    
    inline Complex(ld xx = 0, ld yy = 0) : x(xx), y(yy) {}
    
    inline Complex operator + (const Complex &b) const {
        return Complex(x + b.x, y + b.y);
    }
    
    inline Complex operator - (const Complex &b) const {
        return Complex(x - b.x, y - b.y);
    }
    
    inline Complex operator * (const Complex &b) const {
        return Complex(x * b.x - y * b.y, x * b.y + y * b.x);
    }
    
    inline Complex operator / (const Complex &b) const {
        ld t = b.x * b.x + b.y * b.y;
        return Complex((x * b.x + y * b.y) / t, (y * b.x - x * b.y) / t);
    }
} P[N], Q[N], R[N];

int rev[N];

inline int calc(int n) {
    int len = 1;

    while (len < n)
        len <<= 1;

    for (int i = 0; i < len; ++i)
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? (len >> 1) : 0);

    return len;
}

inline void FFT(Complex *f, int n, int op) {
    for (int i = 0; i < n; ++i)
        if (i < rev[i])
            swap(f[i], f[rev[i]]);

    for (int k = 1; k < n; k <<= 1) {
        Complex tG(cos(Pi / k), sin(Pi / k) * op);

        for (int i = 0; i < n; i += k << 1) {
            Complex buf = 1;

            for (int j = 0; j < k; ++j) {
                Complex fl = f[i + j], fr = buf * f[i + j + k];
                f[i + j] = fl + fr, f[i + j + k] = fl - fr;
                buf = buf * tG;
            }
        }
    }

    if (op == -1) {
        for (int i = 0; i < n; ++i)
            f[i] = f[i] / n;
    }
}

inline void Mul(int *f, int n, int p, int *res) {
    int len = calc(n * 2 - 1);

    for (int i = 0; i < n; ++i)
        P[i] = f[i];

    for (int i = n; i < len; ++i)
        P[i] = 0;

    FFT(P, len, 1);

    for (int i = 0; i < len; ++i)
        P[i] = P[i] * P[i];

    FFT(P, len, -1);

    for (int i = 0; i < n * 2 - 1; ++i)
        res[i] = (ll)round(P[i].x) % p;
}

inline void MTT(int *f, int n, int *g, int m, int p, int *res) {
    int len = calc(n + m - 1);

    for (int i = 0; i < len; ++i)
        P[i] = Q[i] = R[i] = 0;

    for (int i = 0; i < n; ++i)
        P[i] = Complex(f[i] / B, f[i] % B), R[i] = Complex(f[i] / B, -(f[i] % B));

    for (int i = 0; i < m; ++i)
        Q[i] = Complex(g[i] / B, g[i] % B);

    FFT(P, len, 1), FFT(Q, len, 1), FFT(R, len, 1);

    for (int i = 0; i < len; ++i)
        P[i] = P[i] * Q[i], R[i] = R[i] * Q[i];

    FFT(P, len, -1), FFT(R, len, -1);

    for (int i = 0; i < n + m - 1; ++i) {
        ll a1a2 = (ll)round((P[i].x + R[i].x) / 2) % p, b1b2 = (ll)round((R[i].x - P[i].x) / 2) % p,
            a1b2 = (ll)round((P[i].y + R[i].y) / 2) % p, a2b1 = (ll)round((P[i].y - R[i].y) / 2) % p;
        res[i] = (((a1a2 * B + a1b2 + a2b1) * B + b1b2) % p + p) % p;
    }
}
} // namespace Poly

int getsiz(int u, int f) {
    siz[u] = 1;

    for (int v : G.e[u])
        if (!vis[v] && v != f)
            siz[u] += getsiz(v, u);

    return siz[u];
}

void getroot(int u, int f, int Siz) {
    siz[u] = 1, mxsiz[u] = 0;
 
    for (int v : G.e[u])
        if (!vis[v] && v != f)
            getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
 
    mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
 
    if (!root || mxsiz[u] < mxsiz[root])
        root = u;
}

int dfs(int u, int f, int d) {
    ++g[d];
    int mxd = d;

    for (int v : G.e[u])
        if (!vis[v] && v != f)
            mxd = max(mxd, dfs(v, u, d + 1));

    return mxd;
}

void solve(int u) {
    vis[u] = true;
    int m = dfs(u, 0, 0);
    Poly::Mul(g, m + 1, Mod, g);

    for (int i = 0; i <= m * 2; ++i)
        f[i] = add(f[i], g[i]), g[i] = 0;

    for (int v : G.e[u])
        if (!vis[v]) {
            int m = dfs(v, u, 1);
            Poly::Mul(g, m + 1, Mod, g);

            for (int i = 0; i <= m * 2; ++i)
                f[i] = dec(f[i], g[i]), g[i] = 0;
        }

    for (int v : G.e[u])
        if (!vis[v])
            root = 0, getroot(v, u, getsiz(v, u)), solve(root);
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    getroot(1, 0, n), solve(root);
    prework(n);

    for (int i = 0; i < n; ++i)
        f[i] = 1ll * f[i] * fac[n - i - 1] % Mod;

    Poly::MTT(f, n, invfac, n, Mod, f);

    for (int i = 0; i < n; ++i)
        printf("%d ", 1ll * invfac[n - 1] * fac[n - i - 1] % Mod * f[n - i - 1] % Mod);

    return 0;
}

[ARC124E] Pass to Next

\(n\) 个站成一圈,初始第 \(i\) 个人手中有 \(a_i\) 个球。

此时所有人同时进行一次传球操作,其中一次传球操作定义为选择若干数量(不超过原有手上球的数量)的球给右边的人。

记传球后第 \(i\) 个人手中有 \(b_i\) 个球,\(B\) 为所有 \(b_{1 \sim n}\) 构成的集合,求:

\[\left( \sum_{b_{1 \sim n} \in B} \prod_{i = 1}^n b_i \right) \bmod 998244353 \]

\(n \le 10^5\)

先将每个人手里球的数量的乘积转化为组合意义,即每个人从自己的球中选一个的方案数,则每个人选的球可以拆分为原来的球和传过来的球。

先考虑链的情况,设:

  • \(f_i\) 表示第 \(i\) 个人选原来的球,考虑前 \(i - 1\) 个人选球的方案数。
  • \(g_i\) 表示第 \(i\) 个人选传来的球,考虑前 \(i\) 个人选球的方案数。
  • \(s_k(n) = \sum_{i = 1}^n i^k\)

考虑转移,具体分四类:

  • \(f_i \times s_1(a_i) \to f_{i + 1}\)
  • \(g_i \times (a_i + 1) \to f_{i + 1}\)
  • \(f_i \times (\sum_x (a_i - x)x) = f_i \times (a_i s_1(a_i) - s_2(a_i)) \to g_{i + 1}\)
  • \(g_i \times s_1(a_i) \to g_{i + 1}\)

接下来考虑去重。设 \(x_i\) 表示第 \(i\) 个人传出去的球数,那么如果所有 \(x_i\) 都为正,那么将所有 \(x_i \gets x_i - 1\) 得到的局面相同。先算出没有限制的答案,然后减去每个位置都递送球的答案即可。对于前两种转移中令 \(a_i \gets a_i - 1\) (不能全选不然就传不了球),后两种转移不用改变(因为没传球时没有转移)。

接下来考虑环上怎么处理。先让 \(f_1 = 1\) ,跑一遍后 \(f_1\) 就是第一个人选原有球的答案。再让 \(g_1 = 1\) ,跑一遍后 \(g_1\) 就是第一个人选传来球的答案。

两种情况要分开讨论避免互相转移(第一个人又选原有球又选递送球),时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = 499122177, inv6 = 166374059;
const int N = 1e5 + 7;

int a[N], f[N], g[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int S1(int n) {
    return 1ll * n * (n + 1) % Mod * inv2 % Mod;
}

inline int S2(int n) {
    return 1ll * n * (n + 1) % Mod * (n * 2 + 1) % Mod * inv6 % Mod;
}

inline int solve(int k, int d) {
    f[1] = k, g[1] = k ^ 1;

    for (int i = 1; i <= n; ++i) {
        f[i % n + 1] = add(1ll * f[i] * S1(a[i] - d) % Mod, 
            1ll * g[i] * (a[i] - d + 1) % Mod);
        g[i % n + 1] = add(1ll * f[i] * dec(1ll * a[i] * S1(a[i]) % Mod, S2(a[i])) % Mod, 
            1ll * g[i] * S1(a[i]) % Mod);
    }

    return k ? f[1] : g[1];
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    printf("%d", dec(add(solve(1, 0), solve(0, 0)), add(solve(1, 1), solve(0, 1))));
    return 0;
}

CF1842G Tenzing and Random Operations

给出 \(a_{1 \sim n}\) 和常数 \(v\)\(m\) 次操作,每次等概率选择一个后缀加 \(v\) ,求最终 \(\prod_{i = 1}^n a_i\) 的期望。

\(n \le 5000\)

先将期望转计数,然后将乘积用乘法分配律拆开为 \(\prod_{i = 1}^n (a_i + v + v + \cdots)\) ,则可以分类讨论每一项的贡献:

  • 选择 \(a_i\) :系数为 \(a_i\)
  • 选择前面选择过的 \(j\) 个操作之一:系数为 \(j \times v\)
  • 选择前面没有选择过的操作:系数为 \((m - j) \times i \times v\) ,其中 \(m - j\) 表示从剩余操作中选取,\(i\) 表示可以操作的后缀数量。

好处是此时至多钦定 \(n\) 次操作,剩下的操作可以任意决策。即系数等用到时再算,没用到的部分不做区分。

\(f_{i, j}\) 表示 \(1 \sim i\) 钦定 \(j\) 次操作的答案,则:

\[f_{i, j} \times (a_{i + 1} + jv) \to f_{i + 1, j} \\ f_{i, j} \times (m - j) (i + 1)v \to f_{i + 1, j + 1} \]

答案即为:

\[\frac{1}{n^m} \sum_{j = 0}^{\min(n, m)} f_{n, j} \times n^{m - j} \]

类似的题:[ABC231G] Balls in Boxes

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e3 + 7;

int a[N], f[N][N];

int n, m, v;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

signed main() {
    scanf("%d%d%d", &n, &m, &v);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);
    
    f[0][0] = 1;
    
    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= min(m, i); ++j) {
            f[i + 1][j] = add(f[i + 1][j], 1ll * f[i][j] * add(a[i + 1], 1ll * j * v % Mod) % Mod);
            f[i + 1][j + 1] = add(f[i + 1][j + 1], 1ll * f[i][j] * (m - j) % Mod * (i + 1) % Mod * v % Mod);
        }
    
    int ans = 0;
    
    for (int i = 0; i <= min(n, m); ++i)
        ans = add(ans, 1ll * f[n][i] * mi(n, m - i) % Mod);
    
    printf("%d", 1ll * ans * mi(n, Mod - 1 - m) % Mod);
    return 0;
}

初始有一个长度为 \(n\) 的全 \(0\) 序列 \(c_{1 \sim n}\)\(k\) 次操作,每次给 \(a_i\) 个位置加一,求所有方案 \(\prod_{i = 1}^n c_i\) 的和。

\(n \le 1000\)\(k \le 20\)

考虑组合意义,\(\prod_{i = 1}^n c_i\) 转化为从 \(c_i\) 个操作中选一个,设 \(x_i\) 表示第 \(i\) 个操作被 \(x_i\) 个位置选,则答案为:

\[\sum_{\sum x_i = n} \frac{n!}{\prod_{i = 1}^k x_i!} \prod_{i = 1}^k \binom{n - x_i}{a_i - x_i} \]

\(f_{i, j}\) 表示考虑前 \(i\) 个操作,\(\sum x = j\) 的贡献和,不难做到 \(O(n^2 k)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e3 + 7, K = 2e1 + 7;

int f[K][N], fac[N], inv[N], invfac[N];

int n, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d%d", &n, &k);
    prework(n), f[0][0] = 1;

    for (int i = 1; i <= k; ++i) {
        int x;
        scanf("%d", &x);

        for (int j = 0; j <= n; ++j)
            for (int l = 0; l <= min(x, n - j); ++l)
                f[i][j + l] = add(f[i][j + l], 1ll * f[i - 1][j] * invfac[l] % Mod * C(n - l, x - l) % Mod);
    }

    printf("%d", 1ll * fac[n] * f[k][n] % Mod);
    return 0;
}

CF1278F Cards

\(m\) 张牌,其中有一张王牌。

\(n\) 次操作,每次随机打乱后查看第一张牌是否为王牌。

\(x\) 表示第一张牌为王牌的次数,求 \(x^k\) 的期望模 \(998244353\) 的值,其中 \(k\) 为给定的值。

\(n, m < 998244353\)\(k \le 5000\)

考虑所求值转化为长度为 \(k\) 、每个位置的值为 \(x\) 个数之一的序列数量,其中 \(x\) 个数分别为抽到王牌的轮次,则一个序列可能对应的 \(x\) 只与其不同元素数量有关。

\(f_{i, j}\) 表示考虑到第 \(i\) 个位置,选了 \(j\) 个不同数的序列的贡献,则:

\[f_{i, j} = f_{i - 1, j} \times j + f_{i - 1, j - 1} \times (n - (j - 1)) \times \frac{1}{m} \]

时间复杂度 \(O(k^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

int f[N][N];

int n, m, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

signed main() {
    scanf("%d%d%d", &n, &m, &k);
    int invm = mi(m, Mod - 2);
    f[0][0] = 1;

    for (int i = 1; i <= k; ++i)
        for (int j = 1; j <= i; ++j)
            f[i][j] = add(1ll * f[i - 1][j - 1] * (n - (j - 1)) % Mod * invm % Mod, 1ll * f[i - 1][j] * j % Mod);

    printf("%d", accumulate(f[k] + 1, f[k] + k + 1, 0ll) % Mod);
    return 0;
}

[AGC005D] ~K Perm Counting

给定 \(n, k\) ,求有多少个长度为 \(n\) 的排列,满足 \(\forall 1 \le i \le n, |p_i - i| \ne k\) ,答案对 \(924844033\) 取模。

\(n \times 2 \times 10^3\)

考虑容斥,钦定恰好 \(i\) 个位置满足 \(|p_i - i| \ne k\) ,剩下的位置随便排,方案数为 \((n - i)!\)

\((i, p_i)\) 对应到 \(n \times n\) 的网格图上,则 \((i, i + k)\)\((i, i - k)\) 分别构成两条斜率为 \(1\) 的直线。

此时需要在这些位置上放 \(i\) 个互不攻击的车,考虑将互相攻击的位置连起来,则整个图形如若干条链,具体的有 \(2 (n \bmod k)\) 条长度为 \(\lfloor \frac{n}{k} \rfloor\) 的链和 \(2 (k - n \bmod k)\) 条长度为 \(\lfloor \frac{n}{k} \rfloor - 1\) 的链。答案即为大小为 \(i\) 的独立集数量,不难直接 DP 求解。

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

#include <bits/stdc++.h>
using namespace std;
const int Mod = 924844033;
const int N = 2e3 + 7;

int fac[N], f[N << 1][N], a[N << 1];

int n, k, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int sgn(int n) {
    return n & 1 ? Mod - 1 : 1;
}

inline void prework() {
    fac[0] = 1;

    for (int i = 1; i <= n; ++i)
        fac[i] = 1ll * fac[i - 1] * i % Mod;
}

signed main() {
    scanf("%d%d", &n, &k);
    prework();
    a[m = 0] = 1;

    for (int i = 1; i <= n % k; ++i)
        a[m += n / k] = 1, a[m += n / k] = 1;

    for (int i = 1; i <= k - n % k; ++i)
        a[m += n / k - 1] = 1, a[m += n / k - 1] = 1;

    f[0][0] = 1;

    for (int i = 1; i <= m; ++i) {
        f[i][0] = 1;

        for (int j = 1; j <= n; ++j)
            f[i][j] = add(f[i - 1][j], f[i - 1 - !a[i - 1]][j - 1]);
    }

    int ans = 0;

    for (int i = 0; i <= n; ++i)
        ans = add(ans, 1ll * sgn(i) * f[m][i] % Mod * fac[n - i] % Mod);

    printf("%d", ans);
    return 0;
}

[ABC134F] Permutation Oddness

给定 \(n, k\) ,求有多少个长度为 \(n\) 的排列满足 \(\sum_{i = 1}^n |p_i - i| = k\) ,答案对 \(10^9 + 7\) 取模。

\(n \le 50\)

由于绝对值的贡献与相对大小有关,考虑按 \(1 \sim n\) 的顺序填。

考虑排列的组合意义:将 \(n\) 个元素和 \(n\) 个位置匹配。

考虑 DP,需要同时记录有多少个元素和位置与后面匹配,不难发现二者数量相等。

\(f_{i, j, k}\) 表示考虑到 \(i\) 号元素和 \(i\) 号位置,有 \(j\) 个元素/位置和 \(> i\) 的匹配,目前 \(\sum |p_i - i| = k\) 的方案数,则:

  • \(i\) 号元素与 \(i\) 号位置匹配:\(f_{i - 1, j, k} \to f_{i, j, k}\)
  • 两者的匹配对象均在 \(i\) 之后:\(f_{i - 1, j, k} \to f_{i, j + 1, k - 2i}\)
  • 两者的匹配对象均在 \(i\) 之前:\(j^2 \times f_{i - 1, j, k} \to f_{i, j - 1, k + 2i}\)
  • 两者的匹配对象一个在 \(i\) 之前、一个在 \(i\) 之后:\(2j \times f_{i - 1, j, k} \to f_{i, j, k}\)

时间复杂度 \(O(n^4)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e1 + 7;

int f[N][N][N * N << 1];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%d", &n, &m);
    f[0][0][n * n] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= i; ++j)
            for (int k = -n * n; k <= n * n; ++k)
                if (f[i - 1][j][k + n * n]) {
                    f[i][j][k + n * n] = add(f[i][j][k + n * n], 1ll * (j * 2 + 1) * f[i - 1][j][k + n * n] % Mod);
                    f[i][j + 1][k - i * 2 + n * n] = add(f[i][j + 1][k - i * 2 + n * n], f[i - 1][j][k + n * n]);
                    f[i][j - 1][k + i * 2 + n * n] = add(f[i][j - 1][k + i * 2 + n * n], 
                        1ll * j * j * f[i - 1][j][k + n * n] % Mod);
                }

    printf("%d", f[n][0][m + n * n]);
    return 0;
}

CF715E Complete the Permutations

给定两个长度为 \(n\) 的排列 \(p, q\) ,但其中有一些位置未知,记为 \(0\)

定义两个排列 \(p, q\) 之间的距离为:最小的在 \(p\) 上交换数字的次数,满足交换后 \(p, q\) 相等。

会与 \(0 \le i \le n - 1\) ,求两个排列距离为 \(i\) 的方案数 \(\bmod 998244353\)

\(n \le 250\)

先考虑 \(p, q\) 确定时计算距离,将 \(p_i \to q_i\) 连成若干个置换环,则距离记为 \(n\) 减去置换环数量。

证明:由于交换两个元素至多只会从置换环上剥离出一个点,因此距离下界为 \(n\) 减去置换环数量,上界不难直接构造即可取得。

考虑按边端点是否确定分四类讨论:

  • \(p \to q\) ,记为一类边,数量为 \(n_1\)
  • \(p \to 0\) ,记为二类边,数量为 \(n_2\)
  • \(0 \to q\) ,记为三类边,数量为 \(n_3\)
  • \(0 \to 0\) ,记为四类边,数量为 \(n_4\)

注意若存在 \(x \to 0 \to x\) ,则可以直接将其合并为四类边。

下面考虑连接这些边成环的限制:

  • 一类边:前面必须是二、四类边,后面必须是三、四类边。
  • 二类边:前面必须是二、四类边。
  • 三类边:后面必须是三、四类边。
  • 四类边:无限制。

接下来考虑方案数的求解。

  • 对于一类边,可以直接把它缩成一个点,记录一下是否形成环即可。

  • 对于二类边,考虑钦定一些边组成环,剩下的边接到四类边后面。

    \(f_i\) 表示二类边形成 \(i\) 个环的方案数,则:

    \[\]

    \[\]

  • 对于三类边,设 \(g_i\) 表示三类边形成 \(i\) 个环的方案数,则:

    \[\]

    \[\]

  • 对于四类边,设 \(h_i\) 表示四类边形成 \(i\) 个环的方案数,则:

    \[\]

    \[ \]

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2.5e2 + 7;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline bool merge(int x, int y) {
        x = find(x), y = find(y);

        if (x == y)
            return false;

        return fa[y] = x, true;
    }
} dsu;

int a[N], b[N], c[N], fac[N], inv[N], invfac[N], S[N][N], f[N], g[N], h[N], res[N], ans[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }

    S[0][0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= i; ++j)
            S[i][j] = add(S[i - 1][j - 1], 1ll * (i - 1) * S[i - 1][j] % Mod);
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed 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);

    dsu.prework(n);
    int cnt = 0;

    for (int i = 1; i <= n; ++i)
        if (a[i] && b[i] && !dsu.merge(a[i], b[i]))
            ++cnt;

    int n2 = 0, n3 = 0, n4 = 0;

    for (int i = 1; i <= n; ++i) {
        if (a[i])
            a[i] = dsu.find(a[i]);

        if (b[i])
            b[i] = dsu.find(b[i]);

        if (a[i] && b[i])
            continue;
        else if (a[i] && !b[i])
            ++c[a[i]], ++n2;
        else if (!a[i] && b[i])
            ++c[b[i]], ++n3;
        else
            ++n4;
    }

    int k = count(c + 1, c + n + 1, 2);
    n2 -= k, n3 -= k, n4 += k;
    prework(n);

    if (n4) {
        for (int i = 0; i <= n2; ++i)
            for (int j = i; j <= n2; ++j)
                f[i] = add(f[i], 1ll * C(n2, j) * S[j][i] % Mod * fac[n2 - j + n4 - 1] % Mod * invfac[n4 - 1] % Mod);

        for (int i = 0; i <= n3; ++i)
            for (int j = i; j <= n3; ++j)
                g[i] = add(g[i], 1ll * C(n3, j) * S[j][i] % Mod * fac[n3 - j + n4 - 1] % Mod * invfac[n4 - 1] % Mod);
    } else {
        memcpy(f, S[n2], sizeof(int) * (n2 + 1));
        memcpy(g, S[n3], sizeof(int) * (n3 + 1));
    }

    for (int i = 0; i <= n4; ++i)
        h[i] = 1ll * S[n4][i] * fac[n4] % Mod;

    for (int i = 0; i <= n2; ++i)
        for (int j = 0; j <= n3; ++j)
            res[i + j] = add(res[i + j], 1ll * f[i] * g[j] % Mod);

    for (int i = 0; i <= n2 + n3; ++i)
        for (int j = 0; j <= n4; ++j)
            ans[i + j] = add(ans[i + j], 1ll * res[i] * h[j] % Mod);

    for (int i = 0; i < n; ++i)
        printf("%d ", n - i < cnt ? 0 : ans[n - i - cnt]);

    return 0;
}

[ARC128F] Game against Robot

给定 \(n\) 个数 \(val_{1 \sim n}\) ,求所有 \(1 \sim n\) 的排列的权值和。对于排列 \(p_{1 \sim n}\) ,其权值定义如下:

  • A、B两人轮流玩游戏,A 先手。
  • A 每次可以取走任意一个数,B 每次必须取走 \(p\) 最大的位置上的数。
  • 都取完时游戏结束,权值定义为 A 取走数字和的最大值。

\(n \le 10^6\)\(n\) 为偶数

先考虑求排列的权值,先将所有数按 \(p\) 排序,然后令 \(n \gets \frac{1}{2} n\)

对于每个位置,记 \(c_i = 1\) 表示 A 选这个位置,\(c_i = -1\) 表示 B 选这个位置,则合法的 \(\pm 1\) 序列应当满足:

\[\forall i, \sum_{j = 1}^i c_i \le 1 \]

对于 A 选择位置集合 \(a_1 < a_2 < \cdots < a_n\) ,对于所有 \(i \in [1, n]\) ,均满足 \(i - (a_i - i) \le 1\) ,即 \(a_i \ge 2i - 1\) 。因此有一个贪心,从后往前确定 \(a_n, a_{n - 1}, \cdots, a_1\) ,每次往堆中扔两个数,每次取出最大值即可。

由此贪心不难发现选取数的方案只与 \(val\) 的相对大小有关,考虑固定一个数 \(x\) ,将 \(\ge x\) 的视作 \(1\)\(< x\) 的视作 \(0\) ,统计 \(\ge x\) 的数在所有排列的被选次数和。

设有 \(m\)\(1\)\(2n - m\)\(0\) ,则需要统计所有 01 序列中选出数字和的和 \(F_m\) ,最后钦定顺序乘上 \(m! (2n - m)!\) 即可。

为便于分析,将序列翻转变成从前往后选,每两个数绑一组。记 \(s\) 表示当前堆中 \(1\) 的数量,\(x \in \{ 0, 1, 2 \}\) 表示当前组中 \(1\) 的数量,则每次令 \(s \gets \max(s + x - 1, 0)\)

\(d_i \in \{ -1, 0, 1 \}\) 表示每一组 \(1\) 的数量减去 \(1\) 的值,考虑数最后 \(s\) 的值,则每次相当于令 \(s \gets \max(0, s + d_i)\) ,放到网格图上就相当于每次令 \(x \to x + 1, y \to \max(0, y' \in \{ y - 1, y, y + 1 \})\)

发现这个 \(\max\) 不好处理,考虑直接不要这个 \(\max\) ,则最后会走到 \((n, m - n)\) ,记全局 \(y\) 坐标最小值为 \(k\) ,则原来走到的点为 \((n, m - n - k)\) ,对应 A 选的数字和为 \(n + k\)

考虑统计全局最小值为 \(k\) 的 01 序列数量,对于相邻的两个数:

  • 若为两个 \(0\) ,则 \(y \to y - 1\) ,有一种方案。
  • 若为两个 \(1\) ,则 \(y \to y + 1\) ,有一种方案。
  • 否则 \(y \to y\) ,有两种方案。

先不考虑最低点的限制,考虑走到 \((N, M)\) 的方案数,即:

\[\begin{aligned} F(N, M) &= [x^M](x^{-1} + 2 + x)^N \\ &= [x^M] x^{-N} (1 + 2x + x^2)^N \\ &= [x^{N + M}] (x + 1)^{2N} \\ &= \binom{2N}{N + M} \end{aligned} \]

根据格路计数的翻转理论,最低点 \(\ge k\) 方案数即为:

\[\binom{2n}{n + m - n} - \binom{2n}{n + 2(k - 1) - (m - n)} = \binom{2n}{m} - \binom{2n}{m - 2k + 2} \]

因此最低点恰好为 \(k\) 的方案数即为:

\[\left( \binom{2n}{m} - \binom{2n}{m - 2k + 2} \right) - \left( \binom{2n}{m} - \binom{2n}{m - 2k} \right ) = \binom{2n}{m - 2k} - \binom{2n}{m - 2k + 2} \]

\(p = \min(m - n, 0)\) ,则 \(k \in [-n, p]\) (可能取不到 \(-n\) ,此时方案数为 \(0\) ),则:

\[\begin{aligned} F_m &= \sum_{k = -n}^p (n + k) \left( \binom{2n}{m - 2k} - \binom{2n}{m - 2k + 2} \right) \\ &= \sum_{k = -p}^n (n - k) \left( \binom{2n}{m + 2k} - \binom{2n}{m + 2(k + 1)} \right) \\ &= n \sum_{k = -p}^n \left( \binom{2n}{m + 2k} - \binom{2n}{m + 2(k + 1)} \right) - \sum_{k = -p}^n k \binom{2n}{m + 2k} + \sum_{k = -p}^n k \binom{2n}{m + 2(k + 1)} \\ &= n \binom{2n}{m - 2p} - \sum_{k = -p}^n k \binom{2n}{m + 2k} + \sum_{k = -p + 1}^{n + 1} (k - 1) \binom{2n}{m + 2k} \\ &= n \binom{2n}{m - 2p} + p \binom{2n}{m - 2p} - \sum_{k = p + 1}^n \binom{2n}{m + 2k} \end{aligned} \]

前缀和处理后一项,时间复杂度 \(O(n \log n)\) ,瓶颈在排序。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e6 + 7;

int a[N], fac[N], inv[N], invfac[N], f[N], g[N << 1];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    sort(a + 1, a + n + 1, greater<int>()), prework(n);
    g[0] = C(n, 0), g[1] = C(n, 1);

    for (int i = 2; i <= n * 2; ++i)
        g[i] = add(g[i - 2], C(n, i));

    for (int m = 0; m <= n; ++m) {
        int p = max(n / 2 - m, 0);
        f[m] = 1ll * dec(dec(1ll * (n / 2) * C(n, m + p * 2) % Mod, 1ll * p * C(n, m + p * 2) % Mod),
            dec(g[m + n], g[m + p * 2])) * fac[m] % Mod * fac[n - m] % Mod;
    }

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        ans = add(ans, 1ll * a[i] * dec(f[i], f[i - 1]) % Mod);

    printf("%d", ans);
    return 0;
}

P8367 [LNOI2022] 盒

\(n\) 个位置,第 \(i\) 个位置有 \(a_i\) 个球。将一个球在 \(i, i + 1\) 之间移动的代价为 \(w_i\) ,求所有最终状态的最小代价和。

\(n \le 5 \times 10^5\)\(S = \sum_{i = 1}^n a_i \le 2 \times 10^6\)

对于固定的最终状态,显然可以 \(O(n)\) 遍历求出最小代价。考虑每个 \(i, i + 1\) 被跨过的次数,记 \(s\) 为前缀和,答案即为:

\[\begin{aligned} & \sum_{i = 1}^{n - 1} w_i \sum_{j = 0}^S |j - s_i| \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \\ =& \sum_{i = 1}^{n - 1} w_i \left( \left( \sum_{j = 0}^S (j - s_i) \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \right) + 2 \left( \sum_{j = 0}^{s_i} (s_i - j) \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \right) \right) \end{aligned} \]

设:

\[f(i, m) = \sum_{j = 0}^m \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \\ g(i, m) = \sum_{j = 0}^m j \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \]

则答案即为:

\[\sum_{i = 1}^{n - 1} w_i ((g(i, S) - s_i \times f(i, S)) + 2(s_i \times f(i, s_i) - g(i, s_i))) \]

先考虑求 \(g\)

\[\begin{aligned} g(i, m) &= \sum_{j = 0}^m j \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \\ &= \left( \sum_{j = 0}^m (i + j) \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \right) - \left( \sum_{j = 0}^m i \binom{j + i - 1}{i - 1} \binom{n - i - 1 + S - j}{n - i - 1} \right) \\ &= \left( \sum_{j = 0}^m i \binom{j + i}{i} \binom{n - i - 1 + S - j}{n - i - 1} \right) - i \times f(i, m) \end{aligned} \]

发现前一项也与 \(f\) 类似,下面考虑求 \(f\)

考虑 \(f\) 的组合意义,即从 \((0, 0)\) 走到 \((n, S)\) 、每步只能向右或向上、\(i - 1 \to i\) 时纵坐标 \(\le m\) 的方案数。由于 \(n, S\) 都不大,考虑递推:

  • \(f(i, m) \to f(i, m + 1)\) :直接加上经过 \((i - 1, m + 1) \to (i, m + 1)\) 的方案数即可。
  • \(f(i, m) \to f(i + 1, m)\) :发现后者合法的路径集合一定被前者包含,而二者相差的部分恰好都经过了 \((i, m) \to (i, m + 1)\) ,直接减去即可。

时间复杂度 \(O(n + S)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e6 + 7;

int fac[N], inv[N], invfac[N], s[N], w[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

struct Solver {
    int n, m, p, q, ans;

    inline Solver(int _n, int _m) : n(_n), m(_m), p(0), q(0), ans(C(n + m - 1, n - 1)) {}

    inline int query(int x, int y) {
        while (p < x) {
            ++p;

            if (q != m)
                ans = dec(ans, 1ll * C(p + q, p) * C(n - p + m - q - 1, n - p) % Mod);
        }

        while (q < y) {
            ++q;
            ans = add(ans, 1ll * C(p + q, p) * C(n - p - 1 + m - q, n - p - 1) % Mod);
        }

        return ans;
    }
};

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);

        for (int i = 1; i <= n; ++i)
            scanf("%d", s + i), s[i] += s[i - 1];

        for (int i = 1; i < n; ++i)
            scanf("%d", w + i);

        int ans = 0, res1 = C(s[n] + n - 1, n - 1), res2 = C(s[n] + n, n);
        Solver A(n - 1, s[n]), B(n, s[n]);

        for (int i = 1; i < n; ++i)
            ans = add(ans, 1ll * w[i] * add(dec(1ll * dec(res2, res1) * i % Mod, 1ll * s[i] * res1 % Mod),
                dec(2ll * s[i] * A.query(i - 1, s[i]) % Mod, 
                    2ll * dec(B.query(i, s[i]), A.query(i - 1, s[i])) * i % Mod)) % Mod);
        
        printf("%d\n", ans);
    }

    return 0;
}

考虑限制

[ARC101F] Robots and Exits

数轴上有 \(n\) 个机器人和 \(m\) 个出口,位置均为正整数坐标,且这 \(n + m\) 个坐标互异。

现在执行若干次操作,每次可以将所有机器人同时向左或向右移动。若一个机器人移动到了出口,则其会消失,直至所有机器人都消失时停止操作。

求机器人消失的方案数,两种方案不同当且仅当存在一个机器人在两个方案中不从同一出口消失。

\(n, m \le 10^5\)

显然机器人一定从左/右最近的出口出去,记 \(l_i, r_i\) 表示第 \(i\) 个机器人左右离它最近的出口的距离。

\(0\)\(1\) 表示机器人从左/右的出口出去,则问题转化为对 01 序列 \(a_{1 \sim n}\) 计数。

考虑挖掘 01 序列合法的充要条件。根据左右距离关系可以推出性质:若 \(l_i \le l_j \and r_j \le r_i\) ,则 \(a_i = 1 \to a_j = 1\)\(a_j = 0 \to a_i = 0\)

可以发现满足这些限制的 01 序列都是合法的,且所有 01 序列均满足这些限制,因此可以用这些限制记数 01 序列。

考虑将问题转化为初始有一个全 \(0\) 序列,求有多少种放 \(1\) 的方案数。将不互相限制的点拿出来计数,这样就能不重不漏。

将所有点按 \(l\) 为第一关键字升序、\(r\) 为第二关键字降序排序之后去重,不互相限制的点集排序后显然满足 \(r\) 递增,问题转化为有多少 \(r\) 递增的子序列。不难树状数组做到 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7;

struct Node {
    int l, r;

    inline bool operator == (const Node &rhs) const {
        return l == rhs.l && r == rhs.r;
    }

    inline bool operator < (const Node &rhs) const {
        return l == rhs.l ? r > rhs.r : l < rhs.l;
    }
} nd[N];

int a[N], b[N], f[N];

int n, m, tot;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

namespace BIT {
int c[N];

int n;

inline void update(int x, int k) {
    for (; x <= n; x += x & -x)
        c[x] = add(c[x], k);
}

inline int query(int x) {
    int res = 0;

    for (; x; x -= x & -x)
        res = add(res, c[x]);

    return res;
}
} // namespace BIT

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= m; ++i)
        scanf("%d", b + i);

    vector<int> vec;

    for (int i = 1; i <= n; ++i) {
        if (a[i] < b[1] || a[i] > b[m])
            continue;

        int j = upper_bound(b + 1, b + m + 1, a[i]) - b;
        nd[++tot] = (Node){b[j] - a[i], a[i] - b[j - 1]};
        vec.emplace_back(a[i] - b[j - 1]);
    }

    sort(nd + 1, nd + tot + 1), tot = unique(nd + 1, nd + tot + 1) - nd - 1;
    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
    BIT::n = vec.size();
    int ans = 1;

    for (int i = 1; i <= tot; ++i) {
        int x = lower_bound(vec.begin(), vec.end(), nd[i].r) - vec.begin() + 1;
        BIT::update(x, f[i] = add(BIT::query(x - 1), 1)), ans = add(ans, f[i]);
    }

    printf("%d", ans);
    return 0;
}

[AGC009C] Division into Two

给定 \(n\) 个数,需要将其划分为两个集合,满足两个集合内元素的最小差值分别 \(\ge A\)\(\ge B\) ,求方案数。

\(n \le 10^5\)

排序后设 \(f_i\) 表示最后一个 \(A\) 集合选的是 \(a_i\) 时的方案数,转移时枚举上一个选取的 \(j\) ,限制即为:

  • \(a_i - a_j \ge A\)
  • \(\forall k \in [j + 1, i - 2], a_{k + 1} - a_k \ge B\)
  • \(j\) 后第一个不选的和 \(j\) 前最后一个不选的数的差 \(\ge B\)

发现第三个限制很难处理,考虑钦定 \(A \ge B\) ,则此时有解的必要条件是 \(\forall i, a_{i + 1} - a_{i - 1} \ge B\) ,因此只要一开始判定后就无需考虑第三个性质了,直接双指针前缀和优化即可做到线性。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7;

ll a[N];
int s[N];

ll A, B;
int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    scanf("%d%lld%lld", &n, &A, &B);

    if (A < B)
        swap(A, B);

    for (int i = 1; i <= n; ++i)
        scanf("%lld", a + i);

    for (int i = 2; i < n; ++i)
        if (a[i + 1] - a[i - 1] < B)
            return puts("0"), 0;

    s[0] = 1;

    for (int i = 1, j = 0, k = 0; i <= n; ++i) {
        while (k + 1 < i && a[i] - a[k + 1] >= A)
            ++k;

        s[i] = add(s[i - 1], j <= k ? dec(s[k], j ? s[j - 1] : 0) : 0);

        if (i > 1 && a[i] - a[i - 1] < B)
            j = i - 1;
    }

    int p = n - 1;

    while (p && a[p + 1] - a[p] >= B)
        --p;

    printf("%d", dec(s[n], p ? s[p - 1] : 0));
    return 0;
}

CF1158F Density of subarrays

定义序列的密度为最大的 \(p\) 满足所有 \(c^p\) 个长度为 \(p\) 、元素 \(\in [1, c]\) 的序列均为其子序列。

给定 \(n, c\) 与长度为 \(n\) 、元素 \(\in [1, c]\) 的序列,对于 \(p = 0, 1, \cdots n\) ,求密度为 \(p\) 的非空子序列数量 \(\bmod 998244353\)

\(n, c \le 3000\)

先不管非空的限制,最后减去即可。考虑如何计算序列的密度,考虑判定密度是否 \(\ge x\) 。记 \(p_{1 \sim c}\) 表示 \(1 \sim c\) 第一次出现的位置,则 \(\max_{i = 1}^c p_i + 1\) 及之后的位置密度 \(\ge x - 1\) ,因此可以规约子问题。

规约子问题的形式有利于 DP,设 \(f_{i, j}\) 表示以 \(i\) 开头、密度 \(\ge j\) 的子数列数量,转移就枚举 \([i, k]\) 包含 \(1 \sim c\) ,为了避免算重需要强制 \(k\) 为对应颜色的唯一位置,设 \(cnt_i\) 表示 \(i\) 的出现次数,则:

\[g_{l, r} = [a_l \ne a_r] \times 2^{cnt_{a_l} - 1} \prod_{i \ne a_l \and i \ne a_r} (2^{cnt_i} - 1) \]

\(s_{i, j} = \sum_{k \ge i} f_{k, j}\) ,则:

\[f_{i, j} = \sum_k g_{i, k} \times s_{k + 1, j - 1} \]

注意到第二维为 \(\le \frac{n}{c}\) ,因此时间复杂度为 \(O(\frac{n^3}{c})\) 。发现 \(c\) 在分母的部分,这启发我们寻找一个 \(c\) 较小时优秀的算法。

考虑依次考虑每个元素选不选取,设 \(f_{i, j, s}\) 表示考虑前 \(i\) 个元素,组成子序列密度为 \(j\) 、数字出现集合为 \(s\) 的方案数,转移时若 \(s = \{1, 2, \cdots, c \}\) 则令 \(j \to j + 1, s \to \empty\) ,时间复杂度 \(O(\frac{n^2}{c} 2^c)\)

平衡复杂度,取 \(c\) 的阈值为 \(\log n\) 即可做到 \(O(\frac{n^3}{\log n})\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e3 + 7;

int a[N];

int n, c;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

namespace Method1 {
int f[N][N], g[N][N], s[N][N], pw[N], ipw[N];

inline void solve() {
    pw[0] = 0;

    for (int i = 1; i <= n; ++i)
        ipw[i] = mi(pw[i] = add(2ll * pw[i - 1] % Mod, 1), Mod - 2);

    for (int i = 1; i <= n; ++i) {
        vector<int> cnt(c + 1);
        int none = c, res = 0;

        for (int j = i; j <= n; ++j) {
            none -= !cnt[a[j]], ++cnt[a[j]];

            if (!none) {
                if (cnt[a[j]] == 1) {
                    res = add(pw[cnt[a[i]] - 1], 1);

                    for (int k = 1; k <= c; ++k)
                        if (k != a[i])
                            res = 1ll * res * pw[cnt[k]] % Mod;
                } else {
                    if (a[j] == a[i])
                        res = 2ll * res % Mod;
                    else
                        res = 1ll * res * ipw[cnt[a[j]] - 1] % Mod * pw[cnt[a[j]]] % Mod;
                }

                if (a[j] != a[i])
                    g[i][j] = 1ll * res * ipw[cnt[a[j]]] % Mod;
            }
        }
    }

    s[n + 1][0] = 1;

    for (int i = n; i; --i) {
        f[i][0] = add(pw[n - i], 1);

        for (int j = 1; j <= (n - i + 1) / c; ++j)
            for (int k = i + c - 1; k <= n && s[k + 1][j - 1]; ++k)
                f[i][j] = add(f[i][j], 1ll * g[i][k] * s[k + 1][j - 1] % Mod);

        for (int j = 0; j <= (n - i + 1) / c; ++j)
            s[i][j] = add(s[i + 1][j], f[i][j]);
    }

    s[1][0] = dec(s[1][0], 1);

    for (int i = 0; i <= n; ++i)
        printf("%d ", dec(s[1][i], s[1][i + 1]));
}
} // namespace Method1

namespace Method2 {
int f[2][N][N];

inline void solve() {
    f[0][0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= i / c; ++j)
            memset(f[i & 1][j], 0, sizeof(int) << c);

        for (int j = 0; j <= i / c; ++j)
            for (int s = 0; s < (1 << c); ++s) {
                if (!f[~i & 1][j][s])
                    continue;

                f[i & 1][j][s] = add(f[i & 1][j][s], f[~i & 1][j][s]);
                int t = s | (1 << (a[i] - 1));

                if (t == (1 << c) - 1)
                    f[i & 1][j + 1][0] = add(f[i & 1][j + 1][0], f[~i & 1][j][s]);
                else
                    f[i & 1][j][t] = add(f[i & 1][j][t], f[~i & 1][j][s]);
            }
    }

    for (int i = 0; i <= n; ++i) {
        int ans = (i ? 0 : Mod - 1);

        for (int s = 0; s < (1 << c); ++s)
            ans = add(ans, f[n & 1][i][s]);

        printf("%d ", ans);
    }
}
} // namespace Method2

signed main() {
    scanf("%d%d", &n, &c);

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    if (c > max(__lg(n), 1))
        Method1::solve();
    else
        Method2::solve();

    return 0;
}

QOJ9839. Selection Sort Count

给定选择排序每轮的交换次数 \(c_{1 \sim n - 1}\) ,求有多少种排列符合给出的交换次数。

其中选择排序定义为:枚举 \(i = 1, 2, \cdots, n - 1\) ,再枚举 \(j = i + 1, i + 2, \cdots n\) ,若 \(p_i > p_j\) ,则交换 \(p_i, p_j\) ,并令 \(c_i \gets c_i + 1\)

\(n \le 2 \times 10^5\)

注意到,如果⼀个位置上的数字在第 \(i\) 轮被交换过,且它不是 \(i\) ,那么它在第 \(i + 1\) 轮⼀定也会被交换。

证明:首先可以发现在一轮交换中⼀个位置参与交换的充要条件是,在此轮交换开始时,这个位置上的数是前缀 \(\min\)

在第 \(i\) 轮交换中,一共有 \(c_i + 1\) 个位置参与了交换,升序记为 \(p_{0 \sim c_i}\) ,交换前这些位置上的值为 \(a_{0 \sim c_i}\) 。模拟可以发现 \(a_{c_i}\) (全局最小值 \(i\))被换到了最前面,其他位置都向后移动。

由于移动前 \(a_{i + 1}\)\(a_i\) 后的下一个前缀 \(\min\) ,因此 \((p_i, p_{i + 1})\) 之间的数均 \(> a_i\) ,因此 \(a_i\) 移动后仍为前缀最小值。

接下来考虑倒推,假设目前已知第 \(i + 1\) 轮参与交换的位置,则第 \(i\) 轮交换的位置就是位置 \(i\)\(c_i\) 个在第 \(i + 1\) 轮参与交换的位置。

考虑从 \(c_{i + 1} + 1\) 个位置中任意选出 \(c_i\) 个位置,根据交换过程,可以发现它们有唯一的合法方案:第一个位置上的数移动到 \(i\) ,后面每个位置上的数都移动到前一个位置,\(i\) 放到最后一个位置。

\(f_i\) 表示第 \(i\) 轮的合法方案数,则 \(f_i = f_{i + 1} \times \binom{c_{i + 1} + 1}{c_i}\) ,时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7;

int c[N], fac[N], inv[N], invfac[N];

int n;

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i)
        scanf("%d", c + i);

    prework(n);
    int ans = 1;

    for (int i = n - 1; i; --i)
        ans = 1ll * ans * C(c[i + 1] + 1, c[i]) % Mod;

    printf("%d", ans);
    return 0;
}

CF1517F Reunion

给定一棵 \(n\) 个点的树,每个点分别有 \(\frac{1}{2}\) 的概率为黑色(或白色)。

一个二元组 \((x, r)\) 合法当且仅当与 \(x\) 距离不超过 \(r\) 的点均为白色,定义一棵树的价值为:

  • 若全为黑点,则价值为 \(-1\)
  • 若全为白点,则价值为 \(n\)
  • 否则价值为合法二元组中 \(r\) 的最大值。

求树的价值的期望。

\(n \le 300\)

首先转化为求价值和,最后除以 \(2^n\) 即可。直接对所有数统计价值统计显然没有前途,考虑统计价值为 \(r\) 的树的数量,前缀和转化为统计存在半径 \(\ge r\) 的白点圆的树的数量。

但是存在性的限制很容易算重,考虑将其转化为全局性的限制:所有黑点周围 \(\le r\) 的点的并集不为全集。

这个染色问题可以考虑 DP,需要记录黑点往上覆盖的最远距离、子树内最深的没有被覆盖的点,不难发现这两个信息只有一个有效。

\(f_{u, i}\) 表示 \(u\) 子树内:

  • \(i \ge 0\) :子树内黑点往上覆盖的最远距离为 \(i\)
  • \(i < 0\) :则表示子树内最深没覆盖点到 \(u\) 距离为 \(-i - 1\)

枚举 \(r = 1, 2, \cdots n\) ,直接转移即可,贡献即为 \(\sum_{i = 0}^{n - 1} f_{1, -1 - i}\) ,注意需要额外统计全为黑点的贡献。

由于第二维只有 \(O(siz_u)\) 个位置有值,因此时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353, inv2 = (Mod + 1) / 2;
const int N = 3e2 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int f[N][N << 1];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void dfs(int u, int fa, int r) {
    f[u][r + n] = f[u][-1 + n] = 1;

    for (int v : G.e[u]) {
        if (v == fa)
            continue;

        dfs(v, u, r);
        vector<pair<int, int> > hu, hv;

        for (int i = 0; i <= n * 2; ++i) {
            if (f[u][i])
                hu.emplace_back(i - n, f[u][i]), f[u][i] = 0;

            if (f[v][i])
                hv.emplace_back(i - n, f[v][i]);
        }

        for (auto itu : hu)
            for (auto itv : hv) {
                int i = itu.first, j = itv.first, res = 1ll * itu.second * itv.second % Mod;

                if (i >= 0 && j >= 0)
                    f[u][max(i, j - 1) + n] = add(f[u][max(i, j - 1) + n], res);
                else if (i >= 0 && j < 0)
                    f[u][(i + j >= 0 ? i : j - 1) + n] = add(f[u][(i + j >= 0 ? i : j - 1) + n], res);
                else if (i < 0 && j >= 0)
                    f[u][(i + j >= 0 ? j - 1 : i) + n] = add(f[u][(i + j >= 0 ? j - 1 : i) + n], res);
                else
                    f[u][min(i, j - 1) + n] = add(f[u][min(i, j - 1) + n], res);
            }
    }
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    int ans = Mod - 1;

    for (int i = 1; i <= n; ++i) {
        memset(f, 0, sizeof(f)), dfs(1, 0, i);

        for (int i = 0; i < n; ++i)
            ans = add(ans, f[1][i]);
    }

    for (int i = 1; i <= n; ++i)
        ans = 1ll * ans * inv2 % Mod;

    printf("%d", ans);
    return 0;
}

LOJ2731. 「JOISC 2016 Day 1」棋盘游戏

有一个 \(3\times n\) 的棋盘,开始时棋盘有一些格子上已经摆上了棋子(记为 o ),剩下的格子都是空的(记为 x )。每次可以选择一个空的格子摆上棋子,这个格子必须满足以下两个条件之一:

  • 这个格子上下两格都有棋子。
  • 这个格子左右两格都有棋子。

求有多少种不同的摆满棋盘的摆放顺序。

\(n \le 2000\)

先考虑无解情况:四个角有 x ,或第一行有两个相邻的 x ,或第三行有两个相邻的 x

若一个格子能填,则要么上下比它早填,要么左右比它早填。考虑给每个格子一个数,表示填入棋子的时间,x 的是 \(0\)o 的是 \([1, m]\) ,其中 \(m\) 为空格个数。

然后考虑 DP 求每个 x 的连通块的答案,显然它们的顺序互不干扰,最后用组合数合并即可。

\(f_{i, j, 0/1}\) 表示在 \((2, i)\)\(j\)\((2, i)\) 是否比 \((2, i + 1)\) 先填(填的数更小),把这个连通块到第 \(i\) 列的所有 \(x\) 填一个排列,都填合法的方案数。不难发现只有 \(f_{i, j, 1}\) 对答案有贡献。由题可得每个 x 要么比左右晚要么比上下晚。

根据 \(f_{i, j}\)\(f_{i - 1, j}\) 的第三维可得 \(i - 1, i, i + 1\) 的时序关系,具体转移:

  • 对于该列三个 x 的情况:
    • 左在中前,中在右前,上下在中前:\(f_{i, j, 1} \gets (j - 1) \times (j - 2) \times \sum_{k = 1}^{j - 3} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下在中前:\(f_{i, j, 0} \gets (j - 1) \times (j - 2) \times \sum_{k = 1}^{j - 3} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下一个在中后、一个在中前:\(f_{i, j, 0} \gets 2 \times (j - 1) \times (siz_i - j) \times \sum_{k = 1}^{j - 2} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下在中后:\(f_{i, j, 0} \gets (siz_i - j) \times (siz_i - j - 1) \times \sum_{k = 1}^{j - 1} f_{i - 1, k, 1}\)
    • 左在中后,中在右前,上下在中前:\(f_{i, j, 1} \gets (j - 1) \times (j - 2) \times \sum_{k = j - 2}^{siz_{i - 1}} f_{i - 1, k, 0}\)
    • 左在中后,中在右后,上下在中前:\(f_{i, j, 0} \gets (j - 1) \times (j - 2) \times \sum_{k = j - 2}^{siz_{i - 1}} f_{i - 1, k, 0}\)
  • 对于该列两个 x 的情况:
    • 左在中前,中在右前,上下在中前:\(f_{i, j, 1} \gets (j - 1) \times \sum_{k = 1}^{j - 2} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下在中前: \(f_{i, j, 0} \gets (j - 1) \times \sum_{k = 1}^{j - 2} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下一个在中后、一个在中前:\(f_{i, j, 0} \gets (siz_i - j) \times \sum_{k = 1}^{j - 1} f_{i - 1, k, 1}\)
    • 左在中后,中在右前,上下在中前:\(f_{i, j, 1} \gets (j - 1) \times \sum_{k = j - 1}^{siz_{i - 1}} f_{i - 1, k, 0}\)
    • 左在中后,中在右后,上下在中前:\(f_{i, j, 0} \gets (j - 1) \times \sum_{k = j - 1}^{siz_{i - 1}} f_{i - 1, k, 0}\)
  • 对于该列一个 x 的情况:
    • 左在中前,中在右前,上下在中前:\(f_{i, j, 1} \gets \sum_{k = 1}^{j - 2} f_{i - 1, k, 1}\)
    • 左在中前,中在右后,上下在中前: \(f_{i, j, 0} \gets \sum_{k = 1}^{j - 2} f_{i - 1, k, 1}\)
    • 左在中后,中在右前,上下在中前:\(f_{i, j, 1} \gets \sum_{k = j}^{siz_{i - 1}} f_{i - 1, k, 0}\)
    • 左在中后,中在右后,上下在中前:\(f_{i, j, 0} \gets \sum_{k = j}^{siz_{i - 1}} f_{i - 1, k, 0}\)

前缀和优化即可做到 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e3 + 7;

int f[N][N << 1][2], s[N][N << 1][2];
int fac[N * 3], inv[N * 3], invfac[N * 3], siz[N];
char str[5][N];

int n, m;

inline int add(int x, int y) {
    x += y;

    if (x >= Mod)
        x -= Mod;

    return x;
}

inline int dec(int x, int y) {
    x -= y;

    if (x < 0)
        x += Mod;

    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;

    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline bool check() {
    for (int i = 1; i < n; ++i) {
        if (str[1][i] == 'x' && str[1][i + 1] == 'x')
            return false;
        else if (str[3][i] == 'x' && str[3][i + 1] == 'x')
            return false;
    }

    return str[1][1] != 'x' && str[1][n] != 'x' && str[3][1] != 'x' && str[3][n] != 'x';
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= 3; ++i)
        scanf("%s", str[i] + 1), m += count(str[i] + 1, str[i] + n + 1, 'x');

    prework(n * 3);

    if (!check())
        return puts("0"), 0;

    int ans = 1;

    for (int i = 1; i <= n; ++i) {
        if (str[2][i] == 'o')
            continue;

        int cnt = (str[1][i] == 'x') + 1 + (str[3][i] == 'x');
        siz[i] = siz[i - 1] + cnt;

        if (i == 1 || str[2][i - 1] == 'o') {
            f[i - 1][0][1] = 1;

            for (int j = 0; j <= m; ++j)
                s[i - 1][j][1] = 1;
        }

        if (cnt == 3) {
            for (int j = 1; j <= siz[i]; ++j) {
                if (j >= 3) {
                    f[i][j][0] = add(f[i][j][0], 1ll * (j - 1) * (j - 2) % Mod * s[i - 1][j - 3][1] % Mod);
                    f[i][j][1] = add(f[i][j][1], 1ll * (j - 1) * (j - 2) % Mod * s[i - 1][j - 3][1] % Mod);
                    
                    f[i][j][0] = add(f[i][j][0], 1ll * (j - 1) * (j - 2) % Mod *
                        dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 3][0]) % Mod);
                    f[i][j][1] = add(f[i][j][1], 1ll * (j - 1) * (j - 2) % Mod *
                        dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 3][0]) % Mod);
                }

                f[i][j][0] = add(f[i][j][0], 1ll * (siz[i] - j) * (siz[i] - j - 1) % Mod *
                    s[i - 1][j - 1][1] % Mod);

                if (j >= 2)
                    f[i][j][0] = add(f[i][j][0], 2ll * (j - 1) * (siz[i] - j) % Mod *
                        s[i - 1][j - 2][1] % Mod);
            }
        } else if (cnt == 2) {
            for (int j = 1; j <= siz[i]; ++j) {
                if (j >= 2) {
                    f[i][j][0] = add(f[i][j][0], 1ll * (j - 1) * s[i - 1][j - 2][1] % Mod);
                    f[i][j][1] = add(f[i][j][1], 1ll * (j - 1) * s[i - 1][j - 2][1] % Mod);

                    f[i][j][0] = add(f[i][j][0], 1ll * (j - 1) *
                        dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 2][0]) % Mod);
                    f[i][j][1] = add(f[i][j][1], 1ll * (j - 1) *
                        dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 2][0]) % Mod);
                }

                f[i][j][0] = add(f[i][j][0], 1ll * (siz[i] - j) * s[i - 1][j - 1][1] % Mod);
            }
        } else {
            for (int j = 1; j <= siz[i]; ++j) {
                f[i][j][0] = add(f[i][j][0], s[i - 1][j - 1][1]);
                f[i][j][1] = add(f[i][j][1], s[i - 1][j - 1][1]);

                f[i][j][0] = add(f[i][j][0], dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 1][0]));
                f[i][j][1] = add(f[i][j][1], dec(s[i - 1][siz[i - 1]][0], s[i - 1][j - 1][0]));
            }
        }

        for (int j = 1; j <= m; ++j) {
            s[i][j][0] = add(s[i][j - 1][0], f[i][j][0]);
            s[i][j][1] = add(s[i][j - 1][1], f[i][j][1]);
        }

        if (i == n || str[2][i + 1] == 'o')
            ans = 1ll * ans * s[i][siz[i]][0] % Mod * invfac[siz[i]] % Mod;
    }

    printf("%d", 1ll * ans * fac[m] % Mod);
    return 0;
}

刻画合法态

可以选择找到合法态的充要条件,然后直接队合法态计数;也可以将合法态转化为其他的东西(如构造新序列),然后对转化后的东西计数。

CF1503E 2-Coloring

给定 \(n, m\) ,需要将 \(n \times m\) 的棋盘的每个格子染上蓝色或黄色,要求每行恰好有一段蓝色的格子,每列恰好有一段黄色的格子。求染色方案数,答案对 \(998244353\) 取模。

\(n, m \le 2021\)

考虑刻画合法的棋盘的形态:

  • 蓝色连通块紧贴上/下边界,黄色连通块紧贴左/右边界:否则另一种颜色不只一段。
  • 同色连通块的数量 \(\le 2\) :以黄色为例,若 \(\ge 3\) 则必有一边有两块,此时该颜色边界上不只一段,不合法。
  • 不可能蓝色全连通且黄色全连通:若蓝色全连通则会割断左边界黄色和右边界的黄色,黄色全连通同理。

先考虑蓝色全连通的情况,此时一定存在一个分界点 \(i\) 满足 \(\le i\)\(> i\) 的部分分别有一个黄色连通块,而两个连通块贴在 \((i, i + 1)\) 这条线上的区间显然是不交的,不妨钦定左边区间在右边区间上方。

考虑左边贴着 \((i, i + 1)\) 的边界的最低点,则左边黄色连通块的边界(不算左边界)会被该点分为两端路径,只要对这两段路径计数,然后用乘法原理合并即可。由于钦定的点为左边区间的最低点,因此上方的路径的最后一步必须为向下走,下方的路径最后一步必须为向右走。

黄色全连通的情况只要交换 \(n, m\) 做一次即可。对于两种颜色均有两个连通块的情况,实际就是左右在 \((i, i + 1)\) 边界上的区间紧贴着的情况,顺带计数即可。

时间复杂度 \(O(nm)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

int fac[N], inv[N], invfac[N], s[N];

int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

signed main() {
    scanf("%d%d", &n, &m);
    prework();
    int ans = 0;

    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= m; ++j)
            s[j] = add(s[j - 1], 1ll * C(i + j - 1, i) * C(i - 1 + m - j, i - 1) % Mod);

        for (int j = 1; j <= m; ++j)
            ans = add(ans, 2ll * s[j] % Mod * C(n - i - 1 + j, n - i - 1) % Mod * C(n - i + m - j - 1, n - i) % Mod);
    }

    swap(n, m);

    for (int i = 1; i < n; ++i) {
        for (int j = 1; j <= m; ++j)
            s[j] = add(s[j - 1], 1ll * C(i + j - 1, i) * C(i - 1 + m - j, i - 1) % Mod);

        for (int j = 1; j <= m; ++j)
            ans = add(ans, 2ll * s[j - 1] % Mod * C(n - i - 1 + j, n - i - 1) % Mod * C(n - i + m - j - 1, n - i) % Mod);
    }

    printf("%d", ans);
    return 0;
}

P2467 [SDOI2010] 地精部落

\(1 \sim n\)\(n!\) 种排列中波浪序列的数量 \(\bmod p\) ,其中波浪序列为对于任意 \(1 < i < n\) 均有 \([a_{i - 1} < a_i] \and [a_{i + 1} < a_i]\)\([a_{i - 1} > a_i] \and [a_{i + 1} > a_i]\)

\(n \le 4200\)

在一个波浪序列中,注意到:

  • \(i\)\(i + 1\) 不相邻,则交换二者可以得到一个新的波浪序列。
  • \(a_i \gets n - a_i + 1\) 可以得到一个新的波浪序列。
  • 反转波浪序列可以得到一个新的波浪序列。

\(f_{i, j}\) 表示选了 \(1 \sim i\) 且第一个数为 \(j\) 作为山峰的波浪序列方案数,则答案为 \(2 \sum_{i = 2}^n f_{n, i}\)

  • \(j\)\(j - 1\) 不相邻时,直接交换二者即可得到新的波浪序列,有 \(f_{i, j} \gets f_{i, j - 1}\) 。可以直接转移是因为 \(j - 1\) 为山峰时 \(j\) 不可能在 \(j - 1\) 后面。
  • \(j\)\(j - 1\) 相邻时,此时 \(j\) 为山峰而 \(j - 1\) 为山谷,问题转化为求 \(i - 1\) 个数 \([1, j) \cup (j, i]\)\(j - 1\) 开头为山谷的方案数,其等于 \(j - 1\) 开头为山峰的方案数,有 \(f_{i, j} \gets f_{i - 1, i - j + 1}\) 。可以直接用 \(i - 1\) 是因为只和相对大小有关直接平移即可, \(i - j + 1\) 是因为需要得到山谷的方案数。

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

#include <bits/stdc++.h>
using namespace std;
const int N = 4.2e3 + 7;

int f[2][N];

int n, Mod;

inline int add(int x, int y) {
	x += y;
	
	if (x >= Mod)
		x -= Mod;
	
	return x;
}

signed main() {
	scanf("%d%d", &n, &Mod);
	f[0][2] = 1;
	
	for (int i = 3; i <= n; ++i)
		for (int j = 2; j <= i; ++j)
			f[i & 1][j] = add(f[i & 1][j - 1], f[~i & 1][i - j + 1]);
	
	int ans = 0;
	
	for (int i = 2; i <= n; ++i)
		ans = add(ans, f[n & 1][i]);
	
	printf("%d", 2ll * ans % Mod);
	return 0;
}

QOJ8009. Growing Sequences

给定 \(n, c\) ,求满足以下条件的序列数量 \(\bmod 998244353\)

  • 长度为 \(n\) ,元素均为 \(1 \sim c\) 中的整数。
  • \(\forall i \in [1, n - 1], 2 a_i \le a_{i + 1}\)

\(n \le 60\)\(c \le 10^{18}\)

元素的偏序看起来不太好处理,考虑对类差分序列 \(c_i = a_i - 2 a_{i - 1}\) 计数(规定 \(a_0 = 0\) ),这样就可以将限制转化为 \(c_1 \ge 1\)\(c+i \ge 0\)\(a_n = \sum_{i = 1}^n 2^{n - i} c_i \le c\)

先处理 \(c_1 \ge 1\) 的限制,一个简单的方法是差分去重,用 \(n\) 的答案减去 \(n - 1\) 的答案即可。

考虑数位 DP,但是并不好处理 \(a_n \le c\) 的限制,原因是值域太大,无法存储到状态中。

由于 \(a_n = \sum_{i = 1}^n 2^{n - i} c_i\) ,因此考虑将 \(c_i\) 二进制下的每个 \(1\) 位和系数匹配,这样就只需要记录进位数量,并且每个位仍然独立。

dfs(x, k, d, lead) 表示从低到高考虑到第 \(x\) 位、当前有 \(k\) 个空位可以填、进位数量为 \(d\) 、是否大于上界的方案数,转移时枚举 \(k\) 个空位中填 \(j\) 个,需要乘上系数 \(\binom{k}{j}\) ,然后递归子问题处理即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 998244353;
const int N = 61;

int f[N][N][2], fac[N], inv[N], invfac[N];

ll m;
int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

int dfs(int x, int k, int d, bool lead) {
    if (x > __lg(m))
        return !d && !lead;
    else if (~f[x][d][lead])
        return f[x][d][lead];

    f[x][d][lead] = 0;

    for (int j = 0; j <= k; ++j)
        f[x][d][lead] = add(f[x][d][lead], 1ll * C(k, j) * dfs(x + 1, min(n, k + 1), (j + d) >> 1, 
            (((j + d) & 1) == (m >> x & 1) ? lead : (((j + d) & 1) > (m >> x & 1)))) % Mod);

    return f[x][d][lead];
}

signed main() {
    scanf("%d%lld", &n, &m);
    prework(), memset(f, -1, sizeof(f));
    int ans = dfs(0, 1, 0, false);

    if (--n)
        memset(f, -1, sizeof(f)), ans = dec(ans, dfs(0, 1, 0, false));
    else
        ans = dec(ans, 1);

    printf("%d", ans);
    return 0;
}

CF1943D2 Counting Is Fun (Hard Version)

给定 \(n, m, p\) ,求满足以下条件的序列数量 \(\bmod p\)

  • 长度为 \(n\) ,元素为 \(0 \sim m\) 中的整数。
  • 可以通过若干次操作变为全 \(0\) ,一次操作定义为选择一个长度 \(\ge 2\) 的区间做区间减 \(1\)

\(m \le n \le 3000\)

考虑刻画合法序列的充要条件,钦定 \(a_0 = a_{n + 1} = 0\) ,可以发现序列合法的充要条件为不存在 \(a_i > a_{i - 1} + a_{i + 1}\)

必要性:显然,因为一个位置 \(i\) 至多操作 \(a_{i - 1} + a_{i + 1}\) 次。

充分性:考虑每个相邻的位置 \((i, i + 1)\) 都操作 \(\min(a_i, a_{i + 1})\) 次,但是这样可能会减多,只要把相邻的两个操作合并成一次即可。

下面考虑 DP 计数,朴素的 DP 需要记录当前决策到的位置 \(i\) 、当前值 \(j\) 、上一个值 \(k\) ,状态数目 \(O(n^3)\) 。发现不好整体 DP,需要从状态入手优化。

可以发现不可能存在相邻两个位置均不合法的情况,因此考虑扔掉第三维,设 \(f_{i, j}\) 表示考虑前 \(i\) 个位置、\(a_i = j\) 的合法方案数,则:

\[f_{i, j} = \sum_{x = 0}^m f_{i - 1, x} - \sum_{x = 0}^{m - j - 1} f_{i - 2, x} \times (m - j - x) \]

答案即为 \(f_{n + 1, 0}\) ,不难前缀和优化到 \(O(nk)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;

int f[N][N], s[N][N], g[N][N];

int n, m, Mod;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &Mod);
        f[0][0] = 1, fill(s[0], s[0] + m + 1, 1), fill(g[0], g[0] + m + 1, 0);

        for (int i = 0; i <= m; ++i)
            f[1][i] = 1, s[1][i] = i + 1, g[1][i] = i * (i + 1) / 2;

        for (int i = 2; i <= n + 1; ++i)
            for (int j = 0; j <= m; ++j) {
                f[i][j] = add(dec(s[i - 1][m], 1ll * s[i - 2][m - j] * (m - j) % Mod), g[i - 2][m - j]);
                s[i][j] = add((j ? s[i][j - 1] : 0), f[i][j]);
                g[i][j] = add((j ? g[i][j - 1] : 0), 1ll * f[i][j] * j % Mod);
            }

        printf("%d\n", f[n + 1][0]);
    }

    return 0;
}

另一种方法是考虑合并状态,发现:

  • \(j \le k\) 时,\(j\) 一定符合要求。
  • \(j > k\) 时,\(k\) 一定符合要求。

因此考虑设:

  • \(f_{i, j}\) 表示填了前 \(i\) 个、\(a_{i - 1} = j\) 、规定 \(a_{i - 1} \le a_i\) 的方案数。
  • \(g_{i, j}\) 表示填了前 \(i\) 个、$a_i = j
    $ 、规定 \(a_{i - 1} > a_i\) 的方案数。

考虑转移:

  • \(a_{i - 2} \le a_{i - 1}, a_{i - 1} \le a_i\)\(f_{i, j} \gets \sum_{k = 0}^j f_{i - 1, k}\)
  • \(a_{i - 2} > a_{i - 1}, a_{i - 1} \le a_i\)\(f_{i, j} \gets g_{i - 1, j}\)
  • \(a_{i - 2} \le a_{i - 1}, a_{i - 1} > a_i\)\(g_{i, j} = \sum_{k = 0}^m f_{i - 1, k} \times (\min(k + j, m) - \max(j + 1, k) + 1)\)
  • \(a_{i - 2} > a_{i - 1}, a_{i - 1} > a_i\)\(g_{i, j} \gets \sum_{k = j + 1}^m g_{i - 1, k}\)

不难前缀和优化到 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;

int f[N][N], g[N][N], sf[N], sf2[N], sg[N];

int n, m, Mod;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d%d", &n, &m, &Mod);
        f[1][0] = 1;

        for (int i = 2; i <= n; ++i) {
            for (int j = 0; j <= m; ++j) {
                sf[j] = add(j ? sf[j - 1] : 0, f[i - 1][j]);
                sf2[j] = add(j ? sf2[j - 1] : 0, 1ll * f[i - 1][j] * j % Mod);
            }

            for (int j = m; ~j; --j)
                sg[j] = add(j < m ? sg[j + 1] : 0, g[i - 1][j]);

            for (int j = 0; j <= m; ++j) {
                f[i][j] = add(sf[j], g[i - 1][j]);
                g[i][j] = add(j < m ? sg[j + 1] : 0, sf[m]);
                g[i][j] = add(g[i][j], add(1ll * sf[m - j] * j % Mod, sf2[m - j]));
                g[i][j] = add(g[i][j], 1ll * dec(sf[m], sf[m - j]) * m % Mod);
                g[i][j] = dec(g[i][j], 1ll * sf[j] * (j + 1) % Mod);
                g[i][j] = dec(g[i][j], dec(sf2[m], sf2[j]));
            }
        }

        int ans = 0;

        for (int i = 0; i <= m; ++i)
            ans = add(ans, add(f[n][i], g[n][i]));

        printf("%d\n", ans);
    }

    return 0;
}

[ARC064F] Rotated Palindromes

对于所有长度为 \(n\) 、元素为 \(1 \sim k\) 之间的整数的回文序列 \(A\) ,记集合 \(S_A\) 表示 \(A\) 循环位移若干步后得到的串的集合,求 \(|\cup_A S_A| \bmod (10^9 + 7)\)

\(n, k \le 10^9\)

考虑去重,显然只有循环同构的回文串才会算重。

由于回文串的循环节也是回文串,因此有如下结论:

  • 若最小循环节为偶数,则将循环节的前一半扔到后面去之后就可以得到一个循环同构且不同的回文串。
  • 若最小循环节为奇数,则不存在循环同构且不同的回文串。

\(f_i\) 表示最小循环节为 \(i\) 的回文串数量,则:

\[f_i = k^{\lceil \frac{i}{2} \rceil} - \sum_{j \mid i, j \ne i} f_j \]

答案即为 \(\sum_{d \mid n} f_d \times s(d)\) ,其中 \(s(d) = \begin{cases} d & 2 \nmid d \\ \frac{d}{2} & 2 \mid d \end{cases}\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;

int n, k;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

signed main() {
    scanf("%d%d", &n, &k);
    vector<int> vec;

    for (int i = 1; i * i <= n; ++i)
        if (!(n % i)) {
            vec.emplace_back(i);

            if (n / i != i)
                vec.emplace_back(n / i);
        }

    sort(vec.begin(), vec.end());
    vector<int> f(vec.size());
    int ans = 0;

    for (int i = 0; i < vec.size(); ++i) {
        f[i] = mi(k, (vec[i] + 1) / 2);

        for (int j = 0; j < i; ++j)
            if (!(vec[i] % vec[j]))
                f[i] = dec(f[i], f[j]);

        ans = add(ans, 1ll * f[i] * (vec[i] & 1 ? vec[i] : vec[i] / 2) % Mod);
    }

    printf("%d", ans);
    return 0;
}

CF1310E Strange Function

对于一个多重集 \(S\) ,记 \(f(S)\) 表示每个元素出现次数组成的多重集。

给定 \(n, k\) ,对于所有大小为 \(n\) 的多重集 \(S\) ,求本质不同的 \(f^k(S)\) 的数量 \(\bmod 998244353\)

\(1 \le n, k \le 2020\)

直接枚举初始集合是困难的,但是可以转为枚举最终集合,求一个大小最小的初始集合判定合法性。

直觉上迭代 \(k\) 次后答案很少,考虑特殊处理 \(k\) 比较小的情况,剩下的情况直接搜索剪枝处理。

\(k = 1\) 时,答案即为划分数,不难 \(O(n^2)\) 做完全背包。

\(k = 2\) 时,设 \(B = f(A), C = f(B)\) ,尝试判定一个 \(C\) 的合法性。

\(C = c_{1 \sim m}\)\(c_1 \ge c_2 \ge \cdots \ge c_m\) ,则存在一组 \(v\) ,满足 \(|A| = \sum v_i c_i\) ,由排序不等式得到判定条件为 \(\sum i c_i \le |A| \le n\)

但是 \(c\) 的有序性不好处理,考虑令 \(d_i = c_i - c_{i - 1}\) ,则判定条件转化为 \(\sum d_i \times \frac{i (i + 1)}{2} \le n\) ,不难 \(O(n \sqrt{n})\) 做完全背包。

\(k \ge 3\) 时,考虑直接暴搜,按照 \(k = 2\) 的方法可以求出初始集合的最小大小,极值剪枝掉最小集合大小超过 \(n\) 的状态即可。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e3 + 7;

int f[N];

int n, k, ans;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline bool check(vector<int> a) {
    for (int i = 1; i < k; ++i) {
        reverse(a.begin(), a.end());
        int sum = 0;

        for (int i = 0; i < a.size(); ++i)
            sum += (i + 1) * a[i];

        if (sum > n)
            return false;

        vector<int> b;

        for (int i = 0; i < a.size(); ++i)
            b.insert(b.end(), a[i], i + 1);

        a = b;
    }

    return true;
}

void dfs(vector<int> vec) {
    for (int i = vec.empty() ? 1 : vec.back(); i <= n; ++i) {
        vec.emplace_back(i);

        if (!check(vec))
            break;

        ++ans, dfs(vec), vec.pop_back();
    }
}

signed main() {
    scanf("%d%d", &n, &k);
    
    if (k == 1) {
        f[0] = 1;

        for (int i = 1; i <= n; ++i)
            for (int j = i; j <= n; ++j)
                f[j] = add(f[j], f[j - i]);

        for (int i = 1; i <= n; ++i)
            ans = add(ans, f[i]);
    } else if (k == 2) {
        f[0] = 1;

        for (int i = 1; i * (i + 1) / 2 <= n; ++i)
            for (int j = i * (i + 1) / 2; j <= n; ++j)
                f[j] = add(f[j], f[j - i * (i + 1) / 2]);

        for (int i = 1; i <= n; ++i)
            ans = add(ans, f[i]);
    } else
        dfs({});

    printf("%d", ans);
    return 0;
}

P6836 [IOI 2020] 装饼干

\(k\) 种物品,编号为 \(0 \sim k - 1\) 。第 \(i\) 种物品有 \(a_i\) 个,价值为 \(2^i\)

需要将这些物品装袋,给定 \(x\) ,求有多少个 \(y\) 使得能够凑出至少 \(x\) 袋价值和为 \(y\) 的物品组

\(k \le 60\)\(x \le 10^{18}\) ,多测 \(q \le 1000\)

因为低位可以凑出高位,因此考虑从低到高考虑寻找充要的判定条件:

  • 对于最低位 \(y_0\) ,有 \(a_0 \ge x y_0\)
  • 对于次低位 \(y_1\) ,有 \(a_1 + \frac{1}{2} (a_0 - x y_0) \ge x y_1\) ,即 \(2a_1 + a_0 \ge x (2y_1 + y_0)\)

以此类推得到合法的充要条件为:

\[\forall i \in [0, k - 1], \sum_{j = 0}^i 2^j y_j \le \frac{1}{x} \sum_{j = 0}^i 2^j a_j \]

\(f_{i, j}\) 表示考虑到第 \(i\) 位,且 \(y\) 的前 \(i\)\(\le j\) 的合法 \(y\) 的数量。记 \(s_i = \frac{1}{x} \sum_{j = 0}^i 2^j a_j\) ,若 \(j > s_i\) ,则答案即为 \(f_{i, s_i}\) ,因此只需考虑 \(j \le s_i\) 的情况。

  • \(y_i = 0\) 时,答案即为 \(f_{i - 1, j}\)
  • \(y_i = 1\) 时:
    • \(j \ge 2^{i + 1} - 1\) ,则选 \(1\) 后相当于没有 \(j\) 的限制,答案为 \(f_{i - 1, s_{i - 1}}\)
    • 否则若 \(j \ge 2^i\) ,则可以选这一位,答案为 \(f_{i - 1, j - 2^i}\)

边界是 \(f_{0, j} = \min(j, 1) + 1\) ,直接记忆化搜索,记录每个 \(f_{i, s_i}\) 即可,答案即为 \(f_{59, s_{59}}\) ,单组数据时间复杂度 \(O(k^2)\)

#include "biscuits.h"
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int B = 63;

ll f[B], sum[B];

ll F(int i, ll j) {
    j = min(sum[i], j);

    if (!i)
        return min(j, 1ll) + 1;
    else if (j == sum[i] && ~f[i])
        return f[i];

    ll res = F(i - 1, j);

    if (j >= (1ll << (i + 1)) - 1)
        res += F(i - 1, sum[i - 1]);
    else if (j >= (1ll << i))
        res += F(i - 1, j - (1ll << i));

    if (j == sum[i])
        f[i] = res;

    return res;
}

ll count_tastiness(ll x, vector<ll> a) {
    memset(f, -1, sizeof(f)), memset(sum, 0, sizeof(sum));

    for (int i = 0; i < a.size(); ++i)
        sum[i] = a[i] << i;

    for (int i = 1; i < B; ++i)
        sum[i] += sum[i - 1];

    for (int i = 0; i < B; ++i)
        sum[i] /= x;

    return F(B - 1, sum[B - 1]);
}

P9479 [NOI2023] 桂花树

给出一棵 \(n\) 个点的树 \(T\) ,定义一棵 \(n + m\) 个点的树 \(T'\) 是好的当且仅当:

  • \(\forall 1 \le i, j \le n, \mathrm{LCA}_T(i, j) = \mathrm{LCA}_{T'}(i, j)\)
  • \(\forall 1 \le i, j \le n + m, \mathrm{LCA}_{T'}(i, j) \le \max(i, j) + k\) ,其中 \(k\) 是给定的常数。

求好的树的数量 \(\bmod (10^9 + 7)\)

\(n \le 30000\)\(m \le 3000\)\(k \le 10\)

观察 \(T'\)\(T\) 的结构差异,不难发现 \(T'\) 中的点 \(x \in [n + 1, n + m]\) 只能在 \(T\) 的基础上加入,此时判定条件转化为:

  • \(T'\)\(1 \sim n\) 的虚树为其本身。
  • \(T'\)\(1 \sim i\) 的虚树所有点的标号 \(\le i + k\)

考虑按标号升序确定点 \(x \in [n + 1, n + m]\) ,每次加入一个点时决策:

  • 挂在一个点上作为叶子。
  • 插到一条边中间。
  • 在一条边上新建一个未确定的点,将其挂在该点上作为叶子。
  • 确定一个未确定的点。

由于第二个条件的限制,未确定的点数量 \(\le k\) 。设 \(f_{i, S}\) 表示决策了前 \(i\) 个点,其中未确定的点的状态为 \(S\) 的方案数,记 \(c = i + |S|\) 表示当前树的点数,考虑 \(i + 1\) 的决策进行转移即可做到 \(O(m k 2^k)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e4 + 7;

int fa[N];

int n, m, k;

inline int add(int x, int y) {
	x += y;

	if (x >= Mod)
		x -= Mod;

	return x;
}

inline int dec(int x, int y) {
	x -= y;

	if (x < 0)
		x += Mod;

	return x;
}

signed main() {
	int testid, T;
	scanf("%d%d", &testid, &T);

	while (T--) {
		scanf("%d%d%d", &n, &m, &k);

		for (int i = 2; i <= n; ++i)
			scanf("%d", fa + i);

		vector<int> f(1 << k);
		f[0] = 1;

		for (int i = 0; i < m; ++i) {
			vector<int> g(1 << k);

			for (int s = 0; s < (1 << k); ++s) {
				for (int r = s; r; r = (r - 1) & r) {
					int t = (s ^ (r & -r)) << 1;

					if (t < (1 << k))
						g[t] = add(g[t], f[s]);
				}

				int t = s << 1, c = n + i + __builtin_popcount(s);

				if (t < (1 << k)) {
					g[t] = add(g[t], 1ll * f[s] * (c * 2 - 1) % Mod);
					g[t | 1] = add(g[t | 1], 1ll * f[s] * (c - 1) % Mod);
				}
			}

			f = g;
		}

		printf("%d\n", f[0]);
	}

	return 0;
}

P10008 [集训队互测 2022] Range Minimum Element

有一个长度为 \(n\),值域为 \([1, c]\) 的正整数序列 \(a\)

给定 \(m\) 个区间 \([l_i,r_i]\),设长度为 \(m\) 的序列 \(b\) 满足 \(\forall i \in [1, m], b_i = \min_{j = l_i}^{r_i} a_j\)

求出 \(a\) 在范围内任意取的情况下共能得到多少种不同的 \(b\) ,答案对 \(998244353\) 取模。

\(n \le 100\)\(m \le \frac{n (n + 1)}{2}\)\(c < 998244353\)

考虑将 \(b\) 映射到一个代表元 \(a\) 上,考虑如下过程:从小到大枚举 \(i = 1, 2, \cdots, c\) ,把不被 \(b_j > i\) 的区间 \([l_j, r_j]\) 包含且未确定的位置赋值为 \(i\) ,记其得到的序列为 \(a = g(b)\)

记题目中 \(a \to b\) 的映射为 \(f\) ,则 \(b\) 是合法的当且仅当 \(f(g(b)) = b\) 。容易发现该映射方式对于 \(b_1 \ne b_2\) 一定有 \(g(b_1) \ne g(b_2)\) ,因此问题转化为统计 \(a = g(b)\) 的数量。

考虑区间 DP,设 \(f_{l, r, i}\) 表示只考虑区间 \([l, r]\) ,且值域为 \([1, i]\) 的方案数。枚举第一个填 \(1\) 的位置 \(k\) ,则 \(f_{l, r, i} \gets f_{l, k - 1, i - 1} \times f_{k + 1, r, i}\) 。但是这里的 \(k\) 需要合法,即 \([l, k - 1]\) 都要至少被一个 \(b > 1\) 的区间覆盖,即 \([l, k - 1]\) 内部所有区间的并恰好为 \([l, k - 1]\) ,构造就是钦定这些区间的 \(b\)\(> 1\)

预处理合法的区间后直接 DP 可以做到 \(O(n^3 c)\) ,答案即为 \(f_{1, n, c}\)

可以发现 \(f_{l, r, i}\) 可以视为关于 \(i\) 的次数为 \(r - l + 1\) 的多项式,因此对 \(c = 1, 2, \cdots, n\) DP 后拉格朗日插值反推系数计算答案即可做到 \(O(n^4)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 998244353;
const int N = 1e2 + 7;

struct Interval {
	int l, r;
} a[N * N];

int f[N][N][N], g[N];
bool flag[N][N];

int n, m, c;

inline int add(int x, int y) {
	x += y;

	if (x >= Mod)
		x -= Mod;

	return x;
}

inline int dec(int x, int y) {
	x -= y;

	if (x < 0)
		x += Mod;

	return x;
}

inline int mi(int a, int b) {
	int res = 1;

	for (; b; a = 1ll * a * a % Mod, b >>= 1)
		if (b & 1)
			res = 1ll * res * a % Mod;

	return res;
}

namespace Lagrange {
int f[N], g[N];

inline void solve(int *y, int n) {
    memset(g, 0, sizeof(int) * (n + 1));
    g[0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = i; ~j; --j)
            g[j] = add(1ll * (Mod - i) * g[j] % Mod, g[j - 1]);

    memset(f, 0, sizeof(int) * n);

    for (int i = 1; i <= n; ++i) {
        int mul = 1;

        for (int j = 1; j <= n; ++j)
            if (i != j)
                mul = 1ll * mul * dec(i, j) % Mod;

        mul = 1ll * y[i] * mi(mul, Mod - 2) % Mod;

        for (int j = n - 1, res = g[n]; ~j; --j)
            f[j] = add(f[j], 1ll * mul * res % Mod), res = add(g[j], 1ll * i * res % Mod);
    }
}
} // namespace Lagrange

int main() {
	scanf("%d%d%d", &n, &m, &c);

	for (int i = 1; i <= m; ++i)
		scanf("%d%d", &a[i].l, &a[i].r);

	for (int i = 1; i <= n; ++i)
		for (int j = i; j <= n; ++j) {
			vector<int> c(n + 2);

			for (int k = 1; k <= m; ++k)
				if (i <= a[k].l && a[k].r <= j)
					++c[a[k].l], --c[a[k].r + 1];

			for (int k = 1; k <= n; ++k)
				c[k] += c[k - 1];

			flag[i][j] = !count(c.begin() + i, c.begin() + j + 1, 0);
		}

	for (int i = 0; i <= n + 1; ++i)
		for (int j = 0; j <= n + 1; ++j)
			f[1][i][j] = 1;

	for (int x = 2; x <= n + 1; ++x) {
		for (int i = 0; i <= n; ++i)
			f[x][i + 1][i] = 1;

		for (int len = 1; len <= n; ++len)
			for (int l = 1, r = len; r <= n; ++l, ++r) {
				if (flag[l][r])
					f[x][l][r] = f[x - 1][l][r];

				for (int k = l; k <= r; ++k)
					if (k == l || flag[l][k - 1])
						f[x][l][r] = add(f[x][l][r], 1ll * f[x - 1][l][k - 1] * f[x][k + 1][r] % Mod);
			}
	}

	for (int i = 1; i <= n + 1; ++i)
		g[i] = f[i][1][n];

	Lagrange::solve(g, n + 1);
	int ans = 0;

	for (int i = 0, mul = 1; i <= n + 1; ++i, mul = 1ll * mul * c % Mod)
		ans = add(ans, 1ll * Lagrange::f[i] * mul % Mod);

	printf("%d", ans);
	return 0;
}

转化为概率

没有本质的区别, 但是有时便于分析。

CF838D Airplane Arrangements

飞机上有 \(n\) 个座位,有 \(m\) 个人上飞机,每个人有一个目标座位。

每个人会依次从前门和后门中的一个进来然后找到目标座位,如果目标座位有人的话就会一直往另一边走直到找到一个座位坐下,若走到末端仍然没有座位则不合法。

求合法方案数。

\(m \le n \le 10^6\)

考虑计数转概率分析。新建一个虚点 \(n + 1\) ,将首尾连接起来,这样座位就变成一个环。

每个人可以选择一个目标座位,然后从目标座位开始选一个方向走,如果最后落座 \(n + 1\) 则不合法。不妨钦定目标座位可以选 \(n + 1\) ,反正不合法不会被统计。

从而环上每个点等价,每个空位被落座的概率相等。因此合法的概率为 \(\frac{n + 1 - m}{n + 1}\) ,总共有 \((2(n + 1))^m\) 种方案。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;

int n, m;

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

signed main() {
    scanf("%d%d", &n, &m);
    printf("%d", 1ll * (n + 1 - m) * mi(n + 1, Mod - 2) % Mod * mi((n + 1) * 2, m) % Mod);
    return 0;
}

转换主体

[ARC184D] Erase Balls 2D

二维平面上有 \(n\) 个球,第 \(i\) 个球位于 \((x_i, y_i)\) ,保证 \(x_{1 \sim n}\)\(y_{1 \sim n}\) 分别为 \(1 \sim n\) 的排列,即横纵坐标分别两两不同。

可以执行任意次以下操作:从剩下的球中选择一个球,记为 \(k\) 。对剩下的每个球 \(i\) ,若 \(i, k\) 两个球的二维坐标具有二维偏序关系,则将 \(i\) 移除。

求操作结束后,可能的剩下球的集合的数量 \(\bmod 998244353\)

\(n \le 300\)

直接数最终剩下的球的集合不好做,考虑对选球集合计数。

但是这样会算重,进而考虑增加限制,对满足以下条件的选球集合计数:选了集合外的任意一个球都会导致至少有一个球被删。这样选球集合就与剩下球的集合构成双射。

先将所有球按 \(x\) 坐标排序,则只需要判断对于所有相邻的球 \((p_i, p_{i + 1})\) ,把所有 \(j \in (p_i, p_{i + 1})\)\(y_j \in (y_{p_{i + 1}}, y_{p_i})\) 的球 \(j\) 拿出来,判断是否每个 \(j\) 都满足:在这些球中,\(j\) 前面存在 \(y\) 比它小的球或 \(j\) 后面存在 \(y\) 比它大的球。

考虑 DP,设 \(f_i\) 表示考虑前 \(i\) 个球,且选了第 \(i\) 个球的方案数,转移时可以 \(O(n)\) 判定一个区间的合法性,时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 3e2 + 7;

int a[N], f[N], buc[N], pre[N], suf[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline bool check(int l, int r) {
    int tot = 0;

    for (int i = l + 1; i < r; ++i)
        if (a[l] > a[i] && a[i] > a[r])
            buc[++tot] = a[i];

    pre[0] = n + 1;

    for (int i = 1; i <= tot; ++i)
        pre[i] = min(pre[i - 1], buc[i]);

    suf[tot + 1] = 0;

    for (int i = tot; i; --i)
        suf[i] = max(suf[i + 1], buc[i]);

    for (int i = 1; i <= tot; ++i)
        if (pre[i - 1] >= buc[i] && buc[i] >= suf[i + 1])
            return false;

    return true;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        a[x] = y;
    }

    a[0] = n + 1, f[0] = 1;

    for (int i = 1; i <= n + 1; ++i)
        for (int j = 0; j < i; ++j)
            if (a[j] > a[i] && check(j, i))
                f[i] = add(f[i], f[j]);

    printf("%d", f[n + 1]);
    return 0;
}

CF1474F 1 2 3 4 ...

给定一个序列,求其中最长严格上升子序列长度及其数量。

序列形如如下方式:分为 \(n\) 个段,每一段都是公差为 \(1\) 的等差数列,且相邻段的端点值相等。

\(n \le 50\)

首先 LIS 长度是好求的,显然 LIS 是值域上的连续段,枚举两段的极值处的差值即可。

观察到不同值域的 LIS 在序列上的占据区间一定不交,因此可以分开处理,只需考虑固定 LIS 最小值和最大值的情况。

\(f_{i, j}\) 表示当前值为 \(i\) ,所在段为 \(j\) 的 LIS 方案数,朴素转移不难。由于值域很大,考虑维护所在段为主体,将值域的端点离散化,则相邻端点间的一段上升或下降的区间的转移都是相同的,因此可以用矩阵快速幂优化。

时间复杂度 \(O(n^4 \log V)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 998244353;
const int N = 5e1 + 7;

ll s[N];
int a[N];

int n, X, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

struct Matrix {
    int a[N][N];

    inline Matrix() {
        memset(a, 0, sizeof(a));
    }

    inline Matrix operator * (const Matrix &rhs) const {
        Matrix res;

        for (int i = 0; i < m; ++i)
            for (int j = 0; j < m; ++j)
                for (int k = 0; k < m; ++k)
                    res.a[i][k] = add(res.a[i][k], 1ll * a[i][j] * rhs.a[j][k] % Mod);

        return res;
    }

    inline Matrix operator ^ (ll b) const {
        Matrix base = *this, res;

        for (int i = 0; i < m; ++i)
            res.a[i][i] = 1;

        for (; b; b >>= 1, base = base * base)
            if (b & 1)
                res = res * base;

        return res;
    }
};

inline int solve(int l, int r) {
    m = r - l + 1;
    vector<ll> vec;

    for (int i = l + 1; i <= r; ++i) {
        ll nl = s[i - 1] + (a[i] > 0 ? 1 : -1), nr = s[i];

        if (nl > nr)
            swap(nl, nr);

        vec.emplace_back(nl - 1), vec.emplace_back(nr);
    }

    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
    vec.erase(vec.begin(), lower_bound(vec.begin(), vec.end(), s[l]));
    Matrix f;

    for (int i = l; i <= r; ++i)
        if (s[i] == s[l])
            f.a[0][i - l] = 1;

    for (int i = 1; i < vec.size(); ++i) {
        Matrix base;

        for (int j = l + 1; j <= r; ++j) {
            ll nl = s[j - 1] + (a[j] > 0 ? 1 : -1), nr = s[j];

            if (nl > nr)
                swap(nl, nr);

            if (nl <= vec[i] && vec[i] <= nr) {
                for (int k = l; k < j; ++k)
                    base.a[k - l][j - l] = 1; // f[v - 1][k] -> f[v][j], (k < j)

                if (a[j] > 0)
                    base.a[j - l][j - l] = 1; // f[v - 1][i] -> f[v][i], (s[i - 1] < s[i])
            }
        }

        f = f * (base ^ (vec[i] - vec[i - 1]));
    }

    int res = 0;

    for (int i = 0; i < m; ++i)
        res = add(res, f.a[0][i]);

    return res;
}

signed main() {
    scanf("%d%d", &n, &X);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", a + i), s[i] = s[i - 1] + a[i];

        if (!a[i])
            --i, --n;
    }

    if (!n || *max_element(a + 1, a + n + 1) < 0)
        return printf("1 %d\n", (-s[n] + 1) % Mod), 0;

    ll ans1 = 0;

    for (int i = 1; i <= n; ++i)
        for (int j = i; j <= n; ++j)
            ans1 = max(ans1, s[j] - s[i - 1] + 1);

    int ans2 = 0;

    for (int i = 0; i <= n; ++i)
        for (int j = n; j > i; --j)
            if (s[j] - s[i] + 1 == ans1) {
                ans2 = add(ans2, solve(i, j)), i = j;
                break;
            }

    printf("%lld %d", ans1, ans2);
    return 0;
}

笛卡尔树

P5853 [USACO19DEC] Tree Depth P

给定 \(n, k\) ,对于所有 \(1 \le i \le n\) ,求:

\[\sum_{\pi} \mathrm{dep}(\pi, i) \]

其中 \(\pi\) 为长度为 \(n\) 、逆序对数为 \(k\) 的排列,\(\mathrm{dep}(\pi, i)\) 表示排列 \(\pi\) 构建的小根笛卡尔树上 \(i\) 的深度。

\(n \le 300\)

考虑将 \(u\) 的深度拆分为祖先的数量 \(+1\) ,而 \(v\)\(u\) 的祖先当且仅当 \(a_v < a_u\)\(u, v\) 之间没有 \(< a_v\) 的数。

先考虑求逆序对为 \(k\) 的排列数量,设 \(f_{i, j}\) 表示长度为 \(i\) 、逆序对有 \(j\) 个的排列数量,则 \(i - 1\) 后插入一个数可以对逆序对产生 \([0, i - 1]\) 的贡献,不难前缀和优化到 \(O(n^3)\)

接下来考虑一对祖先关系 \((u, v)\) 的限制条件,可以先固定 \(u, v\) 之间的数,再固定 \(u, v\) ,最后固定其他数。不难发现这个插入过程均在端点处插入,对逆序对的贡献和原来 DP 是类似的,只是钦定了 \(v\) 处必须对逆序对贡献 \(\max(v - u, 0)\)

考虑枚举 \(|v - u|\) ,先撤销 \(v\) 的贡献,再强制插入 \(\max(v - u, 0)\) 的贡献,不难做退背包做到 \(O(nk)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 7;

int f[N * N], g[N * N], ans[N];

int n, k, Mod;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void solve(int x) {
    memcpy(g, f, sizeof(g));

    for (int i = x; i <= k; ++i)
        g[i] = add(g[i], g[i - x]);

    for (int i = k; i; --i)
        g[i] = dec(g[i], g[i - 1]);
}

signed main() {
    scanf("%d%d%d", &n, &k, &Mod);
    f[0] = 1;

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= k; ++j)
            f[j] = add(f[j], f[j - 1]);

        for (int j = k; j >= i; --j)
            f[j] = dec(f[j], f[j - i]);
    }

    fill(ans + 1, ans + n + 1, f[k]);

    for (int i = 1; i < n; ++i) { // i = abs(v - u)
        solve(i + 1);

        if (i <= k) {
            for (int j = 1; j <= n - i; ++j)
                ans[j] = add(ans[j], g[k - i]); // v 在 u 后面,对逆序对产生 v - u 的贡献
        }

        for (int j = i + 1; j <= n; ++j)
            ans[j] = add(ans[j], g[k]); // v 在 u 前面,对逆序对不产生贡献
    }

    for (int i = 1; i <= n; ++i)
        printf("%d ", ans[i]);

    return 0;
}

P8859 冒泡排序

对于排列或圆排列 \(A\) ,定义 \(f(A)\) 为将 \(A\) 升序排序所需的最小操作次数。

每次操作中可以选择一个位置并向前冒泡若干次,一次冒泡定义为:若当前值小于前一个数,则可以交换它与前一个元素。当某次无法冒泡时,这次操作立即停止,否则可以连续冒泡任意次。

给定 \(n, k\) ,求有多少长度为 \(n\) 的排列或圆排列 \(A\) 满足 \(f(A) = k\)

\(n \le 500\)

先考虑排列的情况,设 \(f_{i, j}\) 表示从大到小插入第 \(i\) 个数,讨论 \(i\) 是否需要操作得到:

\[f_{i, j} = f_{i - 1, j} + (i - 1) \times f_{i - 1, j - 1} \]

接下来考虑圆排列的情况,不妨钦定 \(n\) 为最后一个元素,转化为排列的情况。

但是还是有一些差别,因为必然有一个点不会冒泡,可能会将一段前缀放到后面,即进行若干次循环位移。

考虑记录无需操作的数,即所有前缀最大值所在的位置。建立笛卡尔树,前缀最大值数量即为左链上点的数量。

考虑一次循环位移的影响,具体就是把左链最底端的点 \(u\) 接到根的右子树,然后把 \(u\) 的右子树接到 \(u\) 父亲底下(原来 \(u\) 的位置)。而 \(u\) 具体怎么操作无需关心,因为其一定会合法。

对于固定的圆排列,问题转化为最大化左链的长度。设 \(f_{i, j}\) 表示考虑了 \(i\) 个点的二叉树、有 \(j\) 个数不用操作的方案数,枚举左右子树能无需操作的次数,则:

\[\binom{i - 1}{j} \times f_{j, p} \times f_{i - 1 - j, q} \to f_{i, \max(p + 1, q)} \]

不难前缀和优化做到 \(O(n^3)\) ,答案为 \(f_{n - 1, n - 1 - k}\) ,因为 \(n\) 没有右子树。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e2 + 7;

int n, k, tp;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

namespace Method1 {
int f[N][N];

inline int solve() {
    f[0][0] = 1;

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= i; ++j)
            f[i][j] = add(f[i - 1][j], j ? 1ll * (i - 1) * f[i - 1][j - 1] % Mod : 0);

    return f[n][k];
}
} // namespace Method1

namespace Method2 {
int fac[N], inv[N], invfac[N], f[N][N], g[N][N], s[N][N];

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int solve() {
    prework(n), f[0][0] = g[0][0] = s[0][0] = 1;

    for (int i = 1; i < n; ++i) {
        for (int j = 0; j < i; ++j) {
            for (int p = 0; p <= j; ++p)
                f[i][p + 1] = add(f[i][p + 1], 1ll * g[j][p] * s[i - 1 - j][min(i - 1 - j, p)] % Mod);

            for (int q = 1; q <= i - 1 - j; ++q)
                f[i][q] = add(f[i][q], 1ll * s[j][min(j, q - 1)] * g[i - 1 - j][q] % Mod);
        }
        
        for (int j = 0; j <= i; ++j) {
            f[i][j] = 1ll * f[i][j] * fac[i - 1] % Mod;
            s[i][j] = add(j ? s[i][j - 1] : 0, g[i][j] = 1ll * f[i][j] * invfac[i] % Mod);
        }
    }

    return f[n - 1][n - 1 - k];
}
} // namespace Method2

signed main() {
    scanf("%d%d%d", &n, &k, &tp);
    printf("%d", tp == 1 ? Method1::solve() : Method2::solve());
    return 0;
}
posted @ 2025-07-25 21:21  wshcl  阅读(66)  评论(0)    收藏  举报