[GXOI/GZOI2019] 旅行者

[GXOI/GZOI2019] 旅行者

题目描述

J 国有 $n$ 座城市,这些城市之间通过 $m$ 条单向道路相连,已知每条道路的长度。

一次,居住在 J 国的 Rainbow 邀请 Vani 来作客。不过,作为一名资深的旅行者,Vani 只对 J 国的 $k$ 座历史悠久、自然风景独特的城市感兴趣。  
为了提升旅行的体验,Vani 想要知道他感兴趣的城市之间「两两最短路」的最小值(即在他感兴趣的城市中,最近的一对的最短距离)。

也许下面的剧情你已经猜到了—— Vani 这几天还要忙着去其他地方游山玩水,就请你帮他解决这个问题吧。

输入格式

每个测试点包含多组数据,第一行是一个整数 $T$,表示数据组数。注意各组数据之间是互相独立的。

对于每组数据,第一行包含三个正整数 $n,m,k$,表示 J 国的 $n$ 座城市(从 $1 \sim n$ 编号),$m$ 条道路,Vani 感兴趣的城市的个数 $k$。

接下来 $m$ 行,每行包括 $3$ 个正整数 $x,y,z$,表示从第 $x$ 号城市到第 $y$ 号城市有一条长度为 $z$ 的单向道路。注意 $x,y$ 可能相等,一对 $x,y$ 也可能重复出现。

接下来一行包括 $k$ 个正整数,表示 Vani 感兴趣的城市的编号。

输出格式

输出文件应包含 $T$ 行,对于每组数据,输出一个整数表示 $k$ 座城市之间两两最短路的最小值。

输入输出样例 #1

输入 #1

2
6 7 3
1 5 3
2 3 5
1 4 3
5 3 2
4 6 5
4 3 7
5 6 4
1 3 6
7 7 4
5 3 10
6 2 7
1 2 6
5 4 2
4 3 4
1 7 3
7 2 4
1 2 5 3

输出 #1

5
6

说明/提示

样例解释

对于第一组数据,$1$ 到 $3$ 最短路为 $5$;$1$ 到 $6$ 最短路为 $7$;$3,6$ 无法到达,所以最近的两点为 $1,3$,最近的距离为 $5$。

对于第二组数据,$1$ 到 $2$ 最短路为 $6$;$5$ 到 $3$ 最短路为 $6$;其余的点均无法互相达,所以最近的两点为 $1,2$ 和 $5,3$,最近的距离为 $6$。

数据范围

$2 \le k \le n$,$1 \le x,y \le n$,$1 \le z \le 2 \times 10^9$,$T \leq 5$。

测试点编号 n 的规模 m 的规模 约定
1 $\leq$ 1,000 $\leq$ 5,000
2 $\leq$ 1,000 $\leq$ 5,000
3 $\leq$ 100,000 $\leq$ 500,000 保证数据为有向无环图
4 $\leq$ 100,000 $\leq$ 500,000 保证数据为有向无环图
5 $\leq$ 100,000 $\leq$ 500,000 保证数据为有向无环图
6 $\leq$ 100,000 $\leq$ 500,000
7 $\leq$ 100,000 $\leq$ 500,000
8 $\leq$ 100,000 $\leq$ 500,000
9 $\leq$ 100,000 $\leq$ 500,000
10 $\leq$ 100,000 $\leq$ 500,000

2024-12-18 管理员注:在测试点五中可能存在自环

 

