怎么全世界都觉得这个是黑题,来写一个简单做法的题解。
为了方便,后面记 为 Catalan 数列。
直接做比较困难,因此考虑容斥。钦定有 对公共配对,剩下部分被分成了 个连续段。若某一段有 对括号则两个序列在这一块可以分别任取合法序列,贡献就是 。
因此考虑 dp。设 表示当前考虑了前 个连续段,放了 对普通括号的方案权值的和是多少。初始条件显然就是 ,转移枚举下一段的长度 。此时 的含义就是标记了 个配对后剩下 对括号的配对方案数。由结论可知标记 个公共配对的总方案数为 ,二项式反演回去就可以算出答案。时间复杂度 ,好像能用一些科技继续优化时间复杂度但是没啥必要。
::::info[ 是什么?]{open} 这个结论可以理解为是 个配对把整个结构连接成一个 个块组成的树。设 个块的大小分别为 ,则必有 ,这些块内部的方案数加起来就是 ,为了方便这里记 。把所有可能的 对答案的贡献求和得到 。
然后考虑怎么把 个块用 个配对连起来。第 个块有 个地方可以用来连接其他块,若其挂了 个子块则方案数就是 ,整个结构有 个连接因此必有 ,求和得到总连接方法数为 ,换元一下然后直接上 ex-Vandermonde 可得上式的值为 。
然后考虑每个点的儿子数量分别为 ,考虑到 因此套路的把每个 的值都减 ,此时 。一个树按照 DFS 顺序访问这些结点的时候需要满足最后一个点钱已经出现的儿子总数始终不少于已经用掉的点数。考虑下面给出的引理:
:::info循环引理 对一个总和为 的序列,其所有循环移位中恰有一个移位满足上面给出的树的条件。
Reference. :::
所以同一种儿子数加上连接方法从 个不同的位置开始看必然恰好只有一个是真正合法的有根树。因此 种方法中合法方案占比只有 ,最终结果就是 。 ::::
namespace lowspeed_song {
mt19937_64 mt(time(0));
int fac[N], inv[N], ifac[N];
inline void init() {
}
int a[N], b[N], res[N], f[510][510];
inline void sol([[maybe_unused]]int __testcase_id, [[maybe_unused]]int __testcase_sum) {
int n, mod; cin >> n >> mod;
fac[0] = inv[0] = ifac[0] = 1;
fac[1] = inv[1] = ifac[1] = 1;
for (int i = 2; i < N; ++i) {
fac[i] = fac[i - 1] * i % mod;
inv[i] = mod - inv[mod % i] * (mod / i) % mod;
ifac[i] = ifac[i - 1] * inv[i] % mod;
} memset(f, 0, sizeof f);
function<int(int, int)> binom = [&](int a, int b) { return fac[a] * ifac[b] % mod * ifac[a - b] % mod; };
function<int(int, int)> calc = [&](int n, int k) { return binom(n, k) * inv[k + 1] % mod; };
for (int i = 0; i <= n; ++i) f[0][i] = calc(i + i, i) * calc(i + i, i) % mod;
for (int i = 1; i <= n; ++i) for (int j = 0; j <= n; ++j) for (int k = 0; k <= n - j; ++k)
f[i][j + k] = (f[i][j + k] + f[i - 1][j] * f[0][k] % mod) % mod;
for (int i = 0; i <= n; ++i) res[i] = calc(n + n, i) * f[i][n - i] % mod;
for (int i = n - 1; ~i; --i) for (int j = i + 1; j <= n; ++j) res[i] = (res[i] + mod - res[j] * binom(j, i) % mod) % mod;
for (int i = 0; i <= n; ++i) cout << res[i] << ' '; cout << '\n';
}
} // namespace lowspeed_song
喜欢的话,留下你的评论吧~