题解:洛谷 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
【核心思想】
-
问题分析:明明要买 \(B\) 样东西,每样价格 \(A\) 元。购买第 \(I\) 样后再买第 \(J\) 样可享受优惠 \(K_{I,J}\)(\(K_{I,J} = K_{J,I}\))。求最少花费。这是一个最小生成树(MST)问题,核心在于将"单独购买"建模为虚拟节点,将优惠关系建模为边,求连接所有物品的最小成本。
-
算法选择:
- Kruskal 算法:构建带虚拟节点的图,求最小生成树
- 并查集:维护连通性,实现 \(O(\alpha(n))\) 的合并与查询
-
关键步骤:
- 读入数据:读取 \(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\)
-
时间/空间复杂度:
- 时间复杂度:\(O(B^2 \log B^2) = O(B^2 \log B)\),边数为 \(O(B^2)\),排序主导
- 空间复杂度:\(O(B^2)\),存储所有边
-
最小生成树与虚拟节点的核心思想:
- 问题转化:每个物品可以"单独购买"(成本 \(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
浙公网安备 33010602011771号