题解:洛谷 P13020 遍历计数

【题目来源】

洛谷:P13020 [GESP202506 八级] 遍历计数

【题目描述】

给定一棵有 \(n\) 个结点的树 \(T\),结点依次以 \(1,2,\dots,n\) 标号。树 \(T\) 的深度优先遍历序可由以下过程得到:

  1. 选定深度优先遍历的起点 \(s\)\(1 \leq s \leq n\)),当前位置结点即是起点。
  2. 若当前结点存在未被遍历的相邻结点 \(u\) 则遍历 \(u\),也即令当前位置结点为 \(u\) 并重复这一步;否则回溯。
  3. 按照遍历结点的顺序依次写下结点编号,即可得到一组深度优先遍历序。

第一步中起点的选择是任意的,并且第二步中遍历相邻结点的顺序也是任意的,因此对于同一棵树 \(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

【核心思想】

  1. 问题分析:给定一棵有 \(n\) 个节点的树,求所有可能的 DFS 遍历序数量(对 \(10^9\) 取模)。DFS 遍历序由起点选择和每一步访问相邻节点的顺序共同决定。这是一个树形 DP + 排列组合问题,核心在于利用树的结构特性,将问题分解为"起点选择"和"各节点子节点排列"两个独立部分的乘积。

  2. 算法选择

    • 阶乘预处理 + 度数统计:预处理阶乘数组,统计每个节点的度数,利用乘法原理计算方案数
  3. 关键步骤

    • 读入数据\(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\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n)\),预处理阶乘 \(O(n)\),统计度数 \(O(n)\),计算 \(base\)\(ans\)\(O(n)\)
    • 空间复杂度:\(O(n)\),阶乘数组和度数数组
  5. 树形排列组合的核心思想

    • 起点选择的独立性:以节点 \(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
posted @ 2026-08-26 10:13  团爸讲算法  阅读(10)  评论(0)    收藏  举报