[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]\) 中显然没有贡献\()\)
那么有贡献
观察发现答案为
这个东西要想优化, 显然需要优化计算方式
我们现在提取花费 \(\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\) 最后转化到快速统计需要信息的方法
总是要有重头再来的勇气, 然后就是注重利用率而非效率, 注重方法反思

浙公网安备 33010602011771号