CF1874F Jellyfish and OEIS【容斥,DP】

发布时间 2023-10-13 21:19:44作者: came11ia

给定序列 \(m_i\),求有多少排列 \(p\) 满足:对于满足 \(l \le r \le m_l\) 的所有 \((l,r)\)\(p_{l \sim r}\) 都不是 \(l \sim r\) 的排列。答案对 \(10^9 + 7\) 取模。

\(n \le 200\),时限 \(\text{2.0s}\)


限制形如要求全都不合法,我们考虑容斥,即在所有 \(\sum(m_i - i + 1)\) 个区间中钦定一些,要求其对应的 \(p_{l \sim r}\) 必须是 \(l \sim r\) 的一个排列,计算方案数乘上容斥系数后的和。

我们发现,如果钦定的区间存在相交的部分,这个方案数其实并不是很好计算。注意到,对于钦定的两个区间 \([a,b],[c.d]\),若满足 \(a < c \le b < d\),那么其实可以把它们拆成 \([a,c - 1],[c,b],[b+1,d]\)。这样方案数不变,但由于多了一条限制,容斥系数会乘上 \(-1\),于是它们对答案的贡献会相互抵消。更进一步地,如果两组限制方案数相同而区间数奇偶性不同,贡献就能够相互抵消,基于此,我们继续发掘一些更好的性质。

现在考虑一组限制 \(b\),满足其中任意区间都和 \(b\) 中的另一区间有交。我们每次取出 \(b\) 中两个有交的区间,按上面那样拆掉,再扔回 \(b\) 并去重,这样有限次之后能够得到另一组限制 \(t\)。对于任意一组限制 \(c\),我们都可以把它写成某个 \(b\) 加上一些无交(但是可能相互包含,或者和 \(b\) 中的某些区间包含但无交)区间的形式。设 \(b\) 对应的 \(t\) 中有 \(k\) 个区间,那么对于每个区间,选或者不选都不影响答案,这样的方案数一共有 \(2^k\) 种。当 \(k \geq 1\) 时,其中一定恰好有一半是奇数一半是偶数,所以这部分贡献会直接抵消完。

而只有当 \(k=0\),即不存在某两个区间相交时,容斥系数会由无交区间确定,并被保留下来。也就是说,我们仅需要考虑区间构成树结构的情况,这是一个非常大的进步。

此时满足一组限制的方案数容易计算:答案显然就是每一层散点个数阶乘的积。于是可以区间 DP,设 \(f_{l,r}\) 表示只考虑包含于 \(l,r\) 的区间,且假设 \([l,r]\) 这个区间已被选(但不贡献容斥系数),所有方案的容斥系数乘上方案数之和。\(g_{l,r,x}\) 表示只考虑包含于 \([l,r]\) 的区间,这一层有 \(x\) 个散点的所有方案的容斥系数乘上方案数之和。转移考虑当前的 \(l\) 是否新开出一个区间:

  • 不开新区间,\(l\) 会成为一个散点,则 \(g_{l,r,x} \gets g_{l+1,r,x-1}\)
  • 新开一个区间,设之为 \([l,k](k \le m_l)\),则 \([l,r]\) 部分的散点全都会被盖住,即 \(g_{l,r,x} \gets f_{l,k} \times g_{r + 1,k,x}\)

\(g\) 转移完后令 \(f_{l,r} = \sum g_{l,r,x} \times x!\) 即可。最后一个小问题就是在转移 \(g\) 的时候我们时没管新开的区间是 \([l,r]\) 的情况,在这里可以利用 \(f\) 直接补上。总时间复杂度 \(\mathcal{O}(n^4)\),常数很小,可以通过。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 205, mod = 1e9 + 7;
bool Mbe;
int n, lim[N], f[N][N], g[N][N][N];
int fc[N];
bool Med;
int main() {
//	fprintf(stderr, "%.9lf\n", 1.0 * (&Mbe - &Med) / 1048576.0);
	ios :: sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> lim[i];
	for (int i = 1; i <= n + 1; i++) g[i][i - 1][0] = 1;
	fc[0] = 1;
	for (int i = 1; i <= n; i++) fc[i] = 1LL * fc[i - 1] * i % mod;
	for (int L = 1; L <= n; L++) 
		for (int l = 1, r = l + L - 1; r <= n; l++, r++) {
			for (int x = 1; x <= r - l + 1; x++) {
				g[l][r][x] = g[l + 1][r][x - 1];
			}
			for (int k = l; k <= min(lim[l], r - 1); k++) 
				for (int x = 0; x <= r - (k + 1) + 1; x++) {
					g[l][r][x] = (g[l][r][x] + 1LL * g[k + 1][r][x] * (mod - f[l][k]) % mod) % mod;  
				}
			for (int x = 0; x <= r - l + 1; x++) {
				f[l][r] = (f[l][r] + 1LL * fc[x] * g[l][r][x] % mod) % mod;
			}
			if (lim[l] >= r) g[l][r][0] = (g[l][r][0] + mod - f[l][r]) % mod;
		} 
	int ans = f[1][n];
	if (lim[1] == n) ans = 0;
	cout << ans << "\n";
//	cout << 1e3 * clock() / CLOCKS_PER_SEC << "ms\n";
	return 0;
}