[USACO07FEB] Cow Party S

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

往返距离等于 i 到 x 再加 x 到 i;原图从 x 跑一次 Dijkstra,反图再从 x 跑一次 Dijkstra,就能得到所有点的来回最短路。

OJ: luogu

题目 ID: P1821

难度:普及/提高-

标签:最短路图论

日期: 2026-06-20 03:25

题意

nn 头牛都要去编号为 xx 的农场参加派对。
图是有向图,每条边有长度。

每头牛都要:

  1. 从自己家走到 xx
  2. 参加完派对后再从 xx 走回自己家

两段都走最短路。
要求所有牛的“往返最短路长度”里的最大值。

反图直觉图

这张图展示了为什么“求 ixi \to x”可以改成“在反图里求 xix \to i”:

digraph G {
  rankdir=LR;
  subgraph cluster0 {
    label="原图";
    color=gray;
    a [label="i"];
    b [label="..."];
    c [label="x"];
    a -> b -> c;
  }
  subgraph cluster1 {
    label="反图";
    color=gray;
    d [label="x"];
    e [label="..."];
    f [label="i"];
    d -> e -> f;
  }
}

原图里从 ii 走到 xx 的一条路径,边全部反过来后,就变成反图里从 xx 走到 ii 的路径。

思路

先看一个最直接的小数据暴力:

cpp
// brute.cpp:用 Floyd 求任意两点最短路。
// 小数据下可以直接求出 i -> x 和 x -> i,再枚举最大往返距离。
#include <bits/stdc++.h>
using namespace std;

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

int n, m, x;
long long dist_arr[MAXN][MAXN];

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

    cin >> n >> m >> x;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) {
                dist_arr[i][j] = 0;
            }
            else {
                dist_arr[i][j] = INF;
            }
        }
    }

    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        if (w < dist_arr[u][v]) {
            dist_arr[u][v] = w;
        }
    }

    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (dist_arr[i][k] + dist_arr[k][j] < dist_arr[i][j]) {
                    dist_arr[i][j] = dist_arr[i][k] + dist_arr[k][j];
                }
            }
        }
    }

    long long answer = 0;
    for (int i = 1; i <= n; i++) {
        answer = max(answer, dist_arr[i][x] + dist_arr[x][i]);
    }

    cout << answer << '\n';

    return 0;
}

暴力做法可以用 Floyd:

  1. 先求任意两点最短路
  2. 对每个点 ii 计算 dist[i][x]+dist[x][i]dist[i][x] + dist[x][i]
  3. 取最大值

但这题真正的关键不是 Floyd,而是把“去程”和“回程”拆开。

对于每头牛 ii

  • 去派对:ixi \to x
  • 回家:xix \to i

回家这一段很简单,直接在原图上从 xx 跑一次 Dijkstra,就能得到所有 xix \to i 的最短路。

难点是去派对这一段:我们想要的是所有 ixi \to x

这里有一个常用技巧:

  • 把所有边反向,建一张反图

这样原图里:

  • ixi \to \dots \to x

就会变成反图里:

  • xix \to \dots \to i

于是“所有点到 xx 的最短路”,就变成了“反图里从 xx 出发的单源最短路”。

所以整题只要跑两次 Dijkstra:

  1. 原图从 xx 出发,得到 xix \to i
  2. 反图从 xx 出发,得到 ixi \to x

最后枚举每个点 ii

  • distgo[i]+distback[i]dist_{go}[i] + dist_{back}[i]

取最大值即可。

代码

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

const int MAXN = 1000 + 5;
const int MAXM = 100000 + 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, x;

// 原图:求 x -> i 的最短路
int head1[MAXN], to1[MAXM], nxt1[MAXM], w1[MAXM], cnt1;
// 反图:求 i -> x 的最短路,等价于在反图里求 x -> i
int head2[MAXN], to2[MAXM], nxt2[MAXM], w2[MAXM], cnt2;

long long dist_go[MAXN];
long long dist_back[MAXN];
bool vis[MAXN];

void init_graph() {
    cnt1 = 0;
    cnt2 = 0;
    for (int i = 1; i <= n; i++) {
        head1[i] = 0;
        head2[i] = 0;
    }
}

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

// 在给定的图上,从 start 跑一次 Dijkstra。
void dijkstra(int start, int head[], int to[], int nxt[], int w[], long long dist[]) {
    for (int i = 1; i <= n; i++) {
        dist[i] = INF;
        vis[i] = false;
    }

    priority_queue<HeapNode> pq;
    dist[start] = 0;
    pq.push({start, 0});

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

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

        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            long long nd = dist[u] + w[i];
            if (nd < dist[v]) {
                dist[v] = nd;
                pq.push({v, nd});
            }
        }
    }
}

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

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

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

        add_edge(head1, to1, nxt1, w1, cnt1, u, v, len);
        add_edge(head2, to2, nxt2, w2, cnt2, v, u, len);
    }

    dijkstra(x, head1, to1, nxt1, w1, dist_go);
    dijkstra(x, head2, to2, nxt2, w2, dist_back);

    long long answer = 0;
    for (int i = 1; i <= n; i++) {
        answer = max(answer, dist_go[i] + dist_back[i]);
    }

    cout << answer << '\n';

    return 0;
}

复杂度

两次堆优化 Dijkstra:

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

最后扫一遍所有点:

  • O(N)O(N)

总复杂度:

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

空间复杂度:

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

总结

这题最值得记住的是反图这个转换:

  • 求“所有点到某个固定点”的最短路
  • 可以改成“反图里从这个固定点出发”的单源最短路

所以本题本质上是:

  1. 原图一次 Dijkstra
  2. 反图一次 Dijkstra
  3. 合并去程和回程答案

一图流解析

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

一图流解析