1 条题解

  • 0
    @ 2025-10-29 15:26:25

    一、题意理解

    我们有一棵 nn 个节点的树,每条边上有一个小写字母。

    • 任意一条路径(不一定简单路径?但树是无环,所以路径唯一)对应一个字符串,由路径上的边字母按顺序连接而成。
    • 对于每个询问 (u,v)(u,v),我们考虑从 uu 出发的所有路径对应的字符串(包括非空,且起点固定为 uu)。
    • 我们要数出这些字符串中,有多少个字符串字典序严格小于 uvu \to v 路径对应的字符串。

    注意:路径 uvu \leadsto v 是唯一的,因为树没有环。


    二、样例分析

    样例1:

    树结构:

    4 -t- 1 -s- 2 -p- 3
    

    (实际上边是 (4,1,t), (3,2,p), (1,2,s))

    询问1: u=3,v=2u=3, v=2
    323\to 2 字符串 = "p"
    从3出发的路径:

    • 3→2: "p"
    • 3→2→1: "ps"
    • 3→2→1→4: "pst" 比 "p" 小的:无 → 输出 0

    询问2: u=1,v=3u=1, v=3
    131\to 3 = 1→2→3: "sp"
    从1出发的路径:

    • 1→2: "s"
    • 1→2→3: "sp"
    • 1→4: "t" 比 "sp" 小的:只有 "s" → 输出 1

    询问3: u=2,v=1u=2, v=1
    212\to 1 = "s"
    从2出发的路径:

    • 2→1: "s"
    • 2→1→4: "st"
    • 2→3: "p" 比 "s" 小的:只有 "p" → 输出 1

    三、问题难点

    • qq 最多 5×1055\times 10^5n4000n \le 4000,不能对每个询问 O(n)O(n) 做。
    • 我们需要快速统计:从 uu 出发的所有路径对应的字符串中,有多少个字典序小于给定路径 P=uvP = u\leadsto v

    四、关键观察

    1. uu 出发的路径可以分为:

      • 路径 PP 本身(对应字符串 SS
      • 其他路径:它们一定在某个位置与 PP 分叉
    2. 字典序比较规则:

      • 假设另一条路径 QQPP 的前 kk 条边相同,第 k+1k+1 条边不同(或 QQPP 短且在 k+1k+1 处结束)。
      • 比较取决于第一个不同位置的字符大小。
      • 如果 QQPP 的前缀且长度小于 PP,则 QQ 字典序小于 PP
    3. 因此,对于路径 P=uvP = u \to v,我们可以把它拆成节点序列 u=x0,x1,x2,,xm=vu = x_0, x_1, x_2, \dots, x_m = v,边上的字符为 c1,c2,,cmc_1, c_2, \dots, c_m


    五、统计方法

    对于路径 PP,我们考虑每个可能的分叉点 xkx_k0k<m0 \le k < m):

    • 在节点 xkx_k,路径 PP 下一步走向 xk+1x_{k+1},边字符 ck+1c_{k+1}
    • 我们考虑从 xkx_k 出发走 另一条边(不是 xk+1x_{k+1} 的那条)的所有路径,这些路径对应的字符串前 kk 个字符与 PP 相同,第 k+1k+1 个字符不同。
    • 如果这条边的字符 c<ck+1c' < c_{k+1},那么 所有 从这条边出去的路径的字符串都小于 PP
    • 如果 c=ck+1c' = c_{k+1},则需要递归比较下一个字符,但这里我们可以预处理。

    更具体地:

    PP 的字符串为 SS

    我们要数的是:

    • 所有长度小于 S|S| 且是 SS 前缀的路径(它们字典序小于 SS
    • 所有在某个位置 iiSS 不同且该位置字符小于 S[i]S[i] 的路径

    六、预处理思路

    我们可以预处理:

    1. 对每个节点 uu,以 uu 为根的子树信息。
    2. 对每个节点 uu 和每个出边字符 chch,知道从 uu 走字符 chch 的边后,能到达的子树大小(即从那里出发的所有路径数)。
    3. 实际上,我们需要的是:从 uu 出发,走某条边后,所有路径的字符串按字典序排序后的排名信息。

    由于 n4000n \le 4000,我们可以对每个节点作为根,DFS 预处理出从该节点出发的所有路径,并按字典序排序,这样回答询问时直接二分。

    但总路径数太多(O(n2)O(n^2) 级别),不能显式存储。


    七、可行解法:可持久化 Trie

    我们可以把从每个节点 uu 出发的所有路径字符串插入一棵 Trie,并在 Trie 节点上记录子树大小(即有多少个字符串以该节点为前缀)。

    对于询问 (u,v)(u,v)

    • 我们沿着路径 uvu\leadsto v 在 Trie 上走
    • 当在某个 Trie 节点,我们选择比当前路径字符小的分支时,该分支下的所有字符串都小于目标路径,直接加总
    • 如果选择等于当前路径字符的分支,继续向下
    • 最后如果走完路径 PP,那么比它小的还包括所有是它真前缀的路径

    具体步骤:

    1. 对每个节点 uu 建立一棵 Trie,存储从 uu 出发的所有路径对应的字符串。
    2. Trie 节点记录:cntcnt = 该节点对应的字符串个数(即到该节点结束的路径数),szsz = 子树中所有字符串个数。
    3. 由于 nn 不大,我们可以 O(n2)O(n^2) 预处理所有 Trie(实际用可持久化 Trie 可节省空间)。
    4. 查询时,从 uu 的 Trie 根开始,沿着路径 PP 的字符走,累加所有小于的兄弟分支的 szsz

    八、算法步骤

    预处理

    对每个节点 uu

    • uu 出发 DFS 整棵树,记录当前路径字符串,构建 Trie。
    • 或者更高效:从 uu 出发 BFS/DFS,逐个插入字符串到 Trie。

    查询 query(u, v)

    1. 获取路径 uvu\to v 的字符序列 SS
    2. uu 的 Trie 中,从根开始,ans = 0
    3. 对于 i=0i = 0len(S)-1
      • 对当前 Trie 节点的每个子边:
        • 如果字符 ch<S[i]ch < S[i],则 ans += 该子节点的 sz
        • 如果字符 ch==S[i]ch == S[i],则移动到该子节点,并 ans += 该子节点的 cnt(因为真前缀也小于)
          • 这里注意:如果我们在第 ii 步选择等于 S[i]S[i] 的边,那么所有到该节点结束的字符串(即长度为 i+1i+1 的路径)是 SS 的前缀且比 SS 短,所以它们小于 SS
    4. 返回 ans

    九、复杂度

    • 预处理:O(n226)O(n^2 \cdot 26)(每个节点建 Trie,最多 nn 条字符串,长度最多 nn
    • 查询:O(n)O(n)(路径长度)
    • 总查询:O(qn)O(qn),最坏 5×105×40005\times 10^5 \times 4000 太大?需要优化。

    实际上我们可以 O(len(P))O(\text{len}(P)) 完成查询,但 len(P)n\text{len}(P) \le n

    n=4000n=4000q=5×105q=5\times 10^5,最坏 2×1092\times 10^9 操作,不可行。


    十、优化

    注意到 n4000n \le 4000,我们可以预处理所有 (u,v)(u,v) 的答案,O(n2logn)O(n^2 \log n) 预处理,O(1)O(1) 查询。

    预处理:

    • 对每个 uu,收集所有从 uu 出发的路径字符串,排序。
    • 对每个 vv,知道 uvu\leadsto v 的字符串 SS,在排序列表中二分查找 SS 的排名(严格小于的数量)。

    复杂度:O(n2logn)O(n^2 \log n) 预处理,O(1)O(1) 查询。 $n^2 \log n \approx 4000^2 \times 12 \approx 1.92\times 10^8$,可接受。


    十一、最终方案

    1. 预处理 LCA 用于快速获取路径字符串。
    2. 对每个 uu
      • DFS 从 uu 出发的所有路径,记录字符串和终点 vv
      • 对这些字符串排序,记录每个 vv 对应的排名(严格小于的数量)。
    3. 查询时直接输出 rank[u][v]

    十二、代码框架(C++)

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 4008;
    int tr[N][26], tot;
    int ans[N][N], n, q;
    vector<int> V[N]; // 相同父节点,相同边权,不同子节点的数组
    vector<pair<int, int>> G[N]; // 图的信息
    
    void clear() {
        memset(tr, 0, sizeof tr);
    
        for (int i = 0; i <= tot; i++)
            V[i].clear();
    
        tot = 0;
    }
    
    void insert(int u, int fa, int pos) {
        for (auto [v, w] : G[u]) {
            if (v == fa)
                continue;
    
            if (!tr[pos][w])
                tr[pos][w] = ++tot;
    
            int t = tr[pos][w];
            V[t].push_back(v);
            insert(v, u, t);
        }
    }
    
    void calc(int root, int pos, int &s) {
        for (auto v : V[pos])
            ans[root][v] = s;
    
        s += V[pos].size();
    
        for (int i = 0; i < 26; i++) {
            if (!tr[pos][i])
                continue;
    
            calc(root, tr[pos][i], s);
        }
    }
    
    void init() {
        for (int i = 1; i <= n; i++) {
            insert(i, 0, 0);
            int sum = 0;
            calc(i, 0, sum);
            clear();
        }
    }
    
    int main() {
        scanf("%d%d", &n, &q);
        int u, v;
        char ch[5];
    
        for (int i = 1; i < n; i++) {
            scanf("%d%d%s", &u, &v, ch);
            G[u].push_back({v, ch[0] - 'a'});
            G[v].push_back({u, ch[0] - 'a'});
        }
    
        init();
    
        while (q--) {
            scanf("%d%d", &u, &v);
            printf("%d\n", ans[u][v]);
        }
    
        return 0;
    }
    

    十三、总结

    本题的关键在于:

    1. 理解字典序比较规则在树路径字符串中的应用。
    2. 利用 nn 较小的特点,预处理所有 (u,v)(u,v) 的答案。
    3. 对每个起点 uu,收集所有路径字符串排序,通过二分得到排名。
    • 1

    信息

    ID
    4567
    时间
    1500ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    4
    已通过
    1
    上传者