计数中的统计方法

计数中的统计方法

拆分法

CF660E Different Subsets For All Tuples

给定 \(n, m\) ,对于所有长度为 \(n\) ,值域为 \([1, m] \cap \mathbb{Z}\) 的序列,求每个序列中本质不同子序列数量(包括空序列)的和 \(\bmod (10^9 + 7)\)

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

考虑统计每一个子序列的出现次数和,为了避免算重,只统计其第一次出现的贡献。

对于长度为 \(i\) 、在 \(j\) 结束的子序列,则前 \(j\) 个位置中除了 \(i\) 个位置外的位置都只有 \(m - 1\) 种填法(不能与第一个右边的子序列内的数相同),因此答案即为:

\[\begin{aligned} & \sum_{i = 1}^n \sum_{j = i}^n \binom{j - 1}{i - 1} m^{n - j + i} (m - 1)^{j - i} \\ =& \sum_{j = 0}^{n - 1} \sum_{i = 0}^j \binom{j}{i} m^{n - j + i} (m - 1)^{j - i} \\ =& \sum_{j = 0}^{n - 1} m^{n - j} \sum_{i = 0}^j \binom{j}{i} m^i (m - 1)^{j - i} \\ =& \sum_{i = 0}^{n - 1} m^{n - i} (2m - 1)^i \\ \end{aligned} \]

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

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 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);
    int ans = mi(m, n);

    for (int i = 0; i < n; ++i)
        ans = add(ans, 1ll * mi(m, n - i) * mi(m * 2 - 1, i) % Mod);

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

[AGC023E] Inversions

给定序列 \(a_{1 \sim n}\) ,对于所有 \(\forall i, p_i \le a_i\) 的排列 \(p_{1 \sim n}\) ,求逆序对数量的和 \(\bmod (10^9 + 7)\)

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

\(b_{1 \sim n}\) 表示 \(a\) 升序排序后的结果,容易求出排列的数量为 \(S = \prod_{i = 1}^n (b_i - i + 1)\)

\(r_i\) 表示 \(a_i\) 的排名,考虑枚举一对逆序对 \((i, j)\) 计算出现次数,其中 \(i < j\) ,因此 \(p_i > p_j\)

先考虑 \(r_i < r_j\) 的情况,此时 \(r_i < r_k < r_j\)\(k\) 的可选集合都会少 \(1\) ,答案即为:

\[\binom{a_i - r_i + 1}{2} \times \frac{S}{(a_i - r_i + 1) (a_j - r_j + 1)} \times \prod_{r_i < r_k < r_j} \frac{a_k - r_k}{a_k - r_k + 1} \]

再考虑 \(r_i > r_j\) 的情况,此时 \(p_j\) 不能任意取。考虑补集转化,用总方案数减去 \(p_i < p_j\) 的方案数,答案即为:

\[S - \binom{a_j - r_j + 1}{2} \times \frac{S}{(a_j - r_j + 1) (a_i - r_i + 1)} \times \prod_{r_j < r_k < r_i} \frac{a_k - r_k}{a_k - r_k + 1} \]

下面考虑维护这个式子,按排名从小到大加位置,用树状数组维护当前位置左右已被加入的位置数量,线段树维护贡献(需要支持全局乘、单点加、区间查询),时间复杂度 \(O(n \log n)\)

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

int a[N], id[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 int C2(int n) {
    return (1ll * n * (n - 1) / 2) % Mod;
}

namespace BIT {
int c[N];

inline int lowbit(int x) {
    return x & (~x + 1);
}

inline void update(int x, int k) {
    for (; x; x -= lowbit(x))
        c[x] += k;
}

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

    for (; x <= n; x += lowbit(x))
        res += c[x];

    return res;
}
} // namespace BIT

namespace SMT {
int s[N << 2], tag[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

inline void pushup(int x) {
    s[x] = add(s[ls(x)], s[rs(x)]);
}

inline void spread(int x, int k) {
    s[x] = 1ll * s[x] * k % Mod, tag[x] = 1ll * tag[x] * k % Mod;
}

inline void pushdown(int x) {
    if (tag[x] != 1)
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 1;
}

void build(int x, int l, int r) {
    tag[x] = 1;

    if (l == r)
        return;

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void update(int x, int nl, int nr, int pos, int k) {
    if (nl == nr) {
        s[x] = add(s[x], k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (pos <= mid)
        update(ls(x), nl, mid, pos, k);
    else
        update(rs(x), mid + 1, nr, pos, k);

    pushup(x);
}

int query(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return s[x];
    
    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return query(ls(x), nl, mid, l, r);
    else if (l > mid)
        return query(rs(x), mid + 1, nr, l, r);
    else
        return add(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT

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

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

    iota(id + 1, id + 1 + n, 1);

    sort(id + 1, id + 1 + n, [](const int &x, const int &y) {
        return a[x] < a[y];
    });

    pre[0] = 1;

    for (int i = 1; i <= n; ++i)
        pre[i] = 1ll * pre[i - 1] * (a[id[i]] - i + 1) % Mod;

    suf[n + 1] = 1;

    for (int i = n; i; --i)
        suf[i] = 1ll * suf[i + 1] * (a[id[i]] - i + 1) % Mod;

    SMT::build(1, 1, n);
    int ans = 0;

    for (int i = 1; i <= n; ++i) {
        ans = add(ans, 1ll * SMT::query(1, 1, n, 1, id[i]) * suf[i + 1] % Mod);
        ans = add(ans, 1ll * BIT::query(id[i]) * pre[n] % Mod);
        ans = dec(ans, 1ll * SMT::query(1, 1, n, id[i], n) * suf[i + 1] % Mod);
        BIT::update(id[i], 1), SMT::spread(1, a[id[i]] - i);
        SMT::update(1, 1, n, id[i], 1ll * pre[i - 1] * C2(a[id[i]] - i + 1) % Mod);
    }

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

CF2064F We Be Summing

称一个序列 \(b_{1 \sim m}\) 是好的当且仅当存在一个 \(i \in [1, m)\) 满足 \((\min_{j = 1}^i b_j) + (\max_{j = i + 1}^m b_j) = k\) ,其中 \(k\) 为常数。

给出序列 \(a_{1 \sim n}\) 和常数 \(k\) ,求有多少好的子区间。

\(n \le 2 \times 10^5\)\(n < k < 2n\)\(a_i \in [1, n]\)

考虑枚举 \(x = \min_{j = 1}^i b_j, y = \max_{j = i + 1}^m b_j\) ,其中 \(x + y = k\) ,然后单独拿出 \(a = x\)\(a = y\) 的位置做统计。

如果直接枚举值会算重,这是因为前后缀最值可能在一段区间内都是相等的。考虑枚举最值的位置 \(a_i = x, a_j = y\) ,记 \([l_i, r_i]\)\([l_j, r_j]\) 表示 \(i\)\(j\) 产生贡献的区间,其中 \([l_i, r_i]\) 为最大的包含 \(i\) 的区间满足 \(a_i\) 为最小值,\([l_j, r_j]\) 为最大的包含 \(j\) 的区间满足 \(a_j\) 为最大值。则只要满足 \(i < j \and l_j - 1 \le r_i\)\(i, j\) 就能产生贡献(在 \((l_j - 1, l_j)\) 处断开),即 \(l \in [l_i, i], r \in [j, r_j]\) 的区间均合法。

\(L_{i, 0/1}, R_{i, 0/1}\) 分别表示极长包含 \(i\) 的开区间满足 \(a_i\) 为最小值/最大值的左右端点,则条件转化为 \(i < j \and L_{j, 1} < R_{i, 0}\)

  • 对于 \(i < j\) 的限制,这可以用双指针动态维护。
  • 对于 \(L_{j, 1} < R_{i, 0}\) 的限制,其形如二维偏序,不难用树状数组处理。

为了去重方便,在值相同时可以钦定一个大小顺序,显然这不影响答案。

时间复杂度 \(O(n \log n)\)

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

vector<int> p[N];

int a[N], sta[N], L[N][2], R[N][2];

int n, k;

namespace BIT {
int c[N];

inline void clear(int n) {
    memset(c + 1, 0, sizeof(int) * n);
}

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

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

    for (x = max(x, 1); x <= n; x += x & -x)
        res += c[x];

    return res;
}
} // namespace BIT

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

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

        for (int i = 1; i <= n; ++i)
            p[i].clear();

        for (int i = 1; i <= n; ++i)
            scanf("%d", a + i), p[a[i]].emplace_back(i);

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

            L[i][0] = top ? sta[top] + 1 : 1, sta[++top] = i;
        }

        for (int i = 1, top = 0; i <= n; ++i) {
            while (top && a[sta[top]] <= a[i])
                --top;

            L[i][1] = top ? sta[top] + 1 : 1, sta[++top] = i;
        }

        for (int i = n, top = 0; i; --i) {
            while (top && a[sta[top]] >= a[i])
                --top;

            R[i][0] = top ? sta[top] - 1 : n, sta[++top] = i;
        }

        for (int i = n, top = 0; i; --i) {
            while (top && a[sta[top]] < a[i])
                --top;

            R[i][1] = top ? sta[top] - 1 : n, sta[++top] = i;
        }

        BIT::clear(n);
        ll ans = 0;

        for (int i = max(1, k - n); i <= min(n, k - 1); ++i) {
            int j = k - i, k = 0;

            if (p[i].empty() || p[j].empty())
                continue;

            for (int l = 0; l < p[i].size(); ++l) {
                for (; k < p[j].size() && p[j][k] < p[i][l]; ++k)
                    BIT::update(R[p[j][k]][0], p[j][k] - L[p[j][k]][0] + 1);

                ans += (R[p[i][l]][1] - p[i][l] + 1) * BIT::query(L[p[i][l]][1] - 1);
            }

            for (--k; ~k; --k)
                BIT::update(R[p[j][k]][0], -(p[j][k] - L[p[j][k]][0] + 1));
        }

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

    return 0;
}

[AGC008F] Black Radius

有一棵 \(n\) 个点的树,一开始每个节点均为白色,其中一些点是关键点。

需要选择一个关键点 \(x\) ,然后选择一个自然数 \(d\) ,将所有与 \(x\) 距离不超过 \(d\) 的点都染成黑色。

求染色一次后可能的状态数。

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

先考虑所有点都是关键点的情况,设 \(f(x, d) = \{ y \mid \mathrm{dist}(x, y) \le d \}\) ,考虑每次在 \(d\) 最小的点上统计该点集的答案(不统计整棵树的情况,最后算整棵树的贡献),即 \(f(x, d)\) 需要满足:

  • \(f(x, d)\) 不能覆盖整棵树。
    • \(mx(x)\) 表示距离 \(x\) 的最远点 \(y\) 距离 \(x\) 的距离,设 \(mx(x)\) 表示 \(x\) 离最远点的距离,此时限制条件即为 \(f(x, d) < mx(x)\)
  • 对于 \(x\) 的邻居 \(y\) ,均不存在 \(f(x, d) = f(y, d - 1)\)
    • 考虑以 \(x\) 为根,则 \(y\) 子树外与 \(x\) 距离 \(\in [d - 1, d]\) 的点不在 \(f(y, d - 1)\) 中而在 \(f(x, d)\) 中。设 \(se(x)\) 表示删去 \(mx(x)\) 点所在子树后距离 \(x\) 最远点的距离,则限制条件可以表示为 \(se(x) > d - 2\)

因此一个点 \(x\) 的贡献即为 \(\min(se(x) + 2, mx(x))\)

再考虑原问题。对于一个非关键点 \(u\) ,考虑将 \(f(u, d_u)\) 转到 \(f(v, d_v)\) 上,其中 \(v\) 是一个关键点,且 \(f(u, d_u) = f(v, d_v)\) 。则要求 \(v\) 对应子树全部属于 \(f(u, d)\) ,求出这些 \(v\) 对应子树中深度最小的子树记为 \(low_u\) ,则 \(d_u \ge low_u\) 。不难用换根 DP 做到线性。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
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 siz[N], len[N];
char str[N];

ll ans = 1;
int n;

void dfs1(int u, int f) {
    siz[u] = str[u] & 15, len[u] = 0;

    for (int v : G.e[u])
        if (v != f)
            dfs1(v, u), siz[u] += siz[v], len[u] = max(len[u], len[v] + 1);
}

void dfs2(int u, int f, int out) {
    int fir = out, sec = 0, low = (str[u] & 15 ? 0 : (siz[u] < siz[1] ? out : inf));

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

        if (len[v] + 1 > fir)
            sec = fir, fir = len[v] + 1;
        else if (len[v] + 1 > sec)
            sec = len[v] + 1;

        if (siz[v])
            low = min(low, len[v] + 1);
    }

    ans += max(0, min(sec + 2, fir) - low);

    for (int v : G.e[u])
        if (v != f)
            dfs2(v, u, len[v] + 1 == fir ? sec + 1 : fir + 1);
}

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

    scanf("%s", str + 1);
    dfs1(1, 0), dfs2(1, 0, 0);
    printf("%lld", ans);
    return 0;
}

[ARC176D] Swap Permutation

给定排列 \(p_{1 \sim n}\) ,求随机执行 \(m\) 次交换操作后 \(\sum_{i = 1}^{n - 1} |p_i - p_{i + 1}|\) 的期望乘上 \((\frac{n (n - 1)}{2})^m\) 的值。

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

考虑刻画权值的计算方式,把贡献 \(|p_i - p_{i + 1}|\) 的贡献拆到 \(|p_i - p_{i + 1}|\)\(1\) 上。枚举 \(x = 1, 2, \cdots, n\) ,记 \(q_i = [p_i \ge x]\) ,则权值可以表示为 \(\sum_{x = 1}^n \sum_{i = 1}^{n - 1} [q_i \ne q_{i + 1}]\)

考虑对于一对 \((q_i, q_{i + 1})\) ,计算其在所有 01 序列中的贡献和。设 \(f_{i, s}\) 表示操作 \(i\) 次后当前状态为 \(s\) 的方案数,其中 \(s\) 记录的是 \(q_i\)\(q_{i + 1}\) 的 01 值,\(s\) 只有 \((0, 0), (1, 1), (0, 1) / (1, 0)\) 三种情况。

发现这个过程可以矩阵快速幂,而对于一对 \((q_i, q_{i + 1})\) ,等价的 \(x\) 不超过三种,因此可以做到 \(O(n \log m)\)

另一种方法更加暴力,对于一对 \((p_i, p_{i + 1})\) ,由于其他的值最终到达这里的概率都是相等的,因此可以不做区分,记 \(A = p_i\)\(B = p_{i + 1}\)\(C\) 为其他数,则一共只有 \((A, B), (B, A), (A, C), (C, A), (C, B), (B, C), (C, C)\) 七种情况,暴力矩阵快速幂转移即可。

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

int a[N], c[N][3];

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;
}

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

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

