[SDOI2010] 大陆争霸

GitHub跳转原题关系图返回列表

对每个城市分别维护“机器人最早能到城门的时间”和“所有前置发生器最晚被摧毁的时间”,城市真正被摧毁的时间是这两者的最大值,再用 Dijkstra 式过程按时间推进。

OJ: luogu

题目 ID: P2446

难度:提高+/省选-

标签:最短路图论思维

日期: 2026-06-20 05:10

题意

N 个城市和 M 条单向道路,从城市 1 出发,要摧毁城市 N

但进入某个城市之前,必须先摧毁维持它结界的所有发生器。
这些发生器分布在其他城市中。

机器人是无限的。
一旦机器人进入某个城市,就可以立刻自爆并摧毁这个城市里的一个目标,因此这个城市里的发生器也会在那一刻被摧毁。

问最短多久能摧毁城市 N

思路

先看一个更直观的小数据暴力:

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 55;
const long long INF = (1LL << 60);

int n, m;
vector<pair<int, int> > roads[MAXN];
vector<int> depend[MAXN];
long long answer[MAXN];
long long road_arrive[MAXN];
long long shield_need[MAXN];
int remain_need[MAXN];
bool vis[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        roads[i].clear();
        depend[i].clear();
    }

    for (int i = 1; i <= m; i++) {
        int u, v, len;
        cin >> u >> v >> len;
        roads[u].push_back(make_pair(v, len));
    }

    for (int city = 1; city <= n; city++) {
        int k;
        cin >> k;
        remain_need[city] = k;
        for (int j = 1; j <= k; j++) {
            int generator_city;
            cin >> generator_city;
            depend[generator_city].push_back(city);
        }
    }

    for (int i = 1; i <= n; i++) {
        answer[i] = INF;
        road_arrive[i] = INF;
        shield_need[i] = 0;
        vis[i] = false;
    }

    answer[1] = 0;

    // 更直接的小数据写法:
    // 每次暴力找“当前最早可以被摧毁的那个城市”。
    for (int step = 1; step <= n; step++) {
        int choose = 0;
        for (int city = 1; city <= n; city++) {
            if (vis[city]) {
                continue;
            }
            if (city == 1) {
                choose = 1;
                break;
            }
            if (remain_need[city] != 0) {
                continue;
            }
            if (road_arrive[city] >= INF / 2) {
                continue;
            }
            long long cand = max(road_arrive[city], shield_need[city]);
            if (cand < answer[city]) {
                answer[city] = cand;
            }
            if (choose == 0 || answer[city] < answer[choose]) {
                choose = city;
            }
        }

        vis[choose] = true;

        for (size_t i = 0; i < roads[choose].size(); i++) {
            int v = roads[choose][i].first;
            long long nd = answer[choose] + roads[choose][i].second;
            if (nd < road_arrive[v]) {
                road_arrive[v] = nd;
            }
        }

        for (size_t i = 0; i < depend[choose].size(); i++) {
            int v = depend[choose][i];
            remain_need[v]--;
            if (answer[choose] > shield_need[v]) {
                shield_need[v] = answer[choose];
            }
        }
    }

    cout << answer[n] << '\n';

    return 0;
}

brute.cpp 的想法很直接:

  1. 每次暴力枚举当前“最早可能被摧毁”的那个城市
  2. 摧毁它之后:
    • 它会沿着道路给别的城市带来更早的到达时间
    • 它作为发生器,也会解开一批城市的结界限制
  3. 重复这个过程直到首都被摧毁

这个过程最贴题意,但大数据下用暴力找下一个城市太慢。

两个时间都要维护

对一个城市 v 来说,想真正摧毁它,必须同时满足两件事:

  1. 至少有一个机器人已经能到达它的城门
  2. 它的所有前置发生器都已经被摧毁

所以我们分别维护两个量:

  • road_arrive[v]:最早什么时候能有机器人到达 v
  • shield_need[v]:为了进入 v,它所有前置发生器最晚被摧毁的时间

那么城市 v 真正能被摧毁的最早时间就是:

  • max(road_arrive[v], shield_need[v])

这张图展示的就是这个关系:

flowchart LR
  A["road_arrive[v]"] --> C["capture[v] = max(A, B)"]
  B["shield_need[v]"] --> C

图里真正要看的,是“路走到了”还不够,结界也必须同时解开。
而如果结界先解开,机器人也可以之后再到;
如果机器人先到,理论上也可以在城外等到结界全部失效。

为什么可以像 Dijkstra 一样做

当一个城市 u 在时间 answer[u] 被摧毁之后,它只会带来两类影响:

  1. 沿道路更新别的城市的 road_arrive
  2. 作为发生器,更新它所控制城市的 shield_need

