题解:洛谷 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

【核心思想】

  1. 问题分析:给定 \(P\) 个牧场和 \(C\) 条双向道路(带权),以及 \(N\) 头奶牛各自所在的牧场编号。需要选择一个牧场作为集合点(放糖的地方),使得所有奶牛从各自牧场出发到达该牧场的最短距离之和最小。本质是一个多源最短路径问题,可以转换为对每个可能的集合点运行单源最短路径算法(如 Dijkstra 或 SPFA),然后累加所有奶牛到该点的距离,取最小值。

  2. 算法选择

    • SPFA 求单源最短路:由于 \(P \le 800\)\(C \le 1450\),图规模较小,可以对每个牧场作为源点运行一次 SPFA,计算该点到所有奶牛所在牧场的最短距离之和。SPFA 实现简单,适合处理稀疏图。
    • 复杂度\(O(P \cdot (C + P))\)\(P \le 800\),完全可行。
  3. 关键步骤

    • 读入与建图:读取 \(N, P, C\),存储每头牛所在的牧场编号到数组 pos;建立无向图(每条道路加两条有向边)。
    • 枚举源点:对每个牧场 \(i\)\(1 \le i \le P\)):
      • 运行 SPFA 计算从 \(i\) 到所有牧场的最短距离 dist
      • 累加 dist[pos[k]]\(k=1..N\))得到总距离。
      • 取所有 \(i\) 中总距离的最小值。
    • 输出:最小总距离。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(P \times (C + P))\)\(P \le 800, C \le 1450\),约 \(800 \times 2250 = 1.8 \times 10^6\),非常快。
    • 空间复杂度:\(O(P + C)\),存储图和距离数组。
  5. 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
posted @ 2026-08-30 09:19  团爸讲算法  阅读(10)  评论(0)    收藏  举报