    inline friend Matrix operator * (Matrix a, Matrix b) {
        Matrix c;

        for (int i = 0; i < 3; ++i)
            for (int j = 0; j < 3; ++j)
                for (int k = 0; k < 3; ++k)
                    c.a[i][k] = add(c.a[i][k], 1ll * a.a[i][j] * b.a[j][k] % Mod);

        return c;
    }

    inline friend Matrix operator ^ (Matrix a, int b) {
        Matrix res;

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

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

        return res;
    }
} base;

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

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

    for (int i = 1; i < n; ++i) {
        ++c[max(a[i], a[i + 1]) + 1][0];
        ++c[min(a[i], a[i + 1]) + 1][1], --c[max(a[i], a[i + 1]) + 1][1];
        ++c[1][2], --c[min(a[i], a[i + 1]) + 1][2];
    }

    int ans = 0, all = 1ll * (n - 2) * (n - 3) / 2 % Mod;

    for (int i = 1; i <= n; ++i) {
        int c0 = i - 1, c1 = n - i + 1;
        base.a[0][0] = add(all, 2 * (c0 - 2) + 1), base.a[0][1] = 2 * c1, base.a[0][2] = 0;
        base.a[1][0] = c0 - 1, base.a[1][1] = add(all, n - 1), base.a[1][2] = c1 - 1;
        base.a[2][0] = 0, base.a[2][1] = 2 * c0, base.a[2][2] = add(all, 2 * (c1 - 2) + 1);
        base = (base ^ m);

        for (int j = 0; j < 3; ++j)
            ans = add(ans, 1ll * (c[i][j] += c[i - 1][j]) * base.a[j][1] % Mod);
    }

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

P12558 [UOI 2024] Heroes and Monsters

给定 \(a_{1 \sim n}, b_{1 \sim n}\) ,定义 \(f_k\) 表示满足如下条件的集合 \(S\) 的数量:

  • \(S \subseteq \{ 1, 2, \cdots, k \}\)\(|S| = k\)
  • 存在 \(1 \sim n\) 的排列 \(p\) ,满足 \(\forall i \in S, a_i > b_{p_i}\)\(\forall i \notin S, a_i < b_{p_i}\)

\(q\) 次询问 \(\sum_{i = l}^r f_i\)

\(n \le 5000\)\(a_{1 \sim n}, b_{1 \sim n}\) 两两不同

先考虑如何判断 \(S\) 的可行性,考虑贪心,记 \(T = \{ 1, 2, \cdots, k \} \setminus S\) ,将 \(a, b\) 升序排序后需要满足 \(a_{S_i} > b_i\)\(a_{T_i} < b_{|S| + i}\)

枚举 \(|S|\) ,设 \(f_{i, j}\) 表示考虑前 \(i\)\(a\) 、选了 \(j\) 个的方案数,时间复杂度 \(O(n^3)\)

考虑优化,按 \(b_{|S|}\)\(a\) 分成两部分,则前一部分显然可以放入 \(T\) ,后一部分显然可以放入 \(S\) 。因此只需考虑前一部分是否放入 \(S\) ,后一部分是否放入 \(T\) 即可。

这样限制就拆分开来了,只要对前后缀分别 DP 一次即可,每次统计答案就做一次卷积,时间复杂度 \(O(n^2)\)

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

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

int n, q;

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", &n);

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

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

    sort(a + 1, a + n + 1), sort(b + 1, b + n + 1);
    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 && a[i] > b[j] ? f[i - 1][j - 1] : 0);

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

    for (int i = n; i; --i)
        for (int j = 0; j <= n - i + 1; ++j)
            g[i][j] = add(j ? g[i + 1][j - 1] : 0, a[i] < b[i + j] ? g[i + 1][j] : 0);

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

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

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

    scanf("%d", &q);

    while (q--) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", dec(ans[r], l ? ans[l - 1] : 0));
    }

    return 0;
}

贡献延迟计算

CF1608F MEX counting

给定 \(n, k, b_{1 \sim n}\) ,求满足以下条件的序列 \(a\) 的数量:

  • 长度为 \(n\)\(a_i \in [0, n]\)
  • \(|\mathrm{mex}(a_1, a_2, \cdots, a_i) - b_i| \le k\)

\(n \le 2000\)\(k \le 50\)

朴素的 DP 会想到记录当前选了哪些数,但是这样完全无法存储。

考虑只记录选的数字种数,不记录选的具体数字,具体的数字后面再计算。

