P6374 「StOI-1」树上询问(倍增+LCA)
题目链接
题目难度
不难,是蓝题。
题目解法概括
主要考察LCA的本质,然后运用倍增优化求解LCA之类的问题。
思路
lca(a, b) 可以理解为 节点 a 到 节点 b 的简单路径上的转折点,LCA 是有根树上的定义,有根树上的父子关系唯一,所以 a 到 b 的简单路径上的转折点唯一。
那么对于无根树,只有当节点 c 在 a 到 b 的简单路径上,树变成有根树后,c 才有可能成为 a 到 b 的路径上的转折点,而成为 lca(a, b),所以 c 在 a 到 b 的简单路径上,结果大于0,否则结果为0。
通过这一本质,可以明确结果是否为0,现在聚焦到如何求出结果大于 0 情况下结果具体的值。
首先 a 到 b 的简单路径上 c 以外的节点不能为根,不然这个节点就会是 lca(a, b)(也就是 a 到 b 的简单路径上的转折点),a(或 b)能通向的且通向的路径不经过 c 的节点也不能为根,否则 a(或 b)就为 lca(a, b)。
但是真按规律这么实现,时间复杂度太高,不允许。
对于无根树,第一步是指定根节点,转化为有根树,且整个过程根节点不变,不然调整根节点后,又要重新获取树的信息,太花时间,指定根的目的也是为了方便处理,所有的事都有最终目的。
这道题也可以先固定根,那么规律就变成了:
- 若 c = lca(a, b),则结果为 n - nums[jump(a, c)] - nums[jump(b, c)](nums[u] = 以节点 u 为根的子树的节点个数,jump(u, v) = 节点 u 向上跳到节点 v 的下一层所在的节点),如此剔除了简单路径上 c 以外的节点及 a(或 b)能通向的且通向的路径不经过 c 的节点。
- 若 c 为 a 到 lca(a, b) 路径上的节点(即 c = lca(a, c) 且 depth[c] > depth[lca(a, b)]),则结果为 nums[c] - nums[jump(a, c)],nums[c] 不包含节点 b 能通向的且通向的路径不经过 c 的节点及部分 a 到 b 的简单路径上的节点,nums[jump(a, c)] 包含了节点 a 能通向的且通向的路径不经过 c 的节点及部分 a 到 b 的简单路径上的节点,根据容斥原理,可得出答案。
- 若 c 为 b 到 lca(a, b) 路径上的节点(即 c = lca(b, c) 且 depth[c] > depth[lca(a, b)]),则结果为 nums[c] - nums[jump(b, c)]。
分类讨论完毕,变成有根树后,c 在 a 到 b 的简单路径上的情况就这几种。
实现
用倍增优化 jump 函数及求 LCA 的过程。
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;typedef long long LL;const LL Maxn = 5 * 1e5 + 5;
const LL Maxk = 20;vector<LL> tree[Maxn];
LL father[Maxn][Maxk], depth[Maxn], nums[Maxn];void dfs(LL u, LL fa) {depth[u] = depth[fa] + 1;father[u][0] = fa;nums[u] = 1;for (LL i = 1; depth[u] > (1 << i); ++i) {father[u][i] = father[father[u][i - 1]][i - 1];}for (auto v : tree[u]) {if (v == fa) continue;dfs(v, u);nums[u] += nums[v];}return;
}LL LCA(LL u, LL v) {if (depth[u] < depth[v]) swap(u, v);for (LL i = Maxk - 1; i >= 0; --i) {if (depth[father[u][i]] >= depth[v]) u = father[u][i];}if (u == v) return u;for (LL i = Maxk - 1; i >= 0; --i) {if (father[u][i] != father[v][i]) {u = father[u][i];v = father[v][i];}}return father[u][0];
}LL jump(LL u, LL v) {if (depth[u] == depth[v]) return 0;for (LL i = Maxk - 1; i >= 0; --i) {if (depth[father[u][i]] > depth[v]) u = father[u][i];}return u;
}int main() {ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);LL n, q, x, y, a, b, c;cin >> n >> q;for (LL i = 1; i < n; ++i) {cin >> x >> y;tree[x].emplace_back(y);tree[y].emplace_back(x);}dfs(1, 0);LL lca_ab = 0, lca_ac = 0, lca_bc;while (q--) {cin >> a >> b >> c;lca_ab = LCA(a, b), lca_ac = LCA(a, c), lca_bc = LCA(b, c);if (c == lca_ab) cout << (n - nums[jump(a, c)] - nums[jump(b, c)]) << '\n';else if (depth[c] > depth[lca_ab] && c == lca_ac) cout << (nums[c] - nums[jump(a, c)]) << '\n';else if (depth[c] > depth[lca_ab] && c == lca_bc) cout << (nums[c] - nums[jump(b, c)]) << '\n';else cout << 0 << '\n';}return 0;
}
总结
一开始看到这道题没什么思路,一般没什么思路的题需要通过找规律得出答案,可以根据比较小的样例来找规律,首先画出样例,再根据结果和数据的关系发现规律,有的时候直接按规律实现很费劲,那么就需要洞察到等价于规律的易于实现的求解内容。
本篇博客省略了找规律的过程,而是直接说明规律,接着找到等价于规律的易于实现的求解内容。
解决问题的步骤:
- 分解问题
- 搞清楚每步要求什么,注意要尽量适配算法。
- 用有这样特性的算法求得解。
收获
做了这道题,对 LCA 的理解更深刻了。