1 条题解
-
0
一、题意理解
我们有一棵 个节点的树,每条边上有一个小写字母。
- 任意一条路径(不一定简单路径?但树是无环,所以路径唯一)对应一个字符串,由路径上的边字母按顺序连接而成。
- 对于每个询问 ,我们考虑从 出发的所有路径对应的字符串(包括非空,且起点固定为 )。
- 我们要数出这些字符串中,有多少个字符串字典序严格小于 路径对应的字符串。
注意:路径 是唯一的,因为树没有环。
二、样例分析
样例1:
树结构:
4 -t- 1 -s- 2 -p- 3(实际上边是 (4,1,t), (3,2,p), (1,2,s))
询问1:
字符串 = "p"
从3出发的路径:- 3→2: "p"
- 3→2→1: "ps"
- 3→2→1→4: "pst" 比 "p" 小的:无 → 输出 0
询问2:
= 1→2→3: "sp"
从1出发的路径:- 1→2: "s"
- 1→2→3: "sp"
- 1→4: "t" 比 "sp" 小的:只有 "s" → 输出 1
询问3:
= "s"
从2出发的路径:- 2→1: "s"
- 2→1→4: "st"
- 2→3: "p" 比 "s" 小的:只有 "p" → 输出 1
三、问题难点
- 最多 ,,不能对每个询问 做。
- 我们需要快速统计:从 出发的所有路径对应的字符串中,有多少个字典序小于给定路径 。
四、关键观察
-
从 出发的路径可以分为:
- 路径 本身(对应字符串 )
- 其他路径:它们一定在某个位置与 分叉
-
字典序比较规则:
- 假设另一条路径 与 的前 条边相同,第 条边不同(或 比 短且在 处结束)。
- 比较取决于第一个不同位置的字符大小。
- 如果 是 的前缀且长度小于 ,则 字典序小于 。
-
因此,对于路径 ,我们可以把它拆成节点序列 ,边上的字符为 。
五、统计方法
对于路径 ,我们考虑每个可能的分叉点 ():
- 在节点 ,路径 下一步走向 ,边字符 。
- 我们考虑从 出发走 另一条边(不是 的那条)的所有路径,这些路径对应的字符串前 个字符与 相同,第 个字符不同。
- 如果这条边的字符 ,那么 所有 从这条边出去的路径的字符串都小于 。
- 如果 ,则需要递归比较下一个字符,但这里我们可以预处理。
更具体地:
设 的字符串为 。
我们要数的是:
- 所有长度小于 且是 前缀的路径(它们字典序小于 )
- 所有在某个位置 与 不同且该位置字符小于 的路径
六、预处理思路
我们可以预处理:
- 对每个节点 ,以 为根的子树信息。
- 对每个节点 和每个出边字符 ,知道从 走字符 的边后,能到达的子树大小(即从那里出发的所有路径数)。
- 实际上,我们需要的是:从 出发,走某条边后,所有路径的字符串按字典序排序后的排名信息。
由于 ,我们可以对每个节点作为根,DFS 预处理出从该节点出发的所有路径,并按字典序排序,这样回答询问时直接二分。
但总路径数太多( 级别),不能显式存储。
七、可行解法:可持久化 Trie
我们可以把从每个节点 出发的所有路径字符串插入一棵 Trie,并在 Trie 节点上记录子树大小(即有多少个字符串以该节点为前缀)。
对于询问 :
- 我们沿着路径 在 Trie 上走
- 当在某个 Trie 节点,我们选择比当前路径字符小的分支时,该分支下的所有字符串都小于目标路径,直接加总
- 如果选择等于当前路径字符的分支,继续向下
- 最后如果走完路径 ,那么比它小的还包括所有是它真前缀的路径
具体步骤:
- 对每个节点 建立一棵 Trie,存储从 出发的所有路径对应的字符串。
- Trie 节点记录: = 该节点对应的字符串个数(即到该节点结束的路径数), = 子树中所有字符串个数。
- 由于 不大,我们可以 预处理所有 Trie(实际用可持久化 Trie 可节省空间)。
- 查询时,从 的 Trie 根开始,沿着路径 的字符走,累加所有小于的兄弟分支的 。
八、算法步骤
预处理
对每个节点 :
- 从 出发 DFS 整棵树,记录当前路径字符串,构建 Trie。
- 或者更高效:从 出发 BFS/DFS,逐个插入字符串到 Trie。
查询
query(u, v)- 获取路径 的字符序列 。
- 在 的 Trie 中,从根开始,
ans = 0 - 对于 到
len(S)-1:- 对当前 Trie 节点的每个子边:
- 如果字符 ,则
ans += 该子节点的 sz - 如果字符 ,则移动到该子节点,并
ans += 该子节点的 cnt(因为真前缀也小于)- 这里注意:如果我们在第 步选择等于 的边,那么所有到该节点结束的字符串(即长度为 的路径)是 的前缀且比 短,所以它们小于 。
- 如果字符 ,则
- 对当前 Trie 节点的每个子边:
- 返回
ans
九、复杂度
- 预处理:(每个节点建 Trie,最多 条字符串,长度最多 )
- 查询:(路径长度)
- 总查询:,最坏 太大?需要优化。
实际上我们可以 完成查询,但 。
,,最坏 操作,不可行。
十、优化
注意到 ,我们可以预处理所有 的答案, 预处理, 查询。
预处理:
- 对每个 ,收集所有从 出发的路径字符串,排序。
- 对每个 ,知道 的字符串 ,在排序列表中二分查找 的排名(严格小于的数量)。
复杂度: 预处理, 查询。 $n^2 \log n \approx 4000^2 \times 12 \approx 1.92\times 10^8$,可接受。
十一、最终方案
- 预处理 LCA 用于快速获取路径字符串。
- 对每个 :
- DFS 从 出发的所有路径,记录字符串和终点 。
- 对这些字符串排序,记录每个 对应的排名(严格小于的数量)。
- 查询时直接输出
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
信息
- ID
- 4567
- 时间
- 1500ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 4
- 已通过
- 1
- 上传者