\(f_{i, j, k}\) 表示考虑了前 \(i\) 个数、\(\mathrm{mex} = j\) 、选了 \(k\) 种数的方案数,考虑转移:

  • \(a_{i + 1} \ne j\)
    • \(f_{i, j, k} \gets f_{i - 1, j, k} \times k\) :选之前选过的。
    • \(f_{i, j, k} \gets f_{i - 1, j, k - 1}\) :选之前没选过的。
  • \(a_{i + 1} = j\)\(f_{i, j, k} \gets \sum_{l < j} f_{i - 1, l, k - 1} \times \binom{k - 1 - l}{j - l - 1} \times (j - l - 1)!\)

拆开组合数即可前缀和优化做到 \(O(n^2 k)\)

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

int f[2][N][N], s[2][N][N];
int a[N], L[N], R[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;
    }
}

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

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), L[i] = max(0, a[i] - k), R[i] = min(i, a[i] + k);

    f[0][0][0] = s[0][0][0] = 1;

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

                if (k && j)
                    f[i & 1][j][k] = add(f[i & 1][j][k], 
                        1ll * s[~i & 1][min(j - 1, R[i - 1])][k - 1] * invfac[k - j] % Mod);

                s[i & 1][j][k] = add(j ? s[i & 1][j - 1][k] : 0, 1ll * f[i & 1][j][k] * fac[k - j] % Mod);
            }

        for (int j = L[i - 1]; j <= R[i - 1]; ++j) {
            memset(f[~i & 1][j] + j, 0, sizeof(int) * (i - j));
            memset(s[~i & 1][j] + j, 0, sizeof(int) * (i - j));
        }
    }

    int ans = 0;

    for (int i = L[n]; i <= R[n]; ++i)
        for (int j = i; j <= n; ++j)
            ans = add(ans, 1ll * f[n & 1][i][j] * fac[n - i] % Mod * invfac[n - j] % Mod);

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

划分阶段

P5369 [PKUSC2018] 最大前缀和

给定序列 \(a_{1 \sim n}\) ,求打乱排列后 \(n!\) 种可能情况中最大前缀和的和。

\(n \le 20\)

考虑在最后一个最大前缀和处拆贡献,设:

  • \(sum_S\) :集合 \(S\) 的元素和。
  • \(f_S\) :集合 \(S\) 组成的排列中最大前缀和为 \(sum_s\) 的方案数。
  • \(g_S\) :集合 \(S\) 组成的排列中最大前缀和 \(< 0\) 的方案数。

则答案为 \(\sum_S sum_S \times f_S \times g_{U \setminus S}\)

先考虑 \(g\) 的转移,考虑从前往后加数,有 \(g_S = \begin{cases} 0 & sum_S \ge 0 \\ \sum_{x \in S} g_{S \setminus \{ x \}} & sum_S < 0 \end{cases}\)

再考虑 \(f\) 的转移,考虑从后往前加数。若 \(sum_S > 0\) ,则 \(f_{S \cup \{ x \}} \gets f_S\) ,否则无法产生贡献。

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

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

int s[1 << N], f[1 << N], g[1 << 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;
}

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

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

	for (int i = 1; i < (1 << n); ++i)
		s[i] = s[i & -i] + s[(i - 1) & i];

	g[0] = 1;

	for (int i = 0; i < (1 << n); ++i)
		if (s[i] < 0) {
			for (int j = 0; j < n; ++j)
				if (i >> j & 1)
					g[i] = add(g[i], g[i ^ (1 << j)]);
		}

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

	for (int i = 0; i < (1 << n); ++i)
		if (s[i] >= 0) {
			for (int j = 0; j < n; ++j)
				if (~i >> j & 1)
					f[i | (1 << j)] = add(f[i | (1 << j)], f[i]);
		}

	int ans = 0;

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

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

[ARC100F] Colorful Sequences

给定 \(n\) 与长度为 \(m\) 、元素为 \(1 \sim k\) 中的整数的序列 \(a_{1 \sim m}\) ,求所有满足:

  • 长度为 \(n\)
  • 元素为 \(1 \sim k\) 中的整数。
  • 存在一个长度为 \(k\) 的子区间满足 \(1 \sim k\) 出现恰好一次。

的序列中 \(a_{1 \sim m}\) 的出现次数之和 \(\bmod (10^9 + 7)\)

\(n \le 25000\)\(k \le 400\)

先考虑 \(a_{1 \sim m}\) 满足第三个条件的情况,此时答案即为 \(k^{n - m} (n - m + 1)\)

否则满足第三个条件的子区间一定不会跨过 \(a_{1 \sim m}\) ,考虑继续分类讨论。

\(a_{1 \sim m}\) 中存在相同元素,考虑统计不满足第三个条件的序列中 \(a_{1 \sim m}\) 的出现次数和,不难发现两边是独立的,且这只与 \(a_{1 \sim m}\) 左右极长不同色连续段的长度 \(l, r\) 有关。

\(f_{i, j}\) 表示长度为 \(i\) 的序列、末尾极长不同色连续段的长度为 \(j\) 的贡献和,则:

  • 插入一个同色元素:\(f_{i, j} \to f_{i + 1, 1 \sim j}\)
  • 插入一个不同色元素:\(f_{i, j} \times (k - j + 1) \to f_{i + 1, j + 1}\)

\(f_{0, l}\) 扩展左边,\(f_{0, r}\) 扩展右边,最后合并即可,注意转移过程中第二维 \(< k\)

\(a_{1 \sim m}\) 中不存在相同元素,不难发现此时对于任意 \(a_{1 \sim m}\) 答案都是一样的,考虑对所有长度为 \(m\) 的序列 \(a\) 计数,最后将方案数除以 \(\binom{k}{m} \times m!\) 即可。

在前面 DP 的基础上再设 \(g_{i, j}\) 表示长度为 \(m\) 的不同色区间的数量,转移一样,只要在 \(j \ge m\) 时令 \(g_{i, j} \gets f_{i, j}\) 即可。

使用前缀和优化 DP,时间复杂度 \(O(nk)\)

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

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

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 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 int check() {
    set<int> st;

    for (int i = 1, j = 1; i <= m; ++i) {
        for (; st.find(a[i]) != st.end(); ++j)
            st.erase(a[j]);

        st.emplace(a[i]);

        if (st.size() == k)
            return 2;
    }

    return st.size() == m;
}

inline int solve1() {
    set<int> st;
    int len = 0;

    while (st.find(a[len + 1]) == st.end())
        st.emplace(a[++len]);

    fill(f[0], f[0] + len + 1, 1), st.clear(), len = 0;

    while (st.find(a[m - len]) == st.end())
        st.emplace(a[m - len]), ++len;

    fill(g[0], g[0] + len + 1, 1);

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

        for (int j = k; ~j; --j)
            f[i][j] = add(f[i][j], f[i][j + 1]), g[i][j] = add(g[i][j], g[i][j + 1]);
    }

    int ans = 0;

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

    return ans;
}

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

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

            if (j >= m)
                g[i][j] = add(g[i][j], f[i][j]);
        }

        for (int j = k; ~j; --j)
            f[i][j] = add(f[i][j], f[i][j + 1]), g[i][j] = add(g[i][j], g[i][j + 1]);
    }

    int ans = g[n][0];

    for (int i = k; i >= k - m + 1; --i)
        ans = 1ll * ans * mi(i, Mod - 2) % Mod;

    return ans;
}

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

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

    int flag = check(), all = 1ll * mi(k, n - m) * (n - m + 1) % Mod;
    printf("%d", flag == 2 ? all : dec(all, flag ? solve2() : solve1()));
    return 0;
}

AT_joisc2012_kangaroo カンガルー

\(n\) 个点,每个点有两个权值 \(a_i, b_i\) ,保证 \(a_i > b_i\)\(i\) 可以接在 \(j\) 底下当且仅当 \(j\) 是叶子且 \(b_j > a_i\)

不难发现最终局面形如若干条链,求不存在两条链可以拼接时最终局面的方案数。

\(n \le 300\)

考虑将 \(a_i, b_i\) 分开来看,将 \(2n\) 个数降序排序,相等时 \(a_i\) 在前。\(a_i\) 视为右括号,\(b_i\) 视为左括号,则拼接就相当于匹配一对括号。

不难发现最终状态下未匹配的括号可以分为两段,左边为若干个右括号,右边为若干个左括号。设 \(f_{i, j, 0/1}\) 表示前 \(i\) 个括号中存在 \(j\) 个未匹配的左括号、当前最终态是否切换到右半段的方案数,转移不难做到 \(O(n^2)\)

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

pair<int, int> a[N << 1];

int f[2][N][2];

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", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &a[i].first, &a[i + n].first), a[i].second = 1, a[i + n].second = 0;

    sort(a + 1, a + n * 2 + 1, greater<pair<int, int> >());
    f[0][0][0] = 1;

    for (int i = 1, cnt = 0; i <= n * 2; cnt += !a[i++].second) {
        memset(f[i & 1], 0, sizeof(f[i & 1]));

        for (int j = 0; j <= cnt; ++j) {
            if (a[i].second) {
                if (j) {
                    f[i & 1][j - 1][0] = add(f[i & 1][j - 1][0], 1ll * f[~i & 1][j][0] * j % Mod);
                    f[i & 1][j - 1][1] = add(f[i & 1][j - 1][1], 1ll * f[~i & 1][j][1] * j % Mod);
                }

                f[i & 1][j][0] = add(f[i & 1][j][0], f[~i & 1][j][0]);
            } else {
                f[i & 1][j + 1][0] = add(f[i & 1][j + 1][0], f[~i & 1][j][0]);
                f[i & 1][j + 1][1] = add(f[i & 1][j + 1][1], f[~i & 1][j][1]);
                f[i & 1][j][1] = add(f[i & 1][j][1], add(f[~i & 1][j][0], f[~i & 1][j][1]));
            }
        }
    }

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

P9385 [THUPC 2023 决赛] 阴阳阵

