QOJ #8047. DFS Order 4 题解

Description

小青鱼(Little Cyan Fish),又名青羽小(Qingyu Xiao),非常喜欢 DFS 序的概念。今天,他手中有一棵有 nn 个节点的有根树 TT,节点编号从 11nn。这棵树的根是节点 11,而对于每个节点 ii2in2 \le i \le n),其父节点为 fif_i,满足 1fi<i1 \le f_i < i

一个 DFS 序列 D=(D1,D2,,Dn)D = (D_1, D_2, \ldots, D_n) 表示对树进行深度优先遍历时访问节点的顺序。DD 中第 jj 项表示该节点是 DFS 过程中第 jj 个被访问的节点。

在深度优先搜索过程中,如果某个节点有多个子节点,则按节点编号的升序遍历它们。因此,对于每一棵有根树,DFS 序是唯一确定的。

下面是用于生成 DFS 序列的伪代码,其中树 TT 由数组 f={f2,f3,,fn}f = \{f_2, f_3, \ldots, f_n\} 唯一表示:


算法 1:深度优先搜索的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
1: procedure dfs(vertex x)
2: Append x to the end of dfs_order
3: for each child y of x do // 子节点按索引升序遍历
4: dfs(y)
5: end for
6: end procedure

7: procedure generate()
8: Let dfs_order be a global variable
9: dfs_order ← empty list
10: dfs(1)
11: return dfs_order
12: end procedure

DDgenerate() 返回的数组。

由于每棵合法的树对应一个合法的 ff 数组,而 ff 的合法取值有 (n1)!(n - 1)! 种(因为每个 fif_i 可以从 11i1i - 1 中任选一个),所以总共有 (n1)!(n - 1)! 棵不同的树。

现在,小青鱼想知道:对于所有 (n1)!(n - 1)! 种可能的 ff 配置,最终能生成多少种不同的 DFS 序列 DD?我们认为两个 DFS 序列 DDDD' 不同,当且仅当存在某个 1in1 \le i \le n,使得 DiDiD_i \ne D'_i

由于这个数可能非常大,你的任务是计算该数对一个给定素数 PP 取模的结果。

n800n\leq 800

Solution

考虑怎么判断一个序列 aa 合法。

从前往后扫序列,维护当前扫到的 aia_i 的父链 anc1,anc2,,anckanc_1,anc_2,\ldots,anc_k。加入一个数,就是选择 ancj<xanc_{j}<x 的最大的 jj,并把 xx 的父亲设为 ancj1anc_{j-1}

那么这么建出来的树一定满足下面的条件:对于每个点 xx,把 xx 的儿子从小到大拿出来,设为 u1,u2,,umu_1,u_2,\ldots,u_m,同时设 viv_iuiu_i 最大的儿子,如果没有即为 00。然后需要满足 2im,ui<vi1\forall 2\leq i\leq m,u_i<v_{i-1}


考虑把满足条件的树的形态拿出来并对这样的树的拓扑序计数。

而这种树的拓扑图为 xu1,uiui+1,ui+1vix\to u_1,u_i\to u_{i+1},u_{i+1}\to v_i,是个普通的 DAG,不是很好计数。

注意到如果没有 ui+1viu_{i+1}\to v_i,整张图就是个外向树了。

所以考虑把这些边做容斥,即把 [ui+1<vi][u_{i+1}<v_i] 变为 1[vi<ui+1]1-[v_i<u_{i+1}],这等价于选择一些 ui+1viu_{i+1}\to v_i 的边删掉,剩下的进行反向,变为 viui+1v_i\to u_{i+1}

如果 ui<vi<ui+1u_i<v_i<u_{i+1},则可以把 uiui+1u_i\to u_{i+1} 删掉,剩下的图就是外向树了。

显然一个外向树的 DAG 数为 n!1szi\displaystyle n!\cdot\prod\frac{1}{sz_i}


求方案有个直观的想法是设 fif_{i} 表示 ii 个点的答案,转移时枚举根最左边的子树的大小。

如果 u1u_1 的边为 u1u2u_1\to u_{2} 则可以转移,但是如果 u1u_1 的边为 v1u2v_1\to u_2,则 u1u_1 所有儿子的子树大小都会变,这就不能转移了。

所以需要加一维 jj,即 fi,jf_{i,j} 表示大小为 ii 的子树,往根最右边加一个大小为 jj 的子树 um+1u_{m+1},同时要求 umu_{m} 连的是 umum+1u_m\to u_{m+1} 的答案。

转移时同样枚举最左边的子树大小即可。转移方程:

fi,j=k=1i1fik,j×ik+ji+j×fk,0×ki1+jk=1i2fik,j×ik+ji+j×fk,i1k+jf_{i,j}=\sum_{k=1}^{i-1}{f_{i-k,j}\times\frac{i-k+j}{i+j}\times f_{k,0}\times\frac{k}{i-1+j}}-\sum_{k=1}^{\color{red}{i-2}}{f_{i-k,j}\times\frac{i-k+j}{i+j}\times f_{k,i-1-k+j}}

注意:这里如果 u1u_1 连的是 v1u2v_1\to u_2,则需要满足根至少有 22 个儿子,即 k<i1k<i-1,否则就连向那个凭空加入的子树了,这与状态矛盾。

时间复杂度:O(n3)O(n^3)

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 805;

int n, mod;
int inv[kMaxN], f[kMaxN][kMaxN];

int qpow(int bs, int64_t idx = mod - 2) {
int ret = 1;
for (; idx; idx >>= 1, bs = (int64_t)bs * bs % mod)
if (idx & 1)
ret = (int64_t)ret * bs % mod;
return ret;
}

inline int add(int x, int y) { return (x + y >= mod ? x + y - mod : x + y); }
inline int sub(int x, int y) { return (x >= y ? x - y : x - y + mod); }
inline void inc(int &x, int y) { (x += y) >= mod ? x -= mod : x; }
inline void dec(int &x, int y) { (x -= y) < 0 ? x += mod : x; }

void prework() {
for (int i = 1; i <= n; ++i) inv[i] = qpow(i);
}

void dickdreamer() {
std::cin >> n >> mod;
prework();
for (int i = 0; i <= n; ++i) f[0][i] = 1;
for (int i = 0; i <= n - 1; ++i) f[1][i] = inv[i + 1];
for (int i = 2; i <= n; ++i) {
for (int j = 0; j <= n - i; ++j) {
for (int k = 1; k <= i - 1; ++k) {
inc(f[i][j], 1ll * f[i - k][j] * (i - k + j) % mod * inv[i + j] % mod * f[k][0] % mod * k % mod * inv[i - 1 + j] % mod);
if (k < i - 1)
dec(f[i][j], 1ll * f[i - k][j] * (i - k + j) % mod * inv[i + j] % mod * f[k][i - 1 - k + j] % mod);
}
}
}
int ans = f[n][0];
for (int i = 1; i <= n; ++i) ans = 1ll * ans * i % mod;
std::cout << ans << '\n';
}

int32_t main() {
#ifdef ORZXKR
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
int T = 1;
// std::cin >> T;
while (T--) dickdreamer();
// std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
return 0;
}

QOJ #8047. DFS Order 4 题解
https://sobaliuziao.github.io/2025/04/10/post/e76f256a.html
作者
Egg_laying_master
发布于
2025年4月10日
许可协议