Loading

[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)\) 固定,则贡献为:

\[\binom{\text{pref}_{-1}(i)}{2}\binom{\text{count}_{-1}(\min\sim \max) - 2}{\text{pref}_{-1}(i)-2}(\text{pref}_{-1}(i)-2)!(U-\text{pref}_{-1}(i))! \]

注意:这里仅讨论 \(\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)\) 计算有多少中可能的排列即可。还是考虑将贡献的公式列出:

\[\binom{x}{y} y! (z-y)! \]

其中 \(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;
}

总结与反思

对于这种计数问题,常见的技巧是拆贡献和改变计贡献方式。那么,在本题中,在做完这些操作后,发现复杂度不优秀。这种时候,需要先将式子列出来,并观察式子中(或者是式子一部分)所用到的量有没有小于枚举的量,若是可通过预处理的方式来减少冗余计算。

posted @ 2025-04-07 15:47  Pigsyy  阅读(46)  评论(0)    收藏  举报