给定 \(n, m, P\) ,求满足以下条件的图的数量 \(\bmod P\)

  • \(n + m\) 个点,其中 \(1 \sim n\) 为白点,\(n + 1 \sim n + m\) 为黑点。
  • 每个点恰有一条出边,其中黑点的出边必须指向白点,白点的出边没有限制。
  • 不难发现图为有向基环树森林,要求每个环上黑点数量与白点数量的乘积为偶数。

\(n, m \le 2000\)

考虑每次从编号最小且没定出边的点开始,一路确定出边,直到到达一个已经确定出边的点。不难发现每次要么新加一条链到连通块上,要么新加一个 \(\rho\) 状连通块,而 \(\rho\) 又可以拆分为链部分和环部分。

生成合法图的过程是分阶段的,设:

  • \(f_{i, j}\) 表示进行若干轮后,确定了 \(i\) 个白点和 \(j\) 个黑点的出边的方案数。
  • \(g_{i, j, 0/1, 0/1}\) 表示正在生成一条链,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在链上),同时记录钦定的这条链要接到的点的颜色、当前点的颜色。
  • \(h_{i, j, 0/1}\) 表示正在生成一个 \(\rho\) 的链部分,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在链上),同时记录当前点的颜色。
  • \(l_{i, j, 0/1, 0/1, 0/1, 0/1}\) 表示正在生成一个 \(\rho\) 的环部分,确定了 \(i\) 个白点和 \(j\) 个黑点的出边(不一定这 \(i + j\) 个点都在环上),同时记录环末尾的颜色、当前点颜色、环上的白点个数奇偶性、环上的黑点个数奇偶性。

转移就是在 DP 数组时间互相转移,有一些细节需要推导,时间复杂度 \(O(nm)\)

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

int f[N][N], g[N][N][2][2], h[N][N][2], l[N][N][2][2][2][2];

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() {
    scanf("%d%d%d", &n, &m, &Mod);
    f[0][0] = 1;

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

                    for (int x = 0; x <= 1; ++x)
                        if (!(b && x))
                            g[i + !x][j + x][a][x] = add(g[i + !x][j + x][a][x], 
                                1ll * g[i][j][a][b] * (x ? m - j : n - i) % Mod);
                }

            for (int a = 0; a <= 1; ++a) {
                if (!h[i][j][a])
                    continue;

                for (int x = 0; x <= 1; ++x)
                    if (!(a && x))
                        h[i + !x][j + x][x] = add(h[i + !x][j + x][x],
                            1ll * h[i][j][a] * (x ? m - j : n - i) % Mod);

                l[i][j][a][a][!a][a] = add(l[i][j][a][a][!a][a], h[i][j][a]);
            }

            for (int a = 0; a <= 1; ++a)
                for (int b = 0; b <= 1; ++b)
                    for (int c = 0; c <= 1; ++c)
                        for (int d = 0; d <= 1; ++d) {
                            if (!l[i][j][a][b][c][d])
                                continue;

                            if (!(a && b) && !(c && d))
                                f[i][j] = add(f[i][j], l[i][j][a][b][c][d]);

                            for (int x = 0; x <= 1; ++x)
                                if (!(b && x))
                                    l[i + !x][j + x][a][x][c ^ !x][d ^ x] = add(l[i + !x][j + x][a][x][c ^ !x][d ^ x], 
                                        1ll * l[i][j][a][b][c][d] * (x ? m - j : n - i) % Mod);
                        }

            if (i != n) {
                g[i + 1][j][0][0] = add(g[i + 1][j][0][0], 1ll * f[i][j] * i % Mod);
                g[i + 1][j][1][0] = add(g[i + 1][j][1][0], 1ll * f[i][j] * j % Mod);
                h[i + 1][j][0] = add(h[i + 1][j][0], f[i][j]);
            } else {
                g[i][j + 1][0][1] = add(g[i][j + 1][0][1], 1ll * f[i][j] * i % Mod);
                g[i][j + 1][1][1] = add(g[i][j + 1][1][1], 1ll * f[i][j] * j % Mod);
                h[i][j + 1][1] = add(h[i][j + 1][1], f[i][j]);
            }
        }

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

转移路线

对于一类简单的 DP 方程,可以考虑一条转移路线的贡献,转化为组合数的计算。

P3266 [JLOI2015] 骗我呢

给定 \(n, m\) ,求满足以下条件的 \(n \times m\) 的矩阵 \(a\) 数量 \(\bmod (10^9 + 7)\)

  • 每个元素为 \(0 \sim m\) 之间的整数。
  • 对于所有 \(1 \le i \le n\)\(1 \le j < m\) ,满足 \(a_{i, j} < a_{i, j + 1}\)
  • 对于所有 \(1 < i \le n\)\(1 \le j < m\) ,满足 \(a_{i, j} < a_{i - 1, j + 1}\)

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

不难发现一行 \(0 \sim m\) 中只有一个数没出现,因此可以用没出现的数表示一行的状态。

\(f_{i, j}\) 表示第 \(i\) 行没有 \(j\) 的方案数,则 \(f_{i, j} = \sum_{k = 1}^{j + 1} f_{i - 1, k} = f_{i, j - 1} + f_{i - 1, j + 1}\)

考虑一条转移路线的组合意义,答案相当于从 \((0, 0)\) 开始,每步可以向右或左上走,求走到 \((n, m + 1)\) 的方案数,并要求 \(x \le n\)\(y \le m + 1\)

这样形式还是不够清晰,考虑将第 \(i\) 行向右平移 \(i\) 个单位,则问题转化为每步可以向右或上走,求走到 \((n + m + 1, n)\) 的方案数,并要求不能触碰直线 \(y = x + 1\)\(y = x - (m + 2)\)

不难用格路计数的技巧解决。

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

int fac[N], inv[N], invfac[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 n < 0 || m < 0 || m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int calc(int n, int m, int l, int r) {
    auto flip = [](int &x, int &y, int k) {
        swap(x, y), x += k, y -= k;
    };
    
    int ans = C(n + m, m), x = n, y = m;
    
    while (x >= 0 && y >= 0) {
        flip(x, y, l), ans = dec(ans, C(x + y, y));
        flip(x, y, r), ans = add(ans, C(x + y, y));
    }
    
    x = n, y = m;
    
    while (x >= 0 && y >= 0) {
        flip(x, y, r), ans = dec(ans, C(x + y, y));
        flip(x, y, l), ans = add(ans, C(x + y, y));
    }
    
    return ans;
}

signed main() {
	prework();
	scanf("%d%d", &n, &m);
	printf("%d", calc(n + m + 1, n, -1, m + 2));
	return 0;
}

P6811 「MCOI-02」Build Battle 建筑大师

给定 \(n\)\(q\) 次询问,每次给定 \(m\) ,求序列 \(a_{1 \sim n}\) 本质不同子序列的数量 \(\bmod (10^9 + 7)\) ,其中 \(a_i = (i - 1) \bmod m + 1\)

\(n, q \le 10^6\)

先考虑一般序列本质不同子序列的数量的求法,记上一个 \(a_i\) 出现的位置为 \(lst_i\) ,则 \(f_i \gets s_{i - 1} - s_{lst_i - 1}\) ,其中 \(s\)\(f\) 的前缀和。

考虑原问题,由于 \(lst_i = i - m\) ,因此得到 \(f_i = s_{i - 1} - s_{i - m - 1}\) 。又由于 \(s_i = s_{i - 1} + f_i\) ,因此得到 \(s_i = 2 s_{i - 1} - s_{i - m - 1}\)

考虑一条转移路线的组合意义,有一个初始为 \(1\) 的变量 \(v\) ,有两种行动方式:

  • \(x \to x + 1\)\(v \gets 2v\)
  • \(x \to x + m + 1\)\(v \gets -v\)

可以得到:

\[s_n = \sum_{i = 0}^{\lfloor \frac{n}{m + 1} \rfloor} 2^{n - (m + 1) i} \times (-1)^i \times \binom{n - (m + 1)i + i}{i} \]

预处理每个 \(m\) 的答案,时间复杂度 \(O(n \ln n)\)

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

int fac[N], inv[N], invfac[N], pw[N], ans[N];

int n, q;

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;
    }
    
    pw[0] = 1;
    
    for (int i = 1; i <= n; ++i)
        pw[i] = 2ll * pw[i - 1] % Mod;
}

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

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

signed main() {
    scanf("%d%d", &n, &q);
    prework(n);
    
    for (int i = 1; i <= n; ++i)
        for (int j = 0; j <= n / (i + 1); ++j)
            ans[i] = add(ans[i], 1ll * pw[n - (i + 1) * j] * sgn(j) % Mod * C(n - i * j, j) % Mod);
    
    while (q--) {
        int x;
        scanf("%d", &x);
        printf("%d\n", ans[x]);
    }
    
    return 0;
}

差分去重

CF2048G Kevin and Matrices

给定 \(n, m, v\) ,求元素均为 \(1 \sim v\) 之间的整数,且满足下面式子的 \(n \times m\) 的矩阵数量 \(\bmod 998244353\)

\[\min_{i = 1}^n \left( \max_{j = 1}^m a_{i, j} \right) \le \max_{j = 1}^m \left( \min_{i = 1}^n a_{i, j} \right) \]

\(n \times v \le 10^6\)\(m \le 10^9\)

考虑补集转化,统计左式大于右式的方案数。设 \(f(a, b)\) 表示左式 \(\ge a\) 、右式 \(\le b\) 的方案数,答案即为:

\[v^{nm} - \sum_{i = 1}^{v - 1} f(i + 1, i) + \sum_{i = 1}^{v - 2} f(i + 2, i) \]

下面考虑计算 \(f\) ,考虑对行容斥,钦定 \(i\) 行均 \(< a\) ,则每一列独立,列的方案数即为总方案数减去 \(> b\) 的方案数,因此得到:

