背景知识
深度优先搜索与 DFS 序
深度优先搜索算法(DFS)是一种用于遍历或搜索树或图的算法。以下伪代码描述了在树 T 上进行深度优先搜索的过程:
procedure DFS(T, u, L) // T 是被深度优先搜索的树
// u 是当前搜索的节点
// L 是一个链表,保存了所有节点被第一次访问的顺序
append u to L // 将节点 u 添加到链表 L 的末尾
for v in u.children do // 枚举节点 u 的所有子节点 v
DFS(T, v) // 递归搜索节点 v
令 r 为树 T 的根,调用 DFS(T, r, L)
即可完成对 T 的深度优先搜索,保存在链表 L 中的排列被称为 DFS 序。相信聪明的你已经发现了,如果枚举子节点的顺序不同,最终得到的 DFS 序也会不同。
逆序对
给定一个长度为 n 的整数序列 a1,a2,⋯,an,该序列的逆序对数量是同时满足以下条件的有序数对 (i,j) 的数量:
- 1≤i<j≤n;
- ai>aj。
问题求解
给定一棵 n 个节点的树,其中节点 r 为根。求该树所有可能的 DFS 序中逆序对数量之和。
输入格式
第一行输入两个整数 n,r(2≤n≤3×105,1≤r≤n)表示树的大小与根节点。
对于接下来的 (n−1) 行,第 i 行输入两个整数 ui 与 vi(1≤ui,vi≤n),表示树上有一条边连接节点 ui 与 vi。
输出格式
输出一行一个整数,表示该树所有可能的 DFS 序中逆序对数量之和。由于答案可能很大,请对 109+7 取模后输出。
样例输入 1
5 3
1 5
2 5
3 5
4 3
样例输出 1
24
样例输入 2
10 5
10 2
2 5
10 7
7 1
7 9
4 2
3 10
10 8
3 6
样例输出 2
516
样例解释
下图展示了样例 1 中的树。
该树共有 4 种可能的 DFS 序:
- {3,4,5,1,2},有 6 个逆序对;
- {3,4,5,2,1},有 7 个逆序对;
- {3,5,1,2,4},有 5 个逆序对;
- {3,5,2,1,4},有 6 个逆序对。
因此答案为 6+7+5+6=24。
题目大意
给定一棵 nn 个节点的无向树,以 rr 为根。
在深度优先搜索(DFS)过程中,若对每个节点的子节点枚举顺序不同,会得到不同的 DFS 序。
求:所有可能的 DFS 序中,逆序对数量的总和,结果对 109+7 取模。
解题思路
本题的关键在于:不是枚举所有 DFS 序,而是统计所有 DFS 序中逆序对的总数。
我们采用 贡献法:
每对节点 (i,j)(i,j)(i<ji<j)在所有 DFS 序中,有多少次满足 ai>ajai>aj
即:我们枚举所有节点对 (u,v)(u,v)(u<vu<v),计算它们在所有 DFS 序中形成逆序对的次数之和。
分类讨论:节点对 (u,v)(u,v) 的关系
在树中,任意两点 u,vu,v 的关系只有三种:
情况 | 说明 | 逆序对贡献分析 |
---|---|---|
1. u 是 v 的祖先 | 路径 u→vu→v 存在 | u 总在 v 前,不可能构成逆序对(u<vu<v) |
2. v 是 u 的祖先 | 路径 v→uv→u 存在 | v 总在 u 前,u 在后,u>vu>v,可能构成逆序对 |
3. u 和 v 无祖孙关系 | 位于不同子树 | 在 DFS 中,谁先谁后取决于搜索顺序 |
贡献拆解
我们将总逆序对数拆为两部分:
1. 祖先-后代关系中的逆序对贡献
如果 v 是 u 的祖先,且 u>vu>v,那么在所有 DFS 序中,v 总在 u 前,但 u>vu>v,构成逆序对。
- 这种情况的贡献是:所有满足 v 是 u 祖先且 u>vu>v 的 (u,v)(u,v) 对数,乘以 所有 DFS 序的总数。
因为只要满足祖先关系,顺序就固定。
2. 无关节点对(无祖孙关系)的贡献
如果 u 和 v 无祖孙关系,则它们出现在 DFS 序中的相对顺序是随机的,取决于搜索路径。
- 在所有 DFS 序中,u 在 v 前 和 v 在 u 前 的概率是相等的。
- 所以当 u>vu>v 时,它们构成逆序对的概率是
。
- 贡献为:无关节点对中 u>vu>v 的对数 ×
× 所有 DFS 序总数
算法步骤
步骤 1:建图 + 预处理阶乘
- 建无向图
- 预处理阶乘 fact[i]=i!fact[i]=i!,用于后续组合计数
步骤 2:非递归 DFS 求每个节点的子树信息
我们用 非递归 DFS(避免栈溢出)遍历树,同时:
- 维护一个树状数组
c[]
,记录当前路径上已访问的节点 mx[u]
:表示在 uu 的子树中,编号大于 uu 的祖先的节点数(即情况 1 的贡献)
实际上,
mx[u]
就是:在从根到 uu 的路径上,有多少个祖先 vv 满足 v<uv<u,这些 vv 都会在 DFS 序中出现在 uu 前,但 u>vu>v,构成逆序对。
但代码中 mx[u]
实际计算的是:在从根到 uu 的路径上,编号小于 uu 的祖先数量(即 v<uv<u 的祖先数),因为这些祖先会在 uu 前出现,且 u>vu>v,贡献逆序对。
步骤 3:计算每个节点为根的子树内排列方案数 dp[u]
dp[u]
:以 u 为根的子树,在 DFS 中的排列方案数d[u]
:以 u 为根的子树中,所有节点对之间的“依赖关系数”(用于无关节点对计算)
转移:
dp[u] = ∏ dp[v] × fact[childCount]
即:子树排列数相乘,再乘上子节点的排列顺序数。
步骤 4:计算总答案
设:
:所有“祖先-后代”型逆序对的总出现次数(每种 DFS 序都出现)
:所有“子树内依赖”关系数
:所有节点对
:无依赖关系的节点对数(即可以任意顺序)
则:
- 无关节点对中,u>vu>v 的期望贡献为:
- 所有 DFS 序总数为:
最终答案:
答案 =(祖先-后代逆序对)+(无关节点对的期望逆序对) × 总方案数
Java实现:
import java.io.*;
public class Main {
static final int MOD = 1000000007; // 模数 10^9 + 7
static final int MAX_N = 300010; // 最大节点数
static int n, root; // 节点数,根节点编号
// 邻接表建图用数组
static int[] head = new int[MAX_N]; // 每个节点的第一条边的索引
static int[] to = new int[MAX_N << 1]; // to[i] 表示第 i 条边指向的节点
static int[] nextEdge = new int[MAX_N << 1]; // nextEdge[i] 表示第 i 条边的下一条边编号
static int edgeCounter = 1; // 边的编号(从 1 开始,0 表示无边)
// 预处理数组
static long[] factorial = new long[MAX_N]; // factorial[i] = i! mod MOD
static long[] dp = new long[MAX_N]; // dp[u]: 以 u 为根的子树的所有 DFS 序方案数
static int[] dependentPairs = new int[MAX_N]; // dependentPairs[u]: 子树 u 内部的“依赖对”数量(即祖孙关系对)
static int[] ancestorLessCount = new int[MAX_N]; // ancestorLessCount[u]: 从根到 u 的路径上编号小于 u 的祖先数量
static long[] fenwickTree = new long[MAX_N]; // 树状数组,用于动态维护路径上的节点
static class FastReader {
static final int BUFFER_SIZE = 1 << 16;
static byte[] buffer = new byte[BUFFER_SIZE];
static int pos = 0, len = 0;
static int readByte() {
if (pos >= len) {
pos = 0;
try {
len = System.in.read(buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (len == -1) return -1;
}
return buffer[pos++];
}
static int nextInt() {
int num = 0;
int b;
while ((b = readByte()) < 33) {} // 跳过空白字符
do {
num = num * 10 + (b - '0');
} while ((b = readByte()) >= 33); // 读取数字
return num;
}
}
/**
* 向邻接表添加无向边 u-v
*/
static void addEdge(int u, int v) {
to[edgeCounter] = v;
nextEdge[edgeCounter] = head[u];
head[u] = edgeCounter++;
}
public static void main(String[] args) throws IOException {
n = FastReader.nextInt();
root = FastReader.nextInt();
// 初始化邻接表
for (int i = 1; i <= n; i++) {
head[i] = 0;
}
edgeCounter = 1;
// 读入 n-1 条边,构建无向图
for (int i = 0; i < n - 1; i++) {
int u = FastReader.nextInt();
int v = FastReader.nextInt();
addEdge(u, v);
addEdge(v, u);
}
// 预处理阶乘:factorial[i] = i! mod MOD
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i % MOD;
}
int[] stack = new int[n + 1]; // 手动栈,存储待处理节点
int[] parent = new int[MAX_N]; // parent[u]: u 的父节点
int[] state = new int[MAX_N]; // state[u]: 0=未开始,1=子节点遍历中,2=已完成
int[] edgePtr = new int[MAX_N]; // edgePtr[u]: 当前遍历到 u 的第几条边
int top = 0; // 栈顶指针
// 初始化根节点入栈
stack[top++] = root;
parent[root] = 0;
state[root] = 0;
edgePtr[root] = head[root];
// 非递归 DFS 主循环
while (top > 0) {
int u = stack[top - 1]; // 查看栈顶节点
if (state[u] == 0) {
// 第一次访问 u:计算 ancestorLessCount[u]
// 查询:路径上编号在 (u, n] 范围内的祖先数量
long totalCount = 0;
for (int i = n; i > 0; i -= i & -i) totalCount += fenwickTree[i];
long prefixCount = 0;
for (int i = u; i > 0; i -= i & -i) prefixCount += fenwickTree[i];
ancestorLessCount[u] = (int)(totalCount - prefixCount);
// 将当前节点 u 加入路径(更新树状数组)
for (int i = u; i <= n; i += i & -i) {
fenwickTree[i]++;
}
// 初始化 dp 和 dependentPairs
dp[u] = 1;
dependentPairs[u] = 0;
state[u] = 1;
}
if (state[u] == 1) {
// 遍历 u 的子节点
boolean foundChild = false;
for (; edgePtr[u] != 0; edgePtr[u] = nextEdge[edgePtr[u]]) {
int v = to[edgePtr[u]];
if (v == parent[u]) continue;
parent[v] = u;
state[v] = 0;
edgePtr[v] = head[v];
stack[top++] = v;
edgePtr[u] = nextEdge[edgePtr[u]]; // 跳过已加入的边
foundChild = true;
break;
}
if (foundChild) continue;
state[u] = 2;
}
if (state[u] == 2) {
// 回溯:u 的所有子节点已处理完毕
top--;
int childCount = 0;
// 遍历所有子节点,合并 dp 和 dependentPairs
for (int i = head[u]; i != 0; i = nextEdge[i]) {
int v = to[i];
if (v == parent[u]) continue;
dp[u] = dp[u] * dp[v] % MOD;
dependentPairs[u] += dependentPairs[v];
childCount++;
}
// u 的子节点之间两两构成依赖对
dependentPairs[u] += childCount;
// u 的子节点可以任意排列,贡献 factor[childCount]
dp[u] = dp[u] * factorial[childCount] % MOD;
// 回溯:从路径中移除 u,撤销树状数组更新
for (int i = u; i <= n; i += i & -i) {
fenwickTree[i]--;
}
}
}
// 统计所有 dependentPairs 的总和 D
long totalDependent = 0;
for (int i = 1; i <= n; i++) {
totalDependent += dependentPairs[i];
}
totalDependent %= MOD;
// 统计祖先贡献 A = sum mx[i]
long ancestorContribution = 0;
for (int i = 1; i <= n; i++) {
ancestorContribution += ancestorLessCount[i];
}
ancestorContribution %= MOD;
// 计算所有节点对总数 C(n,2)
long inv2 = 500000004; // 1/2 mod MOD
long totalPairs = (long) n * (n - 1) % MOD;
totalPairs = totalPairs * inv2 % MOD;
// 无关节点对数量
long unrelatedCount = (totalPairs - totalDependent + MOD) % MOD;
// 无关节点对的期望逆序对贡献:乘以 1/2
long unrelatedContribution = unrelatedCount * inv2 % MOD;
// 总逆序对期望数量
long expectedInversions = (ancestorContribution + unrelatedContribution) % MOD;
// 最终答案 = 总方案数 × 每种方案的期望逆序对数
long ans = dp[root] * expectedInversions % MOD;
ans = (ans % MOD + MOD) % MOD; // 确保非负
System.out.println(ans);
}
}