题解:洛谷 P13020 遍历计数
【题目来源】
洛谷:P13020 [GESP202506 八级] 遍历计数
【题目描述】
给定一棵有 \(n\) 个结点的树 \(T\),结点依次以 \(1,2,\dots,n\) 标号。树 \(T\) 的深度优先遍历序可由以下过程得到:
- 选定深度优先遍历的起点 \(s\)(\(1 \leq s \leq n\)),当前位置结点即是起点。
- 若当前结点存在未被遍历的相邻结点 \(u\) 则遍历 \(u\),也即令当前位置结点为 \(u\) 并重复这一步;否则回溯。
- 按照遍历结点的顺序依次写下结点编号,即可得到一组深度优先遍历序。
第一步中起点的选择是任意的,并且第二步中遍历相邻结点的顺序也是任意的,因此对于同一棵树 \(T\) 可能有多组不同的深度优先遍历序。请你求出树 \(T\) 有多少组不同的深度优先遍历序。由于答案可能很大,你只需要求出答案对 \(10^9\) 取模之后的结果。
【输入】
第一行,一个整数 \(n\),表示树 \(T\) 的结点数。
接下来 \(n-1\) 行,每行两个正整数 \(u_i, v_i\),表示树 \(T\) 中的一条连接结点 \(u_i, v_i\) 的边。
【输出】
输出一行,一个整数,表示树 \(T\) 的不同的深度优先遍历序数量对 \(10^9\) 取模的结果。
【输入样例】
4
1 2
2 3
3 4
【输出样例】
6
【核心思想】
-
问题分析:给定一棵有 \(n\) 个节点的树,求所有可能的 DFS 遍历序数量(对 \(10^9\) 取模)。DFS 遍历序由起点选择和每一步访问相邻节点的顺序共同决定。这是一个树形 DP + 排列组合问题,核心在于利用树的结构特性,将问题分解为"起点选择"和"各节点子节点排列"两个独立部分的乘积。
-
算法选择:
- 阶乘预处理 + 度数统计:预处理阶乘数组,统计每个节点的度数,利用乘法原理计算方案数
-
关键步骤:
- 读入数据:\(n\) 和 \(n-1\) 条边
- 预处理阶乘:\(fac[i] = i! \bmod 10^9\)
- 统计度数:\(d[u]++, d[v]++\) 对每条边
- 计算固定根时的方案数 \(base\):
- 对每个节点 \(i\),\(base \leftarrow base \times fac[d[i] - 1] \bmod 10^9\)
- 解释:以任意节点为根时,节点 \(i\) 有 \(d[i] - 1\) 个子节点(一条边连父节点),子节点访问顺序可任意排列,方案数为 \((d[i] - 1)!\)
- 枚举起点计算答案:
- 对每个起点 \(i\),第一步有 \(d[i]\) 个相邻节点可选,后续方案数为 \(base\)
- \(ans \leftarrow ans + d[i] \times base \bmod 10^9\)
- 输出结果:\(ans\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),预处理阶乘 \(O(n)\),统计度数 \(O(n)\),计算 \(base\) 和 \(ans\) 各 \(O(n)\)
- 空间复杂度:\(O(n)\),阶乘数组和度数数组
-
树形排列组合的核心思想:
- 起点选择的独立性:以节点 \(i\) 为起点时,第一步有 \(d[i]\) 种选择(选哪个邻居先走)。选定第一步后,树的遍历等价于以该邻居为根的子树遍历,而 \(base\) 已经包含了所有节点(包括 \(i\))的子节点排列方案
- 子节点排列的乘法原理:对于每个节点,其子节点的访问顺序相互独立,方案数为子节点数的阶乘。所有节点的子节点排列方案数相乘即为固定根时的总方案数
- 度数与父子关系:树中每个节点的度数 \(d[i]\) 包含父节点和子节点。以任意节点为根时,每个节点恰好有一条边连向父节点,剩余 \(d[i] - 1\) 条边连向子节点,因此子节点数为 \(d[i] - 1\)
- 起点枚举的正确性:\(base\) 是以"某个固定根"计算的方案数,但实际上树的结构使得 \(base\) 与根的选择无关(因为每个节点的子节点数总是 \(d[i] - 1\),无论谁是根)。枚举起点时只需乘上第一步的 \(d[i]\) 种选择
- 适用于树的遍历计数、排列组合、乘法原理类问题
【算法标签】
普及+ #排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int宏定义为long long,防止阶乘计算溢出
const int N = 100005, mod = 1e9; // N为最大节点数常量,mod为取模数1e9
int n, base=1, ans; // n为节点数,base为固定起点时的DFS序数量,ans为所有起点选择的总数量
int fac[N], d[N]; // fac[i]为i的阶乘对mod取模的结果;d[i]为节点i的度数
signed main() // 使用signed main是因为#define int long long后,main返回值类型需要显式声明
{
cin >> n; // 读入树的节点数n
fac[0] = 1; // 0的阶乘为1
for (int i=1; i<=100000; i++) // 预处理1到100000的阶乘
fac[i] = (fac[i-1] * i) % mod; // fac[i] = i! % mod
for (int i=1; i<n; i++) // 循环读入n-1条边
{
int u, v; cin >> u >> v; // 读入一条边的两个端点
d[u]++; // 节点u的度数加1
d[v]++; // 节点v的度数加1
}
for (int i=1; i<=n; i++) // 计算固定起点时的DFS序数量
{
// 以任意节点为根时,每个节点的子节点排列方式数为(d[i]-1)!
// 因为一条边连向父节点,剩余d[i]-1条边连向子节点,子节点可以任意排列
base = (base * fac[d[i]-1]) % mod; // base累乘所有节点的(d[i]-1)!,得到固定根时的方案数
}
for (int i=1; i<=n; i++) // 枚举每个节点作为DFS起点
// 以节点i为起点时,i有d[i]个相邻节点可作为第一个访问的节点
// 其余节点的子节点排列方式与base相同
ans = (ans + d[i] * base) % mod; // 累加每个起点选择的贡献,起点i有d[i]种第一步选择
cout << ans << endl; // 输出所有不同DFS序的总数对1e9取模的结果
return 0;
}
【运行结果】
4
1 2
2 3
3 4
6
浙公网安备 33010602011771号