\[f(a, b) = \sum_{i = 0}^n (-1)^i \binom{n}{i} (v^{n - i} (a - 1)^i - (v - b)^{n - i} (a - b - 1)^i)^m \]

直接计算即可做到 \(O(nv \log (nm))\)

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

int fac[N], inv[N], invfac[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;
}

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

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;
}

inline int f(int x, int y) {
    int res = 0;

    for (int i = 0; i <= n; ++i)
        res = add(res, 1ll * sgn(i) * C(n, i) % Mod * mi(dec(1ll * mi(v, n - i) * mi(x - 1, i) % Mod, 
            1ll * mi(v - y, n - i) * mi(max(x - y - 1, 0), i) % Mod), m) % Mod);

    return res;
}

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

    while (T--) {
        scanf("%d%d%d", &n, &m, &v);
        int ans = 0;

        for (int i = 1; i <= v; ++i)
            ans = add(ans, dec(add(f(i, i), f(i + 1, i - 1)), add(f(i + 1, i), f(i, i - 1))));

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

    return 0;
}

P10592 BZOJ4361 isn

给定序列 \(a_{1 \sim n}\) ,若序列 \(a\) 不是不降序列,则必须从中删去一个数。这一操作将被不断执行,直到序列非降为止。

求不同的操作方案数 \(\bmod (10^9 + 7)\) ,操作方案不同当且仅当删除的顺序或次数不同。

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

若最后剩下 \(l\) 个数,则操作方式有 \((n - l)!\) 种,考虑对最后的不降序列计数。

统计恰好不降比较困难,考虑统计长度为 \(l\) 的不降序列,再减去长为 \(l + 1\) 的不降序列数量的 \(l\) 倍,最后乘上 \((n - l)!\) 种操作方式即可。

\(f_{i, j}\) 表示 \(a_i\) 结尾长为 \(j\) 的不降子序列数量,不难树状数组优化到 \(O(n^2 \log n)\)

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

int a[N], fac[N], f[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;
}

struct BIT {
    int c[N];

    inline void update(int x, int k) {
        for (; x <= m; 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;
    }
} bit[N];

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

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), vec.emplace_back(a[i]);

    sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
    m = vec.size();

    for (int i = 1; i <= n; ++i)
        a[i] = lower_bound(vec.begin(), vec.end(), a[i]) - vec.begin() + 1;

    fac[0] = 1;

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

    bit[0].update(1, 1);

    for (int i = 1; i <= n; ++i)
        for (int j = i; j; --j)
            bit[j].update(a[i], bit[j - 1].query(a[i]));

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

    int ans = 0;

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

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

[AGC013D] Piling Up

\(n\) 个颜色为黑或白的球,但是每种球的数量未知。

进行 \(m\) 次操作,每次先拿出一个球,再放入黑球、白球各一个,再拿出一个球。

将拿出的球按顺序构成一个序列,对于所有可能的初始状态,求不同的序列数量 \(\bmod (10^9 + 7)\)

\(n, m \le 3000\)

先简化操作,一共有四种:

  • 拿出黑球,放入黑、白球,拿出黑球。
    • 白球数量的变化量:\(0 \to 0 \to 1 \to 1\)
  • 拿出黑球,放入黑、白球,拿出白球。
    • 白球数量的变化量:\(0 \to 0 \to 1 \to 0\)
  • 拿出白球,放入黑、白球,拿出黑球。
    • 白球数量的变化量:\(0 \to -1 \to 0 \to 0\)
  • 拿出白球,放入黑、白球,拿出白球。
    • 白球数量的变化量:\(0 \to -1 \to 0 \to -1\)

不难发现四种操作对应插入序列的元素是不同的,并且白球变化量的变化也是不同的,因此可以考虑对白球变化量的变化序列计数。

\(f_{i, j}\) 表示操作 \(i\) 次后白球数量为 \(j\) 时可能的最终序列数量,但是初始状态并不好设计。若将每个初始状态的 DP 值设为 \(1\) ,则会算重。

发现白球的变化可以用一条折线表示,而折线会算重当且仅当两条折线上下平移后会重合。因此只要用 \(n\) 的答案减去 \(n - 1\) 的答案即可,时间复杂度 \(O(nm)\)

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

int f[N][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 int solve(int n) {
    memset(f, 0, sizeof(f));
    fill(f[0], f[0] + n + 1, 1);

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

    int res = 0;

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

    return res;
}

signed main() {
    scanf("%d%d", &n, &m);
    printf("%d", dec(solve(n), solve(n - 1)));
    return 0;
}

还有一种方法是只统计触碰到 \(0\) 的折线,时间复杂度同样为 \(O(nm)\)

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

int f[N][N][2];

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

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

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

                if (j) {
                    if (j == 1) {
                        if (k) {
                            f[i][j][k] = add(f[i][j][k], add(f[i - 1][j][0], f[i - 1][j][1]));
                            f[i][j - 1][k] = add(f[i][j - 1][k], add(f[i - 1][j][0], f[i - 1][j][1]));
                        }
                    } else {
                        f[i][j][k] = add(f[i][j][k], f[i - 1][j][k]);
                        f[i][j - 1][k] = add(f[i][j - 1][k], f[i - 1][j][k]);
                    }
                }
            }

    int ans = 0;

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

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

调整适应

在保证合法的情况下,对于涉及到多个元素的限制,可以让某一些元素任意取,将一个点的决策适应与其它部分的决策。但这可能会算重,容斥掉其他限制即可。

P3214 [HNOI2011] 卡农

对于 \(1 \sim n\) 个数组成的 \(2^n - 1\) 个非空集合,需要从中选出 \(m\) 个集合满足:

  • 所选集合互不相同。
  • 每个数在集合中的出现次数和为偶数。

求方案数 \(\bmod (10^8 + 7)\)

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

先将无序转化为有序,最后除以 \(m!\) 即可。

\(f_i\) 表示选了 \(i\) 个集合的方案数,注意此时钦定集合是有顺序的。

先解决出现次数为偶数的限制,考虑让前 \(i - 1\) 个集合随便选,那么第 \(i\) 个集合就是确定的,方案数为 \(\binom{2^n - 1}{i - 1} \times (i - 1)!\)

再考虑集合非空的限制,减去 \(f_{i - 1}\) 即可。

最后考虑集合互异的性质,减去 \(f_{i - 2} \times (i - 1) \times (2^n - i + 1)\) 即可。

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

int f[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 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);
    int pw = mi(2, n), fac = 1;
    f[0] = 1;

    for (int i = 1, mul = 1; i <= m; ++i) {
        f[i] = dec(mul, f[i - 1]);

        if (i >= 2)
            f[i] = dec(f[i], 1ll * f[i - 2] * (i - 1) % Mod * dec(pw, i - 1) % Mod);

        mul = 1ll * mul * dec(pw, i) % Mod, fac = 1ll * fac * i % Mod;
    }

    printf("%d", 1ll * f[m] * mi(fac, Mod - 2) % Mod);
    return 0;
}

化子问题

通常是考虑某处的填法,然后将问题拆分为独立的子问题。

P9493 「SFCOI-3」进行一个列的排

给出 \(a_{0 \sim n - 1}\) ,求满足以下条件的 \(0 \sim n - 1\) 的排列数量:对于所有 \(i = 0, 1, \cdots, n - 1\) ,存在一个长度为 \(a_i\) 的子区间满足区间 \(\mathrm{mex}\)\(i\)

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

\(n - 1\) 开始考虑,不难发现 \(\mathrm{mex} = n - 1\) 的区间一定包含 \(0 \sim n - 2\) ,从而 \(a_{n - 1} = n - 2\) ,此时 \(n - 1\) 必须排在序列的开头或末尾,从而可以化子问题。

\(f_{l, r}\) 表示考虑了 \(r - l + 1 \sim n - 1\) 的限制,此时剩余的区间为 \(l, r\) 的方案数,转移枚举当前数放在开头或是结尾即可。

注意特判无解的情况,时间复杂度 \(O(n^2)\) ,空间可以滚动到 \(O(n)\)

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

int a[N], f[2][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;
}

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

    while (T--) {
        scanf("%d", &n);
        bool flag = (n > 1);

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

        if (!flag) {
            puts("0");
            continue;
        }

        memset(f[n & 1] + 1, 0, sizeof(int) * n), f[n & 1][1] = 1;

        for (int len = n; len >= 2; --len) {
            memset(f[~len & 1] + 1, 0, sizeof(int) * n);

            for (int l = 1, r = len; r <= n; ++l, ++r) {
                if (r - 1 >= a[len])
                    f[~len & 1][l] = add(f[~len & 1][l], f[len & 1][l]);

                if (n - l >= a[len])
                    f[~len & 1][l + 1] = add(f[~len & 1][l + 1], f[len & 1][l]);
            }
        }

        int ans = 0;

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

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

    return 0;
}

SP3734 PERIODNI - Periodni

给定一个 \(n\) 列的表格,第 \(i\) 列自底向上有 \(h_i\) 个格子,求在其中放置 \(k\) 个车的方案数 \(\bmod (10^9 + 7)\)

\(n, k \le 500\)

建立小根笛卡尔树,则每个点的管辖范围是子树内高为它的一个极长子矩形,为了防止不同矩形的决策互相影响,考虑删去这个极长子矩形再递归儿子。

\(f_{u, i}\) 表示 \(u\) 子树内放 \(i\) 个车的方案数,考虑转移:

  • 首先求出 \(g_i = \sum_{j = 0}^i f_{lc_u, j} \times f_{rc_u, i - j}\) ,即递归到左右儿子选取的方案数。
  • 接下来枚举 \(u\) 矩形内放的点的数量,设该矩形高、宽分别为 \(H, W\) ,有 \(f_{u, i} = \sum_{j = 0}^i g_{i - j} \times \binom{H}{j} \times \binom{W - (i - j)}{j} \times j!\)

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

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

