L3-032 关于深度优先搜索和逆序对的题应该不会很难吧这件事(30分)(树状DP+非递归DFS+贡献法) (Java)

背景知识

深度优先搜索与 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 中的树。

sample.png

该树共有 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 时,它们构成逆序对的概率是 \frac{1}{2}
  • 贡献为:无关节点对中 u>vu>v 的对数 × \frac{1}{2}× 所有 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:计算总答案

设:

  • A = \sum mx[i]:所有“祖先-后代”型逆序对的总出现次数(每种 DFS 序都出现)
  • D = \sum d[i]:所有“子树内依赖”关系数
  • totalPairs = \binom{n}{2} = \frac{n(n-1)}{2}:所有节点对
  • unrelated = totalPairs - D:无依赖关系的节点对数(即可以任意顺序)

则:

  • 无关节点对中,u>vu>v 的期望贡献为:unrelated = totalPairs - D
  • 所有 DFS 序总数为:dp[root]

最终答案:

ans = dp[root] × \left( A + \frac{1}{2} × \left( \frac{n(n-1)}{2} - D \right) \right)

答案 =(祖先-后代逆序对)+(无关节点对的期望逆序对) × 总方案数

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);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值