[USACO09JAN] Best Spot S

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

利用无向图距离对称性,从每个喜欢的牧场各跑一次 Dijkstra,把到所有点的距离累加后取总和最小的牧场。

OJ: luogu

题目 ID: P2935

难度:普及/提高-

标签:最短路图论

日期: 2026-06-20 03:10

题意

给一个带正边权的无向图,其中有 FF 个“喜欢的牧场”。

要求找一个牧场 xx,使得:

  • xx 到所有喜欢的牧场的距离平均值最小

输出这个最优牧场的编号。

因为平均值分母 FF 对所有候选点都一样,所以本质上就是:

  • 找一个点,使它到所有喜欢牧场的距离总和最小

样例里牧场 1011 的总和一样,但输出是 10,所以代码按编号从小到大扫描,保留最先达到最优值的点。

思路

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

cpp
// brute.cpp:直接 Floyd 求任意两点最短路。
// 规模小的时候很好理解,也适合拿来对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXP = 500 + 5;
const long long INF = (1LL << 60);

int p, f, c;
int fav[MAXP];
long long dist_arr[MAXP][MAXP];

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

    cin >> p >> f >> c;

    for (int i = 1; i <= f; i++) {
        cin >> fav[i];
    }

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

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

    for (int k = 1; k <= p; k++) {
        for (int i = 1; i <= p; i++) {
            for (int j = 1; j <= p; 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];
                }
            }
        }
    }

    int answer = 1;
    long long best_sum = INF;

    for (int i = 1; i <= p; i++) {
        long long cur_sum = 0;
        for (int j = 1; j <= f; j++) {
            cur_sum += dist_arr[i][fav[j]];
        }

        if (cur_sum < best_sum) {
            best_sum = cur_sum;
            answer = i;
        }
    }

    cout << answer << '\n';

    return 0;
}

暴力做法用 Floyd 先求出任意两点最短路,然后枚举每个候选牧场 xx,把:

dist(x,fav1)+dist(x,fav2)+...+dist(x,favF)dist(x, fav_1) + dist(x, fav_2) + ... + dist(x, fav_F)

全部加起来,取最小值即可。

这个写法很好理解,但它更像“直接把题做完”,没有抓住这道题真正想练的最短路模型。

这题更关键的观察有两个:

1. 平均值最小,等价于总和最小

因为每个候选点都要除以同一个 FF,所以比较平均值和比较总和完全一样。

2. 无向图里距离是对称的

对于任意两个点 u,vu, v

dist(u,v)=dist(v,u)dist(u, v) = dist(v, u)

所以如果我们想知道某个候选点 xx 到所有喜欢点的距离和,其实等价于:

  • 从每个喜欢点出发,求它到 xx 的最短路
  • 再把这些距离累加起来

于是就不必“枚举候选点再跑最短路”,而是改成:

  1. 对每个喜欢的牧场跑一次 Dijkstra
  2. 把这次最短路结果累加到所有点的答案里
  3. 最后扫描一遍,找距离总和最小的牧场

这样做的好处是:

  • 图是稀疏图,边权全为正,Dijkstra 很合适
  • 如果喜欢的牧场数量 FFPP 小,那么比“对每个点都跑一次最短路”更省

代码

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

const int MAXP = 500 + 5;
const int MAXC = 8000 * 2 + 5;
const long long INF = (1LL << 60);

struct Node {
    int u;
    long long dist;

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

int p, f, c;
int fav[MAXP];

int head[MAXP], to[MAXC], nxt[MAXC], weight_arr[MAXC], edge_cnt;
long long dist_arr[MAXP];
long long total_dist[MAXP];
bool vis[MAXP];

void init_graph() {
    edge_cnt = 0;
    for (int i = 1; i <= p; i++) {
        head[i] = 0;
        total_dist[i] = 0;
    }
}

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

// 从一个“喜欢的牧场”出发跑单源最短路,
// 再把它到所有点的距离累加到 total_dist 里。
void dijkstra(int start) {
    for (int i = 1; i <= p; i++) {
        dist_arr[i] = INF;
        vis[i] = false;
    }

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

    while (!pq.empty()) {
        Node 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_arr[u] + weight_arr[i];
            if (nd < dist_arr[v]) {
                dist_arr[v] = nd;
                pq.push({v, nd});
            }
        }
    }

    for (int i = 1; i <= p; i++) {
        total_dist[i] += dist_arr[i];
    }
}

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

    cin >> p >> f >> c;
    init_graph();

    for (int i = 1; i <= f; i++) {
        cin >> fav[i];
    }

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

    // 图是无向图,所以 dist(候选点, 喜欢点) = dist(喜欢点, 候选点)。
    // 与其枚举每个候选点都跑一次最短路,不如从每个喜欢点跑一次,
    // 再把距离累加到所有候选点上。
    for (int i = 1; i <= f; i++) {
        dijkstra(fav[i]);
    }

    int answer = 1;
    for (int i = 2; i <= p; i++) {
        if (total_dist[i] < total_dist[answer]) {
            answer = i;
        }
    }

    cout << answer << '\n';

    return 0;
}

复杂度

设牧场数为 PP,道路数为 CC

每次 Dijkstra 的复杂度是:

  • O((P+C)logP)O((P + C) log P)

一共跑 FF 次,所以总复杂度是:

  • O(F(P+C)logP)O(F (P + C) log P)

空间复杂度:

  • O(P+C)O(P + C)

总结

这题最值得记住的不是 Dijkstra 模板本身,而是前面的两步转化:

  1. 平均值最小等价于总和最小
  2. 无向图距离对称,所以可以从喜欢点反向出发统计

这样整题就从“枚举一个睡觉点,算它到很多目标点的距离”变成了:

  • 多次单源最短路
  • 距离累加

是很典型的“先换比较对象,再换枚举方向”的题。

一图流解析

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

一图流解析