int a[N], fac[N], inv[N], invfac[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;
}

namespace CST {
int lc[N], rc[N], sta[N];

int root;

inline void build() {
    for (int i = 1, top = 0; i <= n; ++i) {
        int k = top;

        while (k && a[sta[k]] > a[i])
            --k;

        if (k)
            rc[sta[k]] = i;
        else
            root = i;

        if (k < top)
            lc[i] = sta[k + 1];

        sta[top = ++k] = i;
    }
}

vector<int> dfs(int x, int l, int r, int fa) {
    if (!x)
        return {1};

    int H = a[x] - a[fa], W = r - l + 1;
    vector<int> f(W + 1), g(W + 1), gl = dfs(lc[x], l, x - 1, x), gr = dfs(rc[x], x + 1, r, x);

    for (int i = 0; i <= x - l; ++i)
        for (int j = 0; j <= r - x; ++j)
            g[i + j] = add(g[i + j], 1ll * gl[i] * gr[j] % Mod);

    for (int i = 0; i <= W; ++i)
        for (int j = 0; j <= i; ++j)
            f[i] = add(f[i], 1ll * g[i - j] * C(H, j) % Mod * C(W - (i - j), j) % Mod * fac[j] % Mod);

    return f;
}
} // namespace CST

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

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

    CST::build(), prework();
    printf("%d", CST::dfs(CST::root, 1, n, 0)[m]);
    return 0;
}

[AGC026D] Histogram Coloring

给定 \(n\) 列的网格,第 \(i\) 列高为将每 \(h_i\) ,将每个格子染色成红色或蓝色,使得每个 \(2 \times 2\) 的区域都恰好有两个蓝格子和两个红格子,求方案数。

\(n \le 100\)

先考虑 \(h\) 全相等的情况,若最底行存在两个相邻的同色位置,则上面的填色方案是确定的,否则每一行都有两种方法(红蓝交替)。

考虑建立小根笛卡尔树,设 \(f_u\) 表示 \(u\) 子树红蓝交替的方案数,\(g_u\) 表示 \(u\) 子树不为红蓝交替的方案数。

为了防止不同矩形的决策互相影响,考虑删去这个极长子矩形再递归儿子,这样就可以化子问题了。

先考虑 \(f\) 的转移,则要求左右子树均为红蓝交替,即 \(f_u = f_{lc_u} \times f_{rc_u} \times 2^h\) ,其中 \(h\) 表示 \(u\) 管辖的子矩形的高。

再考虑 \(g\) 的转移,不难发现只要考虑最高的行的情况就确定了下面行的情况。若一侧为交替,则最高行有两种填色方案,否则只有一种方案。分类讨论贡献系数:

  • 若左右子树均为红蓝交替,则对于 \((u - 1, u, u + 1)\) 三个位置,仅有红蓝红、蓝红蓝不合法(不满足同色),贡献系数为 \(6\)
  • 若恰有一个子树为红蓝交替,则 \(u\) 位置和红蓝交替的一侧各有两种方案,贡献系数为 \(4\)
  • 若没有子树红蓝交替,则仅 \(u\) 位置有两种方案,贡献系数为 \(2\)

\(g\) 的转移需要特殊处理一下区间最小值在端点的情况。

忽略快速幂的复杂度,时间复杂度 \(O(n)\)

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

int h[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 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 CST {
int lc[N], rc[N], sta[N], f[N], g[N];

int root;

inline void build() {
    for (int i = 1, top = 0; i <= n; ++i) {
        int k = top;

        while (k && h[sta[k]] > h[i])
            --k;

        if (k)
            rc[sta[k]] = i;
        else
            root = i;

        if (k < top)
            lc[i] = sta[k + 1];

        sta[top = ++k] = i;
    }
}

void solve(int x, int l, int r, int fa) {
    if (!x)
        return;

    solve(lc[x], l, x - 1, x), solve(rc[x], x + 1, r, x);
    f[x] = 1ll * (lc[x] ? f[lc[x]] : 1) * (rc[x] ? f[rc[x]] : 1) % Mod * mi(2, h[x] - h[fa]) % Mod;

    if (l == r)
        return;
    else if (x == l)
        g[x] = 2ll * add(f[rc[x]], g[rc[x]]) % Mod;
    else if (x == r)
        g[x] = 2ll * add(f[lc[x]], g[lc[x]]) % Mod;
    else
        g[x] = add(add(6ll * f[lc[x]] * f[rc[x]] % Mod, 4ll * f[lc[x]] * g[rc[x]] % Mod),
            add(4ll * g[lc[x]] * f[rc[x]] % Mod, 2ll * g[lc[x]] * g[rc[x]] % Mod));
}
} // namespace CST

using namespace CST;

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

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

    build(), solve(root, 1, n, 0);
    printf("%d", add(f[root], g[root]));
    return 0;
}

减少状态数

CF924F Minimal Subset Difference

\(f(n)\) 表示在 \(n\) 的所有相邻数位之间插入加号或减号,最终得到的算式的值的绝对值的最小值。

\(T\) 组询问,每次给定 \(l, r, k\) ,求 \(n \in [l, r]\)\(f(n) \le k\)\(n\) 的数量。

\(T \le 5 \times 10^4\)\(1 \le l \le r \le 10^{18}\)\(0 \le k \le 9\)

先考虑如何求 \(f\) ,只要做背包即可。

考虑背包的值域,有结论:对于值域为 \([0, w]\) 的序列,最小化 \(f(n)\) 时最大前缀和的绝对值 \(\le w(w - 1)\)

由于正负是对称的,因此只要记录 \([0, 72]\) 即可,转移就是 \(f_j \to f_{j + n_i}\) 以及 \(f_j \to f_{|j - n_i|}\)

暴搜可以发现对于所有 \(n\) ,有效的背包状态只有 \(10^4\) 级别,因此可以压到一个 __int128 里用 map 存储。

预处理 \(g_{i, S, k}\) 表示无最高位限制,还要填 \(i\) 位,此时背包状态为 \(S\)\(f(n) \le k\) 方案数,直接数位 DP 即可。

注意背包时 \(j + n_i\)\(j - n_i\) 的部分用位运算,\(n_i - j\) 的部分暴力枚举 \(j \in [0, n_i]\) 可以优化复杂度。注意 __int128 的常数。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e4 + 7;

map<__int128, int> mp;

__int128 num[N], trans[N][10];
ll f[20][N][10];
int digit[20];

__int128 all = ((__int128)1 << 73) - 1;
int tot;

void dfs1(int d, int u) {
    if (~f[d][u][0])
        return;
    else if (!d) {
        for (int i = 0; i <= 9; ++i)
            f[d][u][i] = num[u] & ((1 << (i + 1)) - 1) ? 1 : 0;

        return;
    }

    memset(f[d][u], 0, sizeof(f[d][u]));

    for (int i = 0; i <= 9; ++i) {
        if (trans[u][i] == -1) {
            __int128 v = ((num[u] << i) | (num[u] >> i)) & all;

            for (int j = 0; j <= i; ++j)
                v |= (num[u] >> j & 1) << (i - j);

            if (mp.find(v) == mp.end())
                num[mp[v] = ++tot] = v;

            trans[u][i] = mp[v];
        }

        dfs1(d - 1, trans[u][i]);

        for (int j = 0; j <= 9; ++j)
            f[d][u][j] += f[d - 1][trans[u][i]][j];
    }
}

ll dfs2(int x, bool lead, int s, int k) {
    if (!lead)
        return f[x][s][k];
    else if (!x)
        return num[s] & ((1 << (k + 1)) - 1) ? 1 : 0;

    ll res = 0;

    for (int i = 0, high = lead ? digit[x] : 9; i <= high; ++i)
        res += dfs2(x - 1, lead && i == high, trans[s][i], k);

    return res;
}

inline ll solve(ll n, int k) {
    int len = 0;

    do
        digit[++len] = n % 10, n /= 10;
    while (n);

    return dfs2(len, true, 1, k);
}

signed main() {
    memset(f, -1, sizeof(f)), memset(trans, -1, sizeof(trans));
    num[mp[1] = ++tot] = 1;

    for (int i = 0; i < 20; ++i)
        dfs1(i, 1);

    int T;
    scanf("%d", &T);

    while (T--) {
        ll l, r;
        int k;
        scanf("%lld%lld%d", &l, &r, &k);
        printf("%lld\n", solve(r, k) - solve(l - 1, k));
    }

    return 0;
}

优化状态

[ARC119F] AtCoder Express 3

\(n + 1\) 个点,标号为 \(0 \sim n\) ,其中 \(i\)\(i + 1\) 之间有一条边。

\(1 \sim n - 1\) 的颜色为黑色或白色,每个点会向其同色前驱、后继连双向边,若不存在则向 \(0 / n\) 连边。

已知一些点的颜色,求有多少种染色方法使得 \(0\)\(n\) 的最短路 \(\le m\)

\(n, m \le 4000\)

\(f_{i, j, k, 0/1}\) 表示前 \(i\) 个点,到最后一个白色/黑色点的最短路为 \(j, k\) ,第 \(i\) 个点的颜色为白或黑。若 \(i\) 为白色,则加一个白点后 \(j \to j + 1\) ,加一个黑点后 \(j \to \min(k + 2, j), k \to \min(j + 1, k + 1)\)

直接转移是 \(O(nm^2)\) 的,发现状态数过大,首先考虑优化状态数。

设当前点为白色,若 \(j > k + 2\) ,则加一个黑点后 \(j\) 就会变成 \(k + 2\) ,加一个白点后 \(j\) 仍然更大,不会对答案产生贡献,因此只需保留 \(|j - k| \le 2\) 的状态即可。

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

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

