洛谷CF587C Duff in the Army题解
题目传送门:
原题:CodeForces 587C
洛谷OJ: CF587C
说明 + 思路概览
简短结论:把树上任何一段路径上前 a 小的居民编号找出来(a ≤ 10)。利用 LCA + 二倍增(binary lifting),并在倍增表里为每个跳长存 该段上最小的最多 10 个编号(有序数组)。查询时把从 u、v 向上跳的若干段对应的“前 10”合并后再取前 a 即可。
为什么可行(关键观察):
- 每次询问 a ≤ 10,很小;因此我们只需维护每段路径上最小的最多 10 个编号,合并成本和存储都受限;
- 二倍增允许我们把从 u 到 LCA、从 v 到 LCA 的路径分解为 O(log n) 个“段”;每个段都事先保存了最多 10 个最小编号,查询时只需 O(log n) 次合并,每次合并最多处理 20 个元素 → 很快。
下面先给出复杂度与内存估计,再给带详细注释的代码。
时间复杂度:
- 预处理(构建 f[][] 与 up[][]):O(n log n * 10)(每次合并最多处理 10+10 个元素)
- 每次查询:O(log n * 10)
总体满足 n,m,q ≤ 1e5 的要求。
空间复杂度:f[][] 是 O(n log n),每个 up[i][j] 最多存 10 个整数(所以大致 O(n log n * 10)),在常见 CF 限制下可接受(注意实现中把每个城市的居民初步裁剪为最多 10 个)。
详注代码(已做必要的性能/正确性细节优化:对每个城市的居民只保留前 10 个)
// CF587C - Duff in the Army
// 详注版实现:LCA + 二倍增 + 每段保存前10小合并
// 关键点:因为 a ≤ 10,我们在每个 2^j 段上只保存排序后的前 10 个居民编号,合并时也裁剪为 10。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
// 2^17 = 131072 > 1e5,17 层足够;设置为 17 可节省一些内存(也可用 20)
const int LOGN = 17;
const int LIMIT = 10; // 每段我们只关心最小的最多 10 个编号
int n, m, q;
vector<int> adj[MAXN]; // 树的邻接表
int dep[MAXN]; // 深度(根深度为 0)
int fa[MAXN]; // 直接父亲(dfs 填充)
int up_node[MAXN][LOGN + 1]; // 二倍增父节点表:up_node[v][j] = v 的 2^j 上的祖先(如果越界为 0)
vector<int> seg[MAXN]; // seg[u] = 当前城市 u 的居民(已排序,并已裁剪为 <= LIMIT)
// up_list[u][j] 表示从 u 向上长度为 2^j 的那段(不含端点 2^j 的祖先)里**最小的最多 LIMIT 个编号**
// 我们用二维数组形式存储:up_list[u][j] 是 vector<int>(大小 ≤ LIMIT)
vector<int> up_list[MAXN][LOGN + 1];
// 合并两个已升序数组,取前 LIMIT 个(类似归并,有界)
vector<int> merge_topk(const vector<int>& A, const vector<int>& B) {
vector<int> res;
res.reserve(min(LIMIT, (int)(A.size() + B.size())));
int i = 0, j = 0;
while ((i < (int)A.size() || j < (int)B.size()) && (int)res.size() < LIMIT) {
if (i < (int)A.size() && (j >= (int)B.size() || A[i] < B[j])) {
// A 更小
res.push_back(A[i++]);
} else {
// B 更小或 A 已耗尽
res.push_back(B[j++]);
}
}
return res;
}
// DFS 填写 fa[] 和 dep[]
void dfs(int u, int parent) {
fa[u] = parent;
dep[u] = dep[parent] + 1;
for (int v : adj[u]) {
if (v == parent) continue;
dfs(v, u);
}
}
// 查询 u-v 路径上的前 LIMIT 小的编号(返回已排序的数组,长度 ≤ LIMIT)
// 注意:返回的数组不去重(题中每个人只在一个城市出现一次,所以不会出现重复编号)
vector<int> query_path(int u, int v) {
vector<int> res; // 当前合并结果(始终保持有序且长度 ≤ LIMIT)
// 确保 u 深度 >= v 深度
if (dep[u] < dep[v]) swap(u, v);
// 1) 将 u 向上提升到与 v 同深度,合并路段上的信息
for (int j = LOGN; j >= 0; --j) {
int anc = up_node[u][j]; // u 的 2^j 上的祖先(可能为 0)
if (anc != 0 && dep[anc] >= dep[v]) {
// 把从 u 向上长度 2^j 的那段(不含 anc)合并进结果
// up_list[u][j] 保存的就是该段上的前 LIMIT 个编号
res = merge_topk(res, up_list[u][j]);
u = anc;
}
}
// 现在 u 和 v 在同一深度
if (u == v) {
// 路径上的最后还要包含这个公共点 u(LCA),它的居民存在 seg[u]
res = merge_topk(res, seg[u]);
return res;
}
// 2) 同步向上跳,使得 f[u][0] == f[v][0](u 和 v 为 LCA 的不同子节点)
for (int j = LOGN; j >= 0; --j) {
if (up_node[u][j] != up_node[v][j]) {
// 合并 u 方向的那段与 v 方向的那段
res = merge_topk(res, up_list[u][j]);
res = merge_topk(res, up_list[v][j]);
u = up_node[u][j];
v = up_node[v][j];
}
}
// 现在 u 和 v 是 LCA 的直接子节点,LCA = fa[u] = fa[v]
// 需要合并 u、v 两个节点自身的居民,以及 LCA 的居民
res = merge_topk(res, seg[u]);
res = merge_topk(res, seg[v]);
res = merge_topk(res, seg[fa[u]]); // fa[u] == fa[v] 为 LCA(注意 LCA 存在,因为树连通且根为 1)
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> q;
// 读取树的边
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 读取 m 个居民的城市编号:第 i 个人住在 city ci
// 我们把编号 i 放入 seg[ci]
for (int i = 1; i <= m; ++i) {
int city;
cin >> city;
seg[city].push_back(i);
}
// 对每个城市的居民编号排序,并**裁剪为最多 LIMIT 个**(因为我们以后只关心前 LIMIT 个)
for (int i = 1; i <= n; ++i) {
if (!seg[i].empty()) {
sort(seg[i].begin(), seg[i].end());
if ((int)seg[i].size() > LIMIT) seg[i].resize(LIMIT);
}
}
// 准备 DFS:把虚拟父节点 0 的深度设为 -1,使根(1)的深度为 0
dep[0] = -1;
dfs(1, 0);
// 初始化二倍增第一列(j = 0)
for (int v = 1; v <= n; ++v) {
up_node[v][0] = fa[v]; // 2^0 = 1 的祖先就是父亲
up_list[v][0] = seg[v]; // 长度为 2^0 的段(从 v 向上 1 个节点的段)内的前 LIMIT 人:就是当前节点本身
// 注意:up_list[v][0] 的含义是:从 v 开始向上长度 2^0 (即 1) 的那段上的编号集合(不含上端祖先)
// 因为 seg[v] 已经被裁剪为 ≤ LIMIT,up_list[v][0] 也是 ≤ LIMIT
}
// 构建二倍增表:f[v][j] 和 up_list[v][j]
for (int j = 1; j <= LOGN; ++j) {
for (int v = 1; v <= n; ++v) {
int mid = up_node[v][j - 1]; // v 的 2^(j-1) 上的祖先
up_node[v][j] = (mid == 0 ? 0 : up_node[mid][j - 1]);
// up_list[v][j] = merge( up_list[v][j-1], up_list[mid][j-1] )
// 这表示从 v 向上长度 2^j 的那段(不含上端祖先)上的前 LIMIT 编号
if (mid == 0) {
// mid 越界,v 的上层段为空(保持空)
up_list[v][j] = up_list[v][j - 1];
} else {
up_list[v][j] = merge_topk(up_list[v][j - 1], up_list[mid][j - 1]);
}
}
}
// 处理查询
while (q--) {
int u, v, a;
cin >> u >> v >> a;
vector<int> ans = query_path(u, v);
// 输出前 a 个(实际个数可能少于 a)
int k = min((int)ans.size(), a);
cout << k;
for (int i = 0; i < k; ++i) cout << " " << ans[i];
cout << "\n";
}
return 0;
}
额外说明 / 常见坑
-
必须把每个城市的居民裁剪为 ≤10:否则某个城市里的大量居民会导致
an[u][0]很大,之后倍增时不断复制/合并这些大数组会导致超时 / 内存爆炸。实现里我们在输入后对seg[i]做了resize(LIMIT)操作。 -
向上段的定义:在本实现中,
up_list[u][j]表示从u开始“长度为 2^j 的段”上的(按编号升序的)前 LIMIT 个居民,段的上端端点 不包含(即覆盖 u, u 的父, …,直到第 2^j-1 个祖先)。这与up_node[u][j](2^j 的祖先节点)配合使用,能够避免重复并正确覆盖路径节点。 -
LCA 的处理:先把较深的点提升到同深度,再一起跳。最后如果
u==v(LCA 就是这个节点)记得合并它的居民;否则跳到u、v在 LCA 的直接子节点后,合并u、v、以及fa[u](LCA)的居民。 -
题目边界情况:当路径上无人居住,输出
0—— 这在代码里会正确输出。

浙公网安备 33010602011771号