解题思路

  最暴力的做法就是对每个感兴趣节点作为源点跑最短路,然后对到其他感兴趣节点的距离取最小值,时间复杂度为 $O(km\log{n})$,超时。

  不妨假设在感兴趣节点中具有最短路径的两个端点是 $x$ 和 $y$,最短路径为 $x \to v_1 \to v_2 \to \cdots v_m \to y$($x$ 和 $y$ 可以看作是 $v_0$ 和 $v_{m+1}$)。容易知道 $v_i$ 一定不是感兴趣节点,否则会有更短的路径,矛盾了。此时考虑路径上任意一条边 $(v_i, v_{i+1})$ 的端点 $v_i$ 和 $v_{i+1}$,距离 $v_i$ 最近的感兴趣节点一定是 $x$,距离 $v_{i+1}$ 最近的感兴趣节点一定是 $y$。同样的,如果 $x$ 不是距离 $v_i$ 最近的感兴趣节点,那么一定存在另外一个感兴趣节点 $x'$ 距离 $v_i$ 更短,从而有更短的路径,矛盾。同理分析 $v_{i+1}$。

  这启发我们可以先分求出感兴趣节点到每个点 $u$ 的最短距离,记作 $d_1[u]$,同时维护 $c_1[u]$ 表示到达 $u$ 最近的感兴趣节点;以及每个点 $u$ 到感兴趣节点的最短距离,记作 $d_2[u]$,同时维护 $c_2[u]$ 表示 $u$ 到达的最近的感兴趣节点。然后枚举每条边 $(u,v,w)$,如果 $c_1[u] \ne c_2[v]$,那么感兴趣节点 $c_1[u]$ 到 $c_2[v]$ 的最短距离就是 $d_1[u] + w + d_2[v]$。由于在感兴趣节点中最短路径不经过非感兴趣节点,因此通过枚举每条边一定能求得两个感兴趣节点中的最短路径。

  剩下的问题是,如何求出感兴趣节点到每个点的最短距离,这个只需将所有感兴趣节点作为源点跑一遍 dijkstra 即可。如何求出每个点到感兴趣节点的最短距离,由于 dijkstra 求的是从(多个)源点出发到单个节点的最短距离,现在需要求的是单个节点到多个节点的最短距离。因此需要对原图建一个反图(即反转每条边的方向),然后在反图上以所有感兴趣节点作为源点跑一遍 dijkstra。

  AC 代码如下,时间复杂度为 $O(m \log{n})$:

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

typedef long long LL;

const int N = 1e5 + 5, M = 1e6 + 5;
const LL INF = 0x3f3f3f3f3f3f3f3f;

int n, m, k;
int h1[N], h2[N], e[M], wt[M], ne[M], idx;
int a[N];
LL d1[N], d2[N];
int c1[N], c2[N];
bool vis[N];

void add(int *h, int u, int v, int w) {
    e[idx] = v, wt[idx] = w, ne[idx] = h[u], h[u] = idx++;
}

void dijkstra(int *h, LL *d, int *c) {
    priority_queue<array<LL, 2>, vector<array<LL, 2>>, greater<array<LL, 2>>> pq;
    memset(d, 0x3f, n + 1 << 3);
    for (int i = 0; i < k; i++) {
        pq.push({0, a[i]});
        d[a[i]] = 0;
        c[a[i]] = a[i];
    }
    memset(vis, 0, n + 1);
    while (!pq.empty()) {
        auto p = pq.top();
        pq.pop();
        if (vis[p[1]]) continue;
        vis[p[1]] = true;
        for (int i = h[p[1]]; i != -1; i = ne[i]) {
            int v = e[i], w = wt[i];
            if (d[v] > p[0] + w) {
                d[v] = p[0] + w;
                c[v] = c[p[1]];
                pq.push({d[v], v});
            }
        }
    }
}