这两类信息都会让别的城市“更早可达”,不会让答案变差。
因此我们可以像 Dijkstra 那样,每次取当前最早能被摧毁的城市继续扩展。

具体做法:

  1. 城市 1 初始就在我方手中,时间记为 0

  2. 用优先队列维护当前候选的最早摧毁时间

  3. 每摧毁一个城市,就更新:

    • 它沿道路能到达的城市
    • 依赖它作为发生器的城市
  4. 如果某个城市已经满足:

    • 所有前置发生器都处理完
    • 并且已有机器人能到它门口

    就可以用 max(road_arrive, shield_need) 去尝试更新答案

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 3000 + 5;
const int MAXM = 70000 + 5;
const long long INF = (1LL << 60);

struct HeapNode {
    int u;
    long long dist;

    bool operator < (const HeapNode &other) const {
        return dist > other.dist;
    }
};

int n, m;
int head[MAXN], to[MAXM], nxt[MAXM], w[MAXM], edge_cnt;

long long answer[MAXN];      // 城市最早被摧毁的时间
long long road_arrive[MAXN]; // 已经摧毁的城市中,最早什么时候能派机器人到这个城市门口
long long shield_need[MAXN]; // 所有前置结界发生器被摧毁后的最晚时间
int remain_need[MAXN];       // 还有多少前置发生器没被摧毁
bool vis[MAXN];

vector<int> depend[MAXN];    // 城市 u 的发生器会影响哪些城市

void init_graph() {
    edge_cnt = 0;
    for (int i = 1; i <= n; i++) {
        head[i] = 0;
        depend[i].clear();
    }
}

void add_edge(int u, int v, int len) {
    edge_cnt++;
    to[edge_cnt] = v;
    w[edge_cnt] = len;
    nxt[edge_cnt] = head[u];
    head[u] = edge_cnt;
}

void try_update_city(int v, priority_queue<HeapNode> &pq) {
    if (vis[v]) {
        return;
    }
    if (remain_need[v] != 0) {
        return;
    }
    if (road_arrive[v] >= INF / 2) {
        return;
    }

    long long cand = max(road_arrive[v], shield_need[v]);
    if (cand < answer[v]) {
        answer[v] = cand;
        pq.push({v, cand});
    }
}

void solve() {
    for (int i = 1; i <= n; i++) {
        answer[i] = INF;
        road_arrive[i] = INF;
        shield_need[i] = 0;
        vis[i] = false;
    }

    priority_queue<HeapNode> pq;

    // 城市 1 已经被我方控制,时间记为 0。
    answer[1] = 0;
    pq.push({1, 0});

    while (!pq.empty()) {
        HeapNode cur = pq.top();
        pq.pop();

        int u = cur.u;
        if (vis[u]) {
            continue;
        }
        vis[u] = true;

        // 从已经摧毁的城市 u 出发,派机器人沿单向道路去其他城市门口。
        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            long long nd = answer[u] + w[i];
            if (nd < road_arrive[v]) {
                road_arrive[v] = nd;
                try_update_city(v, pq);
            }
        }

        // u 城市里的发生器已经被摧毁,它会解开一批城市的结界限制。
        for (size_t i = 0; i < depend[u].size(); i++) {
            int v = depend[u][i];
            remain_need[v]--;
            if (answer[u] > shield_need[v]) {
                shield_need[v] = answer[u];
            }
            try_update_city(v, pq);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    init_graph();

    for (int i = 1; i <= m; i++) {
        int u, v, len;
        cin >> u >> v >> len;
        add_edge(u, v, len);
    }

    for (int city = 1; city <= n; city++) {
        int k;
        cin >> k;
        remain_need[city] = k;
        for (int j = 1; j <= k; j++) {
            int generator_city;
            cin >> generator_city;
            depend[generator_city].push_back(city);
        }
    }

    solve();
    cout << answer[n] << '\n';

    return 0;
}

复杂度

设发生器依赖关系总数为 R

每条道路最多参与常数次松弛,每条依赖关系也只会处理一次。
优先队列复杂度为:

  • O((M+R)logN)O((M+R) \log N)

空间复杂度:

  • O(M+R+N)O(M + R + N)

总结

这题表面上像最短路,实际上多了一层“结界前置条件”。

最关键的想法就是把一个城市的最早摧毁时间拆成两部分:

  1. 机器人最早什么时候能到
  2. 前置发生器最晚什么时候才会全部被摧毁

最后取两者最大值。
一旦这个式子想清楚,整题就能顺着 Dijkstra 的推进方式写出来。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析