Loading

[Teza Round 1] E. Blossom

前言

感觉是一道比较有意思且适合我难度的题

思路

套路
  • 常见贡献问题

    • 求多种方式的贡献和
      • 往往更改贡献主题, 求花费对应的操作方式个数
      • 求单位部分的贡献, 然后求和
    • 求多种方式的最大贡献
      • 往往转化成判定类问题
  • 没什么约束的问题往往直接推导

经过套路的拆贡献, 我们可以把计算转化成

\(\displaystyle\sum_{[l, r], k} \Big[ \text{numbers from 0 to}\ k\ \text{all appear in}\ [l, r] \Big]\)

进一步考虑全排列的性质
考虑对于一个 \(k, [l, r]\), 对于所有全排列统计贡献, 这同样是套路的
于是考虑对于 \(k, [l, r]\) 固定, 如何计算贡献

下面设当前排列中有 \(c\)\(-1\)
不难发现倘若 \([l, r]\) 中存在 \(c_1 \leq c\)\(-1\), \(0 \sim k\) 中有 \(c_2 \leq c_1\) 个数未出现在当前排列中\((\)也就是需要拼进 \(c_1\)\(-1\) 中, 如果已经存在但是不在 \([l, r]\) 中显然没有贡献\()\)
那么有贡献

\[+\Delta = \binom{c_1}{c_2} \times c_2! \times (c - c_2)! \]

观察发现答案为

\[\sum_{l = 1}^{n} \sum_{r = l}^{n} \sum_{k = 0}^{n - 1} \binom{c_1}{c_2} \times c_2! \times (c - c_2)! \]

这个东西要想优化, 显然需要优化计算方式
我们现在提取花费 \(\displaystyle\binom{c_1}{c_2} \times c_2! \times (c - c_2)!\) 对应的约束

  • 考虑枚举 \(k, c_1\) 之后的合法条件

    • 区间 \([l, r]\)\(c_1\)\(-1\)
    • \(0 \sim k\) 中的数要么不存在于排列中, 要么在 \([l, r]\)
  • 计算区间 \([l, r]\)\(c_2\)\(0 \sim k\) 中的数不存在于排列中

这个时候一定要绕过来, 注意这个时候计算的已经不再是全排列个数了, 而是符合要求的区间个数
考虑对于一个区间, 我们可以计算出最小的的 \(k\) 使得 \(k\) 存在于排列中且不在 \([l, r]\) 中, 这提示我们使用差分的做法, 计算出两端, 然后前缀和一遍计算出 \(f_{i, j}\) 表示 \(c_1 = i, k = j\) 的贡献区间个数

枚举 \(k, c_1\), 动态统计 \(c_2\), 然后计算贡献之和即可

代码
#include <bits/stdc++.h>

const int MAXN = 5050;
const int MOD = 1000000007;

namespace calc {
    int add(int a, int b) { return a + b >= MOD ? a + b - MOD : a + b; }
    int mul(int a, int b) { return (1LL * a * b) % MOD; }
    int sub(int a, int b) { return a - b < 0 ? a - b + MOD : a - b; }
    void addon(int &a, int b) { a = add(a, b); }
    void mulon(int &a, int b) { a = mul(a, b); }
    void subon(int &a, int b) { a = sub(a, b); }
} using namespace calc;

int n, a[MAXN], fac[MAXN], C[MAXN][MAXN], b[MAXN], d[MAXN][MAXN];
bool vis[MAXN];

void solve() {
    scanf("%d", &n);
    for (int i = 0; i <= n; ++i) {
        vis[i] = 0;
        for (int j = 0; j <= n; ++j) d[i][j] = 0;
    }
    fac[0] = 1;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        fac[i] = mul(fac[i - 1], i);
        b[i] = b[i - 1] + (a[i] == -1);
        if (a[i] != -1) vis[a[i]] = 1;
    }
    for (int i = 0; i <= n; ++i) {
        C[i][0] = C[i][i] = 1;
        for (int j = 1; j < i; ++j) C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]);
    }
    int lftmn = n;
    for (int i = 1; i <= n; ++i) {
        int rgtmn = n;
        for (int j = n; j >= i; --j) {
            int x = b[j] - b[i - 1], y = std::min(lftmn, rgtmn);
            ++d[x][0], --d[x][y];
            if (a[j] != -1) rgtmn = std::min(rgtmn, a[j]);
        }
        if (a[i] != -1) lftmn = std::min(lftmn, a[i]);
    }
    for (int i = 0; i <= b[n]; ++i) for (int j = 1; j <= n; ++j) d[i][j] += d[i][j - 1];
    int ans = 0, c2 = 0;
    for (int k = 0; k < n; ++k) {
        c2 += (!vis[i]);
        for (int c1 = c2; c1 <= b[n]; ++c1) {
            int term = mul(C[c1][c2], fac[c2]); term = mul(term, fac[b[n] - c2]); term = mul(term, d[c1][k]);
            addon(ans, term);
        }
    }
    printf("%d\n", ans);
}

int main() {
    int T; scanf("%d", &T);
    while (T--) solve();
    return 0;
}

附: 事实上最关键的观察是

对于一个区间, 关于其合法的 \(c_2 \sim k\) 数量有限且连续, 于是考虑差分

总结

开篇就有两个套路的拆贡献, 甚至难点不在这里
然后难点在最后一个拆贡献

这个题的思维过程很有意思
初步找到对于确定排列的计算方法 \(\to\) 进一步分析找到对于不确定排列的计算方法 \(\to\) 最后转化到快速统计需要信息的方法

总是要有重头再来的勇气, 然后就是注重利用率而非效率, 注重方法反思

posted @ 2025-04-06 15:24  Yorg  阅读(83)  评论(0)    收藏  举报