int f[2][N][5][2];

char str[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 int& F(int op, int j, int k, int c) {
    return f[op][min(j, k + 2)][min(k, j + 2) - min(j, k + 2) + 2][c];
}

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

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

        for (int j = 0; j <= m + 2; ++j)
            for (int k = j - 2; k <= j + 2; ++k) {
                auto update = [](int &x, int y) {
                    x = add(x, y);
                };

                if (str[i + 1] != 'B') {
                    update(F(~i & 1, j + 1, k, 0), F(i & 1, j, k, 0));
                    update(F(~i & 1, min(j + 1, k + 1), min(j + 2, k), 0), F(i & 1, j, k, 1));
                }

                if (str[i + 1] != 'A') {
                    update(F(~i & 1, j, k + 1, 1), F(i & 1, j, k, 1));
                    update(F(~i & 1, min(j, k + 2), min(j + 1, k + 1), 1), F(i & 1, j, k, 0));
                }
            }
    }

    int ans = 0;

    for (int i = 0; i <= m + 2; ++i)
        for (int j = i - 2; j <= i + 2; ++j)
            if (min(i, j) + 1 <= m)
                ans = add(ans, add(F(n & 1, i, j, 0), F(n & 1, i, j, 1)));

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

P9084 [PA 2018] Skwarki

对于一个排列 \(P = p_{1 \sim n}\) ,重复以下操作直到 \(P\) 只剩一个数:将 \(P\) 替换为 \(P\) 的所有局部最大值保持相对顺序形成的数组。

其中 \(p_i\) 是局部最大值当且仅当满足以下两个条件:

  • \(i = 1\)\(p_i > p_{i - 1}\)
  • \(i = n\)\(p_i > p_{i + 1}\)

定义 \(f(P)\) 为上述操作的重复次数。

给定 \(n, k, m\) ,求 \(f(p_{1 \sim n}) = k\) 的长度为 \(n\) 的排列数量 \(\bmod m\)

\(n \le 5000\)

考虑建立大根笛卡尔树,则一个点会被保留当且仅当其左右儿子均存在,注意特殊处理一下首尾元素的情况,而删去不会保留的点后对每个点儿子的存在性是好维护的。

考虑 DP,设 \(f_{i, j}\) 表示长为 \(i\) 的排列,需要删 \(j\) 次删空,已经确定一个边界的方案数,\(g_{i, j}\) 表示边界的均未确定的方案数。讨论左右子树的存在性,以及根被删除的时间,有转移:

\[f_{i, j} = f_{i - 1, j} + g_{i - 1, j - 1} + \sum_{k = 1}^{i - 2} \sum_x \sum_y [\max \{ x, y, y + 1 \}] \times \binom{i - 1}{k} \times f_{k, x} \times g_{i - 1 - k, y} \\ g_{i, j} = 2 \times g_{i - 1, j} + \sum_{k = 1}^{i - 2} \sum_x \sum_y [\max \{ x, y, \min(x, y) + 1 \}] \times \binom{i - 1}{k} \times g_{k, x} \times g_{i - 1 - k, y} \]

可以前缀和优化到 \(O(n^3)\) 。注意到一次不会保留相邻的元素,因此 \(k > \lfloor \log n \rfloor + 1\) 时无解,因此第二维只需保留 \(O(\log n)\) 级别,时间复杂度 \(O(n^2 \log n)\)

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

int C[N][N], f[N][LOGN], g[N][LOGN], sf[N][LOGN], sg[N][LOGN];

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() {
    scanf("%d%d%d", &n, &m, &Mod);

    if (m > __lg(n) + 1)
        return puts("0"), 0;

    C[0][0] = 1;

    for (int i = 1; i <= n; ++i) {
        C[i][0] = 1;

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

    fill(sf[1] + 1, sf[1] + LOGN, f[1][1] = 1);
    fill(sg[1] + 1, sg[1] + LOGN, g[1][1] = 1);

    for (int i = 2; i <= n; ++i) {
        for (int j = 1; j <= __lg(i) + 1; ++j) {
            g[i][j] = 2ll * g[i - 1][j] % Mod;

            for (int k = 1; k < i - 1; ++k) {
                g[i][j] = add(g[i][j], 2ll * C[i - 1][k] * sg[k][j - 1] % Mod * g[i - 1 - k][j] % Mod);
                g[i][j] = add(g[i][j], 1ll * C[i - 1][k] * g[k][j - 1] % Mod * g[i - 1 - k][j - 1] % Mod);
            }

            f[i][j] = add(f[i - 1][j], g[i - 1][j - 1]);

            for (int k = 1; k < i - 1; ++k) {
                f[i][j] = add(f[i][j], 1ll * C[i - 1][k] * sf[k][j] % Mod * g[i - 1 - k][j - 1] % Mod);

                if (j >= 2)
                    f[i][j] = add(f[i][j], 1ll * C[i - 1][k] * f[k][j] % Mod * sg[i - 1 - k][j - 2] % Mod);
            }
        }

        for (int j = 1; j < LOGN; ++j)
            sf[i][j] = add(sf[i][j - 1], f[i][j]), sg[i][j] = add(sg[i][j - 1], g[i][j]);
    }

    int ans = 2ll * f[n - 1][m] % Mod;

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

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

AT_joisc2012_kangaroo カンガルー

\(n\) 个点,每个点有两个权值 \(a_i, b_i\) ,保证 \(a_i > b_i\)\(i\) 可以接在 \(j\) 底下当且仅当 \(j\) 是叶子且 \(b_j > a_i\)

不难发现最终局面形如若干条链,求不存在两条链可以拼接时最终局面的方案数。

\(n \le 300\)

先将所有点按 \(a\) 排序,这样每条链的 \(a\) 都是递减的,记 \(g_i\) 表示 \(1 \sim i - 1\) 中满足 \(b_j > a_i\) 的点的数量。

\(f_{i, j, k}\) 表示考虑了前 \(i\) 个点、形成了 \(j\) 条链、有 \(k\) 条链非法的方案数,其中链非法定义为下面还能接目前存在的链,考虑转移:

  • 将第 \(i\) 个点接在非法链下面:\(f_{i, j, k - 1} \gets f_{i - 1, j, k} \times k\) ,因为 \(i\) 底下显然不能接现有的链,所以非法链数量减少 \(1\)
  • 将第 \(i\) 个点接在合法链下面:\(f_{i, j, k} \gets f_{i - 1, j, k} \times (g_i - (i - 1 - j) - k)\) ,这里 \(g_i - (i - 1 - j)\) 表示可以接 \(i\) 的链的数量,这是因为链上的每个非叶子作为单点时都能接 \(i\) 。而每个非叶子底下一定能接 \(\le i - 1\) ,因此也一定能接 \(i\) ,需要减去 \(k\)
  • 令第 \(i\) 个点成为单点:\(f_{i, j + 1, g_i - (i - 1 - j)} \gets f_{i - 1, j, k}\) ,这是因为非法链一定能接 \(i\)

这里的 \(g\) 实际就是为了优化 DP 的状态维,通过 \(g\) 可以直接算出可以接 \(i\) 的链的数量。

答案即为 \(\sum_{i = 1}^n f_{n, i, 0}\) ,时间复杂度 \(O(n^3)\)

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

pair<int, int> a[N];

int f[2][N][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;
}

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

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &a[i].first, &a[i].second);

    sort(a + 1, a + n + 1, greater<pair<int, int> >());

    for (int i = 1; i <= n; ++i)
        for (int j = 1; j < i; ++j)
            g[i] += (a[j].second > a[i].first);

    f[0][0][0] = 1;

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

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

                f[i & 1][j][k] = add(f[i & 1][j][k], 1ll * f[~i & 1][j][k] * (g[i] - (i - 1 - j) - k) % Mod);
                f[i & 1][j + 1][g[i] - (i - 1 - j)] = add(f[i & 1][j + 1][g[i] - (i - 1 - j)], f[~i & 1][j][k]);
            }
    }

    int ans = 0;

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

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

mod 2 信息统计

  • \(\bmod 2\) 问题首选对消。
  • Lucas 定理:\([x \subseteq y] = \binom{y}{x} \bmod 2\)
  • 调整适应:拿出一个元素,控制结果的奇偶性。

CF1770F Koxia and Sequence

给定 \(n, X, Y\) ,对于所有满足 \(\sum_{i = 1}^n a_i = X\)\(\operatorname{or}_{i = 1}^n a_i = Y\) 的长度为 \(n\) 的序列 \(a_{1 \sim n}\) ,求 \(\oplus_{i = 1}^n a_i\) 的异或和。

\(n \le 2^{40}\)\(X \le 2^{60}\)\(Y \le 2^{20}\)

\(n\) 为偶数时,所有合法序列反转后仍然合法,若回文则异或和为 \(0\) ,因此答案为 \(0\)

否则只需统计 \(a_1\) 的异或和即可,因为 \(a_{2 \sim n}\) 是一个序列长度为偶数的子问题,答案为 \(0\)

考虑拆位,统计 \(a_i\) 每一位为 \(1\) 的方案数的奇偶性,以下钦定第 \(k\) 位为 \(1\)

