利用无向图距离对称性,从每个喜欢的牧场各跑一次 Dijkstra,把到所有点的距离累加后取总和最小的牧场。
OJ: luogu
题目 ID: P2935
难度:普及/提高-
标签:最短路图论堆
日期: 2026-06-20 03:10
题意
给一个带正边权的无向图,其中有
要求找一个牧场
- 从
到所有喜欢的牧场的距离平均值最小
输出这个最优牧场的编号。
因为平均值分母
- 找一个点,使它到所有喜欢牧场的距离总和最小
样例里牧场 10 和 11 的总和一样,但输出是 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 先求出任意两点最短路,然后枚举每个候选牧场
全部加起来,取最小值即可。
这个写法很好理解,但它更像“直接把题做完”,没有抓住这道题真正想练的最短路模型。
这题更关键的观察有两个:
1. 平均值最小,等价于总和最小
因为每个候选点都要除以同一个
2. 无向图里距离是对称的
对于任意两个点
所以如果我们想知道某个候选点
- 从每个喜欢点出发,求它到
的最短路 - 再把这些距离累加起来
于是就不必“枚举候选点再跑最短路”,而是改成:
- 对每个喜欢的牧场跑一次 Dijkstra
- 把这次最短路结果累加到所有点的答案里
- 最后扫描一遍,找距离总和最小的牧场
这样做的好处是:
- 图是稀疏图,边权全为正,Dijkstra 很合适
- 如果喜欢的牧场数量
比 小,那么比“对每个点都跑一次最短路”更省
代码
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;
}复杂度
设牧场数为
每次 Dijkstra 的复杂度是:
一共跑
空间复杂度:
总结
这题最值得记住的不是 Dijkstra 模板本身,而是前面的两步转化:
- 平均值最小等价于总和最小
- 无向图距离对称,所以可以从喜欢点反向出发统计
这样整题就从“枚举一个睡觉点,算它到很多目标点的距离”变成了:
- 多次单源最短路
- 距离累加
是很典型的“先换比较对象,再换枚举方向”的题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
