题解:洛谷 P1828 香甜的黄油
【题目来源】
洛谷:P1828 [USACO3.2] 香甜的黄油 Sweet Butter - 洛谷
【题目描述】
Farmer John 发现了做出全威斯康辛州最甜的黄油的方法:糖。
把糖放在一片牧场上,他知道 \(N\) 只奶牛会过来舔它,这样就能做出能卖好价钱的超甜黄油。当然,他将付出额外的费用在奶牛上。
Farmer John 很狡猾。像以前的 Pavlov,他知道他可以训练这些奶牛,让它们在听到铃声时去一个特定的牧场。他打算将糖放在那里然后下午发出铃声,以至他可以在晚上挤奶。
Farmer John 知道每只奶牛都在各自喜欢的牧场(一个牧场不一定只有一头牛)。给出各头牛在的牧场和牧场间的路线,找出使所有牛到达的路程和最短的牧场(他将把糖放在那)。
【输入】
第一行包含三个整数 \(N,P,C\),分别表示奶牛数、牧场数和牧场间道路数。
第二行到第 \(N+1\) 行,每行一个整数,其中第 \(i\) 行的整数表示第 \(i-1\) 头奶牛所在的牧场号。
第 \(N+2\) 行到第 \(N+C+1\) 行,每行包含三个整数 \(A,B,D\),表示牧场号为 \(A\) 和 \(B\) 的两个牧场之间有一条长度为 \(D\) 的双向道路相连。
【输出】
输出一行一个整数,表示奶牛必须行走的最小的距离和。
【输入样例】
3 4 5
2
3
4
1 2 1
1 3 5
2 3 7
2 4 3
3 4 5
【输出样例】
8
【核心思想】
-
问题分析:给定 \(P\) 个牧场和 \(C\) 条双向道路(带权),以及 \(N\) 头奶牛各自所在的牧场编号。需要选择一个牧场作为集合点(放糖的地方),使得所有奶牛从各自牧场出发到达该牧场的最短距离之和最小。本质是一个多源最短路径问题,可以转换为对每个可能的集合点运行单源最短路径算法(如 Dijkstra 或 SPFA),然后累加所有奶牛到该点的距离,取最小值。
-
算法选择:
- SPFA 求单源最短路:由于 \(P \le 800\),\(C \le 1450\),图规模较小,可以对每个牧场作为源点运行一次 SPFA,计算该点到所有奶牛所在牧场的最短距离之和。SPFA 实现简单,适合处理稀疏图。
- 复杂度:\(O(P \cdot (C + P))\),\(P \le 800\),完全可行。
-
关键步骤:
- 读入与建图:读取 \(N, P, C\),存储每头牛所在的牧场编号到数组
pos;建立无向图(每条道路加两条有向边)。 - 枚举源点:对每个牧场 \(i\)(\(1 \le i \le P\)):
- 运行 SPFA 计算从 \(i\) 到所有牧场的最短距离
dist。 - 累加
dist[pos[k]](\(k=1..N\))得到总距离。 - 取所有 \(i\) 中总距离的最小值。
- 运行 SPFA 计算从 \(i\) 到所有牧场的最短距离
- 输出:最小总距离。
- 读入与建图:读取 \(N, P, C\),存储每头牛所在的牧场编号到数组
-
时间/空间复杂度:
- 时间复杂度:\(O(P \times (C + P))\),\(P \le 800, C \le 1450\),约 \(800 \times 2250 = 1.8 \times 10^6\),非常快。
- 空间复杂度:\(O(P + C)\),存储图和距离数组。
-
SPFA + 枚举:
- SPFA:适用于含负权边的图,但本题边权为正,也可用 Dijkstra。SPFA 的平均性能良好,实现简单。
- 枚举源点:因为 \(P\) 不大,直接枚举所有可能的集合点,对每个点求最短路,然后计算总距离。这是最直接的思路,且容易实现。
- 剪枝/优化:如果某次 SPFA 计算出距离和已经大于当前最优值,可以提前停止(但代码中未实现,因 \(P\) 小不影响)。
- 适用场景:适用于“多点汇聚”类问题,如物流配送中心选址、通信基站选址等。
【算法标签】
普及 #SPFA
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 810; // 最大节点数(牧场数)
const int M = 3010; // 最大边数(道路数)
const int INF = 1e9; // 无穷大值
int h[N]; // 邻接表头指针数组
int w[M]; // 存储边的权重
int e[M]; // 存储边的终点节点
int ne[M]; // 存储下一条边的索引
int idx; // 当前边的索引
int n, p, c; // n:奶牛数量, p:牧场数量, c:道路数量
int dist[N]; // 从源点到各点的最短距离
int que[M]; // SPFA算法队列
int pos[N]; // 存储每头奶牛所在的牧场编号
bool st[N]; // 标记节点是否在队列中
/**
* 添加无向边到邻接表
* @param a 边的起点
* @param b 边的终点
* @param c 边的权重(距离)
*/
void add(int a, int b, int c)
{
e[idx] = b; // 记录边的终点
w[idx] = c; // 记录边的权重
ne[idx] = h[a]; // 新边指向原头节点
h[a] = idx; // 更新头节点指向新边
idx++; // 边索引递增
}
/**
* SPFA算法求单源最短路径
* @param s0 源点(牧场编号)
* @return 所有奶牛从源点出发的总距离,如果不可达返回INF
*/
int spfa(int s0)
{
// 初始化距离数组为无穷大
fill(dist, dist + N, INF);
// 初始化队列
int hh = 0, tt = 0; // 队列头尾指针
dist[s0] = 0; // 源点到自身距离为0
que[tt++] = s0; // 源点入队
// 循环队列处理:防止数组越界
if (tt == M)
{
tt = 0;
}
st[s0] = true; // 标记源点在队列中
// SPFA主循环
while (hh != tt)
{
// 取出队首节点
int t = que[hh++];
// 循环队列处理
if (hh == M)
{
hh = 0;
}
st[t] = false; // 标记节点已出队
// 遍历当前节点的所有邻接边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接节点
// 松弛操作:如果通过当前节点可以缩短到j的距离
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i]; // 更新最短距离
// 如果j不在队列中,将其加入队列
if (st[j] == false)
{
que[tt++] = j;
// 循环队列处理
if (tt == M)
{
tt = 0;
}
st[j] = true; // 标记节点在队列中
}
}
}
}
// 计算所有奶牛从源点出发的总距离
int res = 0;
for (int i = 1; i <= n; i++)
{
int j = pos[i]; // 第i头奶牛所在的牧场
// 如果存在不可达的牧场,返回无穷大
if (dist[j] >= INF)
{
return INF;
}
res += dist[j]; // 累加距离
}
return res;
}
int main()
{
// 输入奶牛数、牧场数、道路数
cin >> n >> p >> c;
// 初始化邻接表头指针为-1(表示空)
memset(h, -1, sizeof(h));
// 输入每头奶牛所在的牧场编号
for (int i = 1; i <= n; i++)
{
cin >> pos[i];
}
// 输入道路信息并构建无向图
while (c--)
{
int a, b, v;
cin >> a >> b >> v;
// 添加双向边(无向图)
add(a, b, v);
add(b, a, v);
}
// 初始化最小总距离为无穷大
int ans = INF;
// 枚举每个可能的聚会地点(牧场)
for (int i = 1; i <= p; i++)
{
// 重置队列标记数组
memset(st, false, sizeof(st));
// 计算以当前牧场为聚会地点的总距离,更新最小值
ans = min(ans, spfa(i));
}
// 输出最小总距离
cout << ans << endl;
return 0;
}
【运行结果】
3 4 5
2
3
4
1 2 1
1 3 5
2 3 7
2 4 3
3 4 5
8
浙公网安备 33010602011771号