[CodeForces 2084E] Blossom
题意
给定长度为 \(n\) 的排列 \(a\),其中一些元素丢失被标记为 \(-1\)。定义排列的权值为其所有子区间的 MEX 值之和。试求所有可能的排列 \(a\) 的权值之和。
数据范围:\(1\le n\le 5000\),\(-1\le a_i<n\)。
思路 1
该思路为笔者的思路。
由于 MEX 的形式难以计数,所以需要拆贡献:\(\mathrm{MEX}_{i=l}^{r}(a_i)=\sum_{i=0}^{n-1} [0\sim i\ 均在\ a_{l\sim r}\ 中]\)。暴力枚举 \(l,r,i\) 的复杂度显然是不行的,在固定了 \(i\) 之后,\(l\le \min(a_j),r\ge \max(a_j)\) 的区间均是合法的,也就是说贡献为 \((\min(a_j)+1)(n-\max(a_j))\),其中 \(j\in [0,i]\)。
若 \(i,\min(a_j),\max(a_j)\) 固定,则贡献为:
注意:这里仅讨论 \(\min,\max\) 均为 \(-1\) 的情况,其余情况类似,读者自行思考。
其中,\(\text{pref}_{-1}(i)\) 为 \(a\) 排列中 \(1\sim i\) 位置的 \(-1\) 的数量;\(\text{count}_{-1}(l\sim r)\) 为 \(l\sim r\) 中的数,在 \(a\) 序列未出现的数量;\(U\) 为排列 \(a\) 中 \(-1\) 的数量。
但是,枚举 \(\min,\max,i\) 的复杂度是吃不消的。观察一下式子,考虑是否存在冗余计算的情况。注意到,式子中至于 \(\text{count}_{-1}(l\sim r)\) 有关,而不与 \(l,r\) 有关。所以,可以对于每个 \(\text{count}_{-1}(l\sim r)\) 的值进行预处理并对 \(i\) 做前缀和。这样,在查询的时候,只需要 \(i\) 在一段区间中(因为有些 \(i\) 是不合法的)的和,通过预处理的前缀和可 \(O(1)\) 查询。
由于该做法繁琐且容易写错,不如题解的思路优秀,所以就不贴代码了,可前往 submission 查看。
思路 2
该思路为题解的思路
MEX 的拆贡献是没有问题的,但是去考虑合法的区间过于麻烦了,实际上对于每个 \((l,r,i)\) 计算有多少中可能的排列即可。还是考虑将贡献的公式列出:
其中 \(x\) 表示排列 \(a\) 的 \(l\sim r\) 位置中 \(-1\) 的数量;\(y\) 表示值域 \(0\sim i\) 中 \(-1\) 的数量;\(z\) 表示排列 \(a\) 中所有 \(-1\) 的数量。
注意到,表达式中不与 \(l,r\) 有关,只与 \(l\sim r\) 位置中 \(-1\) 的数量有关。所以,可以预处理 \(-1\) 数量为 \(i\) 且 \(0\sim j\) 中所有不是 \(-1\) 的数所在的位置均在 \([l,r]\) 中的区间数量,记作 \(f_{i,j}\)。
考虑如何计算 \(f_{i,j}\),枚举每个区间 \([l,r]\),那么 \(i\) 可通过前缀和快速得到,对于 \(j\) 来讲,其所有可能的值为前缀,即 \([0,\min(\min_{k=0}^{i-1} a_k, \min_{k=j+1}^{n-1} a_k)))\),所以直接差分即可。
时间复杂度:\(O(n^2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n;
cin >> n;
const int mod = 1e9 + 7;
std::vector<int> fact(n + 1, 1);
vector C(n + 1, vector<int>(n + 1));
for (int i = 1; i <= n; i ++)
fact[i] = (i64)fact[i - 1] * i % mod;
for (int i = 0; i <= n; i ++)
for (int j = 0; j <= i; j ++)
if (!j) C[i][j] = 1;
else C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
const int inf = 1 << 30;
std::vector<int> a(n), b(n), c(n, 1);
for (int i = 0; i < n; i ++) {
cin >> a[i], b[i] = a[i] == -1;
if (~a[i]) c[a[i]] = 0;
}
for (int i = 1; i < n; i ++) {
b[i] += b[i - 1];
c[i] += c[i - 1];
}
vector dp(n + 1, vector<int>(n));
int l_min = inf, r_min = inf;
for (int i = 0; i < n; i ++) {
r_min = inf;
for (int j = n - 1; j >= i; j -- ) {
int temp = b[j] - (!i ? 0 : b[i - 1]);
int lim = min(l_min, r_min);
dp[temp][0] ++;
if (lim < inf) dp[temp][lim] --;
if (~a[j]) r_min = min(r_min, a[j]);
}
if (~a[i]) l_min = min(l_min, a[i]);
}
for (int i = 0; i <= n; i ++)
for (int j = 1; j < n; j ++)
dp[i][j] += dp[i][j - 1];
int res = 0;
for (int i = 0; i <= n; i ++)
for (int j = 0; j < n; j ++)
(res += (i64)C[i][c[j]] * fact[c[j]] % mod
* fact[c[n - 1] - c[j]] % mod * dp[i][j] % mod) %= mod;
cout << res << endl;
}
int main() {
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
int test;
for (cin >> test; test; test --) solve();
return 0;
}
总结与反思
对于这种计数问题,常见的技巧是拆贡献和改变计贡献方式。那么,在本题中,在做完这些操作后,发现复杂度不优秀。这种时候,需要先将式子列出来,并观察式子中(或者是式子一部分)所用到的量有没有小于枚举的量,若是可通过预处理的方式来减少冗余计算。

浙公网安备 33010602011771号