考虑按位子集反演,设 \(f(z)\) 表示按位或为 \(z\) 子集的方案数,则按位或恰为 \(Y\) 的方案数即为 \(\sum_{y' \subseteq y} (-1)^{|y| - |y'|} f(y')\) 。由于最后统计的权值是异或,因此只需求出其模 \(2\) 意义下的值即可。由于 \(1 \equiv -1 \pmod{2}\) ,因此只要统计 \(\oplus_{z \subseteq y} f(z)\) 即可。

先写出答案的式子(注意此时若 \(2^k \not \subseteq z\) 的答案为 \(0\) ):

\[\bigoplus_{\sum_{i = 1}^n a_i = X} [a_1 - 2^k \subseteq z - 2^k] \times [a_2 \subseteq z] \times \cdots [a_n \subseteq z] \]

考虑逆用 Lucas 定理,得到:

\[\bigoplus_{\sum_{i = 1}^n a_i = X} \binom{z - 2^k}{a_1 - 2^k} \times \binom{z}{a_2} \times \cdots \times \binom{z}{a_n} \bmod 2 \]

然后可以范德蒙德卷积得到:

\[\binom{nz - 2^k}{X - 2^k} \bmod 2 \]

直接算即可做到 \(O(y \log y)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

ll n, X;
int Y;

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

    if (~n & 1)
        return puts("0"), 0;

    int ans = 0;

    for (int k = 1; k <= Y; k <<= 1) {
        if (~Y & k)
            continue;

        for (int z = 1; z <= Y; ++z)
            if ((z & k) && (z | Y) == Y) {
                ll u = n * z - k, v = X - k;

                if (0 <= v && v <= u && (v | u) == u)
                    ans ^= k;
            }
    }

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

CF979E Kuro and Topological Parity

\(n\) 个点,每个点为黑色或白色,有些点颜色未确定。

称一条经过的点为黑白相间的路径为好路径,如果一个图好路径的总数 \(\bmod 2 = p\) ,那么称这个图为好图。

可以在图上加入任意条边,满足其从编号小的点指向编号大的点,对于所有确定未确定颜色的点和加入边的方案,求好图的数量和 \(\bmod (10^9 + 7)\)

\(n \le 50\)

判定好图可以通过 DP 判定,枚举到当前点时,若前面存在一个有奇数条路径结尾的异色点,则可以通过控制该点是否连边达到控制好路径奇偶性的目的。否则由于单点也是路径,因此好路径总数的奇偶性会被改变。

因此考虑 DP of DP,设 \(f_{i, j, k, l}\) 表示前 \(i\) 个点,路径条数奇偶性 \(j\)\(k, l\) 表示是否存在奇数条路径结尾的黑点和白点,转移不难做到 \(O(n)\)

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

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

int n, p;

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, &p);

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

    pw[0] = 1;

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

    f[0][0][0][0] = 1;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= 1; ++j)
            for (int k = 0; k <= 1; ++k)
                for (int l = 0; l <= 1; ++l) {
                    if (a[i + 1] != 0) {
                        if (k) {
                            f[i + 1][j][k][l] = add(f[i + 1][j][k][l], 
                                1ll * f[i][j][k][l] * pw[i - 1] % Mod);
                            f[i + 1][j ^ 1][k][l | 1] = add(f[i + 1][j ^ 1][k][l | 1], 
                                1ll * f[i][j][k][l] * pw[i - 1] % Mod);
                        } else
                            f[i + 1][j ^ 1][k][l | 1] = add(f[i + 1][j ^ 1][k][l | 1], 
                                1ll * f[i][j][k][l] * pw[i] % Mod);
                    }

                    if (a[i + 1] != 1) {
                        if (l) {
                            f[i + 1][j][k][l] = add(f[i + 1][j][k][l], 
                                1ll * f[i][j][k][l] * pw[i - 1] % Mod);
                            f[i + 1][j ^ 1][k | 1][l] = add(f[i + 1][j ^ 1][k | 1][l], 
                                1ll * f[i][j][k][l] * pw[i - 1] % Mod);
                        } else
                            f[i + 1][j ^ 1][k | 1][l] = add(f[i + 1][j ^ 1][k | 1][l], 
                                1ll * f[i][j][k][l] * pw[i] % Mod);
                    }
                }

    int ans = 0;

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

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

[AGC050F] NAND Tree

给定一棵 \(n\) 个点的树,每个点上有权值 \(c_i \in \{ 0, 1 \}\)

定义一次操作为选择一条边,将这条边的两个端点缩成一个点,点权为两端点点权 NAND 的结果(先与再取反)。

不断操作直到只剩一个点,求使得最终点权为 \(1\) 的方案数模 \(2\) 的结果。

\(n \le 300\)

考虑对消:

  • 若相邻两个操作没有选择同一个点,则可以交换顺序使得结果不变,可以对消,总贡献为 \(0\)

  • 对于两条边 \((x, y), (y, z)\) ,若 \(c_x = c_y\) ,则两次操作交换位置后结果相同,可以对消,贡献为 \(0\)

  • 那么会有两种操作顺序:\(f(f(c_x, c_y), c_z)\)\(f(c_x, f(c_y, c_z))\) ,其中 \(f\) 即为 NAND 运算。若 \(c_x = c_y\) ,则结果相同,可以交换对消,贡献为 \(0\)

先考虑 \(2 \nmid n\) 的情况,此时操作次数(边数)为偶数。将操作相邻两两分组,则一组操作相当于:选择一条三个点的链 \((x, y, z)\) ,合并为一个单点,钦定权值为 \(c_x\)

由于相邻两组操作有交,因此可以考虑固定保留到最后的点为根,则每次会选择根的一个儿子作为 \(y\)\(y\) 的一个儿子作为 \(z\) ,方案数为拓扑序数量。

树上拓扑序数量为 \(\frac{n!}{\prod siz_i}\) ,由于只要求模 \(2\) 的值,因此只要记录分子、分母中 \(2\) 的次幂即可。可以 \(O(n)\) dfs 一遍求出 \(siz\) ,时间复杂度 \(O(n^2)\)

\(2 \mid n\) 时只要枚举操作的第一条边即可,时间复杂度 \(O(n^3)\)

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

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, nG;

int a[N], val[N], siz[N], id[N], sctz[N];
bool chosen[N];

int n, ans;

int dfs(int u, int f) {
    siz[u] = 1;
    
    for (int v : nG.e[u]) {
        if (v == f)
            continue;
        
        if (dfs(v, u) == -1)
            return -1;
        
        siz[u] += siz[v];
        
        if (!chosen[v]) {
            if (!chosen[u])
                chosen[u] = true;
            else
                return -1;
        }
    }
    
    return chosen[u] ^ 1;
}

inline void solve() {
    int all = 0;

    for (int i = 1; i <= n / 2; ++i)
        all += __builtin_ctz(i);

    for (int i = 1; i <= n; ++i) {
        if (!val[i])
            continue;

        memset(chosen + 1, false, sizeof(bool) * n);
        
        if (dfs(i, 0) == -1)
            continue;
        
        int cnt = all;
        
        for (int j = 1; j <= n; ++j)
            if (chosen[j])
                cnt -= __builtin_ctz(siz[j] >> 1);
        
        if (!cnt)
            ans ^= 1;
    }
}

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);
    }
    
    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i);

    for (int i = 1; i <= n; ++i)
        sctz[i] = sctz[i - 1] + __builtin_ctz(i);
    
    if (n & 1)
        memcpy(val + 1, a + 1, sizeof(int) * n), nG = G, solve();
    else {
        --n;
        
        for (int x = 1; x <= n; ++x)
            for (int y : G.e[x])
                if (x < y) {
                    for (int i = 1; i <= n + 1; ++i)
                        id[i] = i - (i > y);
                    
                    id[y] = x, nG.clear(n);
                    
                    for (int i = 1; i <= n + 1; ++i)
                        for (int j : G.e[i])
                            if (id[i] != id[j])
                                nG.insert(id[i], id[j]);
                    
                    for (int i = 1; i <= n + 1; ++i)
                        val[id[i]] = a[i];
                    
                    val[x] = !(a[x] & a[y]), solve();
                }
    }
    
    printf("%d", ans);
    return 0;
}

QOJ1261. Inv

给定 \(n, k\) ,求长度为 \(n\) 、逆序对数为 \(k\) 且满足 \(\forall i \in [1, n], p_{p_i} = i\) 的排列数量模 \(2\) 的值。

\(n \le 500\)

不难发现 \(p_{p_i} = i\) 等价于该排列与其逆排列相同,而该排列与逆排列拥有相同的逆序对数,这是因为原排列的每个逆序对 \((i, j)\) 与逆排列的每个逆序对 \((p_j, p_i)\) 一一对应。

因此答案即为长度为 \(n\) 、逆序对数为 \(i\) 的排列数量模 \(2\) 的值,因为原排列与逆排列不同的排列对消,不难背包 DP 做到 \(O(n^3)\)

考虑加强版:对于所有 \(k = 0, 1, \cdots, m\) 均求解以上问题,其中 \(n \le 10^9\)\(m \le \min(\binom{n}{2}, 5 \times 10^4)\)

写出答案的生成函数:

\[\begin{aligned} ans_{n, k} &= [x^k] \prod_{i = 1}^n \frac{1 - x^i}{1 - x} \\ &= [x^k] (1 - x)^{-n} \prod_{i = 1}^n (1 - x^i) \\ &= [x^k] \left( \sum_{i \ge 0} \binom{n - 1 + i}{i} x^i \right) \prod_{i = 1}^n (1 - x^i) \end{aligned} \]

由 Kummer 定理,\(\binom{n - 1 + i}{i} \bmod 2 = [i \operatorname{and} (n - 1) = 0]\) ,因此可以直接求出前一个多项式。

由于 \(1 - x^i \equiv 1 + x_i \pmod{2}\) ,因此算二者卷积可以通过做 \(n\) 次 01 背包求得。由于 \(k \le m\) ,因此只要算 \(\min(n, m)\) 次即可,时间复杂度 \(O(\frac{m^2}{\omega})\)

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

int f[N * N];

int n, k;

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

    f[0] = 1;

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

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

    printf("%d", f[k]);
    return 0;
}
posted @ 2025-07-25 21:26  wshcl  阅读(59)  评论(0)    收藏  举报