题解:洛谷 P1194 买礼物

【题目来源】

洛谷:P1194 买礼物 - 洛谷

【题目描述】

又到了一年一度的明明生日了,明明想要买 \(B\) 样东西,巧的是,这 \(B\) 样东西价格都是 \(A\) 元。

但是,商店老板说最近有促销活动,也就是:

如果你买了第 \(I\) 样东西,再买第 \(J\) 样,那么就可以只花 \(K_{I,J}\) 元,更巧的是,\(K_{I,J}\) 竟然等于 \(K_{J,I}\)

现在明明想知道,他最少要花多少钱。

【输入】

第一行两个整数,\(A,B\)

接下来 \(B\) 行,每行 \(B\) 个数,第 \(I\) 行第 \(J\) 个为 \(K_{I,J}\)

我们保证 \(K_{I,J}=K_{J,I}\) 并且 \(K_{I,I}=0\)

特别的,如果 \(K_{I,J}=0\),那么表示这两样东西之间不会导致优惠。

注意 \(K_{I,J}\) 可能大于 \(A\)

【输出】

一个整数,为最小要花的钱数。

【输入样例】

1 1
0

【输出样例】

1

【核心思想】

  1. 问题分析:明明要买 \(B\) 样东西,每样价格 \(A\) 元。购买第 \(I\) 样后再买第 \(J\) 样可享受优惠 \(K_{I,J}\)\(K_{I,J} = K_{J,I}\))。求最少花费。这是一个最小生成树(MST)问题,核心在于将"单独购买"建模为虚拟节点,将优惠关系建模为边,求连接所有物品的最小成本。

  2. 算法选择

    • Kruskal 算法:构建带虚拟节点的图,求最小生成树
    • 并查集:维护连通性,实现 \(O(\alpha(n))\) 的合并与查询
  3. 关键步骤

    • 读入数据:读取 \(A\)(单独购买成本)和 \(B\)(物品数量)
    • 初始化并查集\(p[i] = i\)\(0\)\(B\)
    • 添加虚拟边:对每个物品 \(i\),添加边 \((0, i, A)\),表示单独购买该物品的成本
    • 添加优惠边:读入 \(K_{I,J}\),若 \(K_{I,J} > 0\),添加边 \((I, J, K_{I,J})\)
    • Kruskal 算法
      • 所有边按权重升序排序
      • 遍历每条边,若两端点不在同一连通分量则合并并累加成本
    • 输出结果\(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(B^2 \log B^2) = O(B^2 \log B)\),边数为 \(O(B^2)\),排序主导
    • 空间复杂度:\(O(B^2)\),存储所有边
  5. 最小生成树与虚拟节点的核心思想

    • 问题转化:每个物品可以"单独购买"(成本 \(A\))或"搭配购买"(成本 \(K_{I,J}\))。引入虚拟节点 \(0\) 表示"购买起点",单独购买等价于从 \(0\)\(i\) 的边,搭配购买等价于 \(i\)\(j\) 的边
    • MST 的最优性:最小生成树保证连接所有物品(含虚拟节点)的总成本最小。每个物品最终通过某条路径连接到虚拟节点(单独购买)或通过其他物品间接连接(搭配购买)
    • 并查集的高效性:路径压缩和按秩合并使每次操作近似 \(O(1)\),支持大规模图的连通性维护
    • \(K_{I,J} > A\) 的处理:Kruskal 贪心策略自动跳过高成本边,优先选择更便宜的连接方式
    • 适用于带基础成本的连接优化、图论建模类最小生成树问题

【算法标签】

普及 #生成树

【代码详解】

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

const int N = 1005;        // 最大节点数
const int M = N * N / 2;   // 最大边数(完全图边数)

// 边结构体
struct Edge
{
    int a, b, w;           // a:起点, b:终点, w:权重

    // 重载小于运算符,用于按权重排序
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
} e[M];                   // 边数组

int A;                     // 建立发电站的成本
int B;                     // 村庄数量
int cur;                   // 当前边数计数器
int ans;                   // 最小生成树的总成本
int cnt;                   // 已选择的边数(实际未使用)
int p[N];                  // 并查集数组,用于记录节点的父节点

/**
 * 并查集查找操作(带路径压缩)
 * @param x 要查找的节点
 * @return 节点x的根节点
 */
int find(int x)
{
    if (p[x] != x)
    {
        p[x] = find(p[x]);  // 路径压缩
    }
    return p[x];
}

int main()
{
    // 输入建立发电站的成本和村庄数量
    cin >> A >> B;

    // 初始化并查集,每个节点独立成集合
    for (int i = 0; i <= B; i++)
    {
        p[i] = i;
    }

    // 添加虚拟边:每个村庄建立发电站的选项
    // 将发电站视为节点0,建立发电站相当于连接村庄到节点0
    for (int i = 1; i <= B; i++)
    {
        e[++cur].a = 0;     // 起点为虚拟节点0(发电站)
        e[cur].b = i;       // 终点为村庄i
        e[cur].w = A;       // 权重为建立发电站的成本A
    }

    // 输入村庄之间架设电线的成本
    for (int i = 1; i <= B; i++)
    {
        for (int j = 1; j <= B; j++)
        {
            int x;
            cin >> x;

            // 跳过成本为0的情况(可能表示无法连接或自身)
            if (x == 0)
            {
                continue;
            }

            // 添加村庄之间的连接边
            e[++cur].a = i;
            e[cur].b = j;
            e[cur].w = x;
        }
    }

    // 调试输出(注释掉的代码)
    // for (int i=1; i<=cur; i++)
    //     cout << e[i].a << " " << e[i].b << " " << e[i].w << endl;

    // 将边按成本从小到大排序(Kruskal算法的关键步骤)
    sort(e + 1, e + cur + 1);

    // 使用Kruskal算法构建最小生成树
    for (int i = 1; i <= cur; i++)
    {
        int a = find(e[i].a);  // 查找起点的根节点
        int b = find(e[i].b);  // 查找终点的根节点
        int w = e[i].w;        // 当前边的权重

        // 如果两个节点不在同一个连通分量中
        if (a != b)
        {
            p[a] = b;         // 合并两个连通分量
            ans += w;          // 累加当前边的成本
        }
    }

    // 输出最小总成本
    cout << ans << endl;

    return 0;
}

【运行结果】

1 1
0
1
posted @ 2026-07-31 17:20  团爸讲算法  阅读(1)  评论(0)    收藏  举报