1 条题解

  • 0
    @ 2026-4-17 20:13:15

    题目解析

    问题重述

    在一棵 nn 个节点的树上,Marat 从节点 xx 出发要去节点 yy,有 mm 个敌人,每个敌人 iiaia_i 走到 bib_i(最短路径)。敌人在时刻 11aia_i 出发,时刻 pp 位于路径上的第 pp 个节点。Marat 也在时刻 11xx 出发,每时刻可以移动到一个相邻节点或停留。Marat 不能在任何时刻与任何敌人在同一顶点相遇(可以在边上相遇)。敌人到达 bib_i 后就不再存在。求 Marat 到达 yy 的最早时刻,或输出 1-1


    关键观察

    1. 答案上界

    如果答案不是 1-1,则答案 2n+1\le 2n+1

    证明:设最优路径为 s1=x,s2,,sk=ys_1=x, s_2, \dots, s_k=y。在时刻 n+1n+1,所有敌人都已到达工作地点(因为敌人路径长度 n\le n)。从 sn+1s_{n+1}sks_k 最多需要 nn 步,所以 k2n+1k \le 2n+1

    2. 坏时刻的定义

    (t,v)(t, v)的,如果在时刻 tt 有敌人在顶点 vv

    性质:所有坏时刻的总数 $\le \sum_{i=1}^m \text{len}(path(a_i, b_i)) \le n \times m$。

    因为每个敌人的路径长度 n\le n,共有 mm 个敌人。


    动态规划思路

    朴素 DP O(n2)O(n^2)

    定义 dpv,tdp_{v,t} 表示 Marat 能否在时刻 tt 位于顶点 vv

    • 初始:dpx,1=1dp_{x,1}=1
    • 转移:如果 dpv,t=1dp_{v,t}=1,则对于 vv 的所有邻居 uu 以及 vv 本身,dpu,t+1=1dp_{u,t+1}=1
    • 如果 (t+1,v)(t+1, v) 是坏时刻,则 dpv,t+1=0dp_{v,t+1}=0

    答案是最小的 tt 使得 dpy,t=1dp_{y,t}=1

    优化到 O(n×m)O(n \times m)

    核心思想

    维护当前时刻 tt 可到达的顶点集合 StS_t,而不是整个 dpdp 表。

    更新规则

    StS_tSt+1S_{t+1}

    1. 坏时刻顶点:如果 (t+1,v)(t+1, v) 是坏时刻,则 vSt+1v \notin S_{t+1}
    2. 继承:如果 vStv \in S_t,则 vSt+1v \in S_{t+1}(可以停留)
    3. 邻居传播:如果 vv 的某个邻居 uStu \in S_t,则 vSt+1v \in S_{t+1}(可以从 uu 走过来)

    两类特殊顶点

    • 类型1:在时刻 t+1t+1 有敌人的顶点(坏时刻顶点)
    • 类型2:不在 StS_t 中,但有邻居在 StS_t 中的顶点

    算法实现细节

    数据结构

    • ok[v]:标记顶点 vv 是否在当前集合 StS_t
    • was[v]:记录顶点 vv 最后一次被加入集合的时刻(用于去重)
    • cur[v]:标记在下一时刻是否有敌人到达 vv(临时标记)

    处理流程

    对于时刻 t=0,1,2,t = 0, 1, 2, \dots

    // 1. 标记当前时刻的敌人位置
    if (t < block.size()) {
        for (auto &u : block[t]) cur[u] = true;
    }
    
    // 2. 找出下一时刻能到达的顶点
    vi nxt;
    for (auto &v : q) {  // q 是候选队列
        if (ok[v] || cur[v] || was[v] == t) continue;
        was[v] = t;
        bool nei = 0;
        for (auto &u : g[v]) nei |= ok[u];  // 检查是否有邻居在当前集合
        if (t && !nei) continue;  // 时刻 0 不需要邻居(起点特殊情况)
        nxt.push_back(v);
    }
    
    // 3. 移除当前时刻有敌人的顶点
    q.clear();
    if (t < block.size()) {
        for (auto &u : block[t]) {
            cur[u] = false;
            if (ok[u]) ok[u] = false;
            q.push_back(u);  // 这些顶点作为下一时刻的候选
        }
    }
    
    // 4. 添加新到达的顶点
    for (auto &v : nxt) {
        ok[v] = true;
        for (auto &u : g[v]) {
            if (!ok[u]) q.push_back(u);  // 邻居作为候选
        }
    }
    
    // 5. 检查答案
    if (ok[y]) {
        cout << t + 1 << '\n';
        return;
    }
    
    // 6. 终止条件
    if (t > (int)block.size() && q.empty()) {
        cout << "-1\n";
        return;
    }
    

    复杂度分析

    每个顶点最多可以进入集合 m+1m+1 次(每次离开后可能再次进入,最多离开 mm 次)。每次进入时,会将其所有邻居加入候选队列。

    • 每个顶点度数之和 = 2n2n
    • 总候选数 (m+1)×2n=O(n×m)\le (m+1) \times 2n = O(n \times m)
    • 坏时刻总数 n×m\le n \times m

    因此总时间复杂度 O(n×m)O(n \times m),空间复杂度 O(n+m)O(n + m)


    示例解释

    示例1

    n=4,m=1,x=1,y=4n=4, m=1, x=1, y=4,敌人从 4411

    • 敌人路径:43214 \to 3 \to 2 \to 1
    • 时刻 11:敌人在 44,Marat 在 11
    • 时刻 22:敌人在 33,Marat 可到 22
    • 时刻 33:敌人在 22,Marat 可到 33
    • 时刻 44:敌人在 11,Marat 可到 44(到达)

    输出 44

    示例2

    n=5,m=1,x=1,y=5n=5, m=1, x=1, y=5,敌人从 5511

    • 如果 Marat 直接走,时刻 2222 相遇
    • 11 等待 1 时刻:时刻 2211,时刻 3322,时刻 4433,时刻 5544,时刻 6655
    • 敌人时刻 2244333344225511,无冲突

    输出 66


    总结

    本题的核心技巧:

    1. 利用答案上界 2n+12n+1 限制搜索范围
    2. 维护可达顶点集合而非完整 DP 表
    3. 利用坏时刻总数 O(n×m)O(n \times m) 的性质
    4. 每个顶点最多进入集合 m+1m+1 次,保证总复杂度 O(n×m)O(n \times m)

    对于 n105,m200n \le 10^5, m \le 200 的数据范围,O(n×m)2×107O(n \times m) \le 2 \times 10^7 是可以接受的。

    • 1

    信息

    ID
    6560
    时间
    1000ms
    内存
    256MiB
    难度
    9
    标签
    递交数
    1
    已通过
    1
    上传者