void solve() {
    cin >> n >> m >> k;
    idx = 0;
    memset(h1, -1, sizeof(h1));
    memset(h2, -1, sizeof(h2));
    while (m--) {
        int u, v, w;
        cin >> u >> v >> w;
        add(h1, u, v, w);
        add(h2, v, u, w);
    }
    for (int i = 0; i < k; i++) {
        cin >> a[i];
    }
    dijkstra(h1, d1, c1);
    dijkstra(h2, d2, c2);
    LL ret = INF;
    for (int i = 1; i <= n; i++) {
        for (int j = h1[i]; j != -1; j = ne[j]) {
            int u = i, v = e[j], w = wt[j];
            if (c1[u] != c2[v]) ret = min(ret, d1[u] + w + d2[v]);
        }
    }
    cout << ret << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    
    return 0;
}

  再提供另外一个很难想到的做法。

  出发点是对每个感兴趣节点作为源点跑最短路很慢,因此能不能像上面做法那样选择多个感兴趣节点作为源点。我们把感兴趣节点划分成两个集合 $S$ 和 $T$(先不用管怎么划分),然后求这两个集合的最短距离。下面以 $S \to T$ 为例($T \to S$ 同理),以集合 $S$ 中的节点作为源点跑 dijkstra,得到 $d[u]$ 表示从集合 $S$ 中的节点出发到达点 $u$ 的最短距离,那么 $S \to T$ 的最短距离就是 $\min\limits_{u \in T}\{d[u]\}$。

  和上面方法一样,不妨假设在感兴趣节点中具有最短路径的两个端点是 $x$ 和 $y$,只要将 $x$ 和 $y$ 分别划分到 $S$ 或 $T$ 两个不同的集合,那么这两个集合的最短距离就是感兴趣节点中的最短距离。因此现在的问题是应该怎么划分。方法还是挺难想到的,就是根据节点编号在二进制下的第 $i \, (0 \leq i \leq \left\lfloor \log{n} \right\rfloor )$ 位进行划分,如果 $u$ 在二进制下的第 $i$ 位是 $0$ 则划分到 $S$,否则划分到 $T$。因为 $x \ne y$,因此一定存在某位 $x$ 和 $y$ 不同,被划分到两个不同的集合。

  因此做法就是枚举每个二进制位,把感兴趣节点划分成两个集合,求两个集合的最短距离。

  AC 代码如下,时间复杂度为 $O(m \log^2{n})$:

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

typedef long long LL;

const int N = 1e5 + 5, M = 5e5 + 5;
const LL INF = 0x3f3f3f3f3f3f3f3f;

int h[N], e[M], wt[M], ne[M], idx;
int a[N];
LL d[N];
bool vis[N];

void add(int u, int v, int w) {
    e[idx] = v, wt[idx] = w, ne[idx] = h[u], h[u] = idx++;
}

LL dijkstra(vector<int> &p, vector<int> &q) {
    priority_queue<array<LL, 2>, vector<array<LL, 2>>, greater<array<LL, 2>>> pq;
    memset(d, 0x3f, sizeof(d));
    for (auto &x : p) {
        pq.push({0, x});
        d[x] = 0;
    }
    memset(vis, 0, sizeof(vis));
    while (!pq.empty()) {
        auto p = pq.top();
        pq.pop();
        if (vis[p[1]]) continue;
        vis[p[1]] = true;
        for (int i = h[p[1]]; i != -1; i = ne[i]) {
            int v = e[i], w = wt[i];
            if (d[v] > p[0] + w) {
                d[v] = p[0] + w;
                pq.push({d[v], v});
            }
        }
    }
    return d[*min_element(q.begin(), q.end(), [&](int i, int j) {
        return d[i] < d[j];
    })];
}

void solve() {
    int n, m, k;
    cin >> n >> m >> k;
    idx = 0;
    memset(h, -1, sizeof(h));
    while (m--) {
        int u, v, w;
        cin >> u >> v >> w;
        add(u, v, w);
    }
    for (int i = 0; i < k; i++) {
        cin >> a[i];
    }
    LL ret = INF;
    for (int i = 0; 1 << i <= n; i++) {
        vector<vector<int>> p(2);
        for (int j = 0; j < k; j++) {
            p[a[j] >> i & 1].push_back(a[j]);
        }
        if (p[0].empty() || p[1].empty()) continue;
        ret = min(ret, dijkstra(p[0], p[1]));
        ret = min(ret, dijkstra(p[1], p[0]));
    }
    cout << ret << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t;
    cin >> t;
    while (t--) {
        solve();
    }
    
    return 0;
}

 

参考资料

  【题解】P5304 [GXOI/GZOI2019]旅行者(dijkstra,图论,最短路):https://www.cnblogs.com/xrkforces/p/luogu-P5304.html

  P5304 [GXOI/GZOI2019]旅行者 - 洛谷专栏:https://www.luogu.com.cn/article/nagswult

posted @ 2025-04-05 22:28  onlyblues  阅读(15)  评论(0)    收藏  举报
Web Analytics