洛谷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;
}

额外说明 / 常见坑

  1. 必须把每个城市的居民裁剪为 ≤10:否则某个城市里的大量居民会导致 an[u][0] 很大,之后倍增时不断复制/合并这些大数组会导致超时 / 内存爆炸。实现里我们在输入后对 seg[i] 做了 resize(LIMIT) 操作。

  2. 向上段的定义:在本实现中,up_list[u][j] 表示从 u 开始“长度为 2^j 的段”上的(按编号升序的)前 LIMIT 个居民,段的上端端点 不包含(即覆盖 u, u 的父, …,直到第 2^j-1 个祖先)。这与 up_node[u][j](2^j 的祖先节点)配合使用,能够避免重复并正确覆盖路径节点。

  3. LCA 的处理:先把较深的点提升到同深度,再一起跳。最后如果 u==v(LCA 就是这个节点)记得合并它的居民;否则跳到 uv 在 LCA 的直接子节点后,合并 uv、以及 fa[u](LCA)的居民。

  4. 题目边界情况:当路径上无人居住,输出 0 —— 这在代码里会正确输出。


posted @ 2025-08-17 07:55  kkman2000  阅读(15)  评论(0)    收藏  举报