Day4
Prim
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
#include<queue>
#define m_p make_pair
using namespace std;
using ll = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
using i128 = __int128;
typedef pair<int,int> pii;
const int maxn = 2e5 + 10;
struct Node {
int u,v,w;
bool operator <(const Node& other) const {
return w < other.w;
}
};
Node e[maxn];
int n,m;
int fa[maxn];
void init() {
for(int i = 1; i <= n; i ++) {
fa[i] = i;
}
}
int find(int x) {
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
bool Union(int x,int y) {
int rootx = find(x),rooty = find(y);
if(rootx != rooty) {
fa[rootx] = rooty;
return true;
}
return false;
}
void solve()
{
cin >> n >> m;
for(int i = 1; i <= m; i ++) {
cin >> e[i].u >> e[i].v >> e[i].w;
}
sort(e + 1,e + m + 1);
init();
long long max_t = 0;
int e_cnt = 0;
for(int i = 1; i <= m; i ++) {
if(Union(e[i].u,e[i].v)) {
max_t = e[i].w;
e_cnt ++;
}
if(e_cnt == n - 1) break;
}
if(e_cnt == n - 1) {
cout << max_t << "\n";
} else {
cout << "-1\n";
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int t = 1;
while(t --) {
solve();
}
//system("pause");
return 0;
}
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cstring>
#include<queue>
#define m_p make_pair
using namespace std;
using ll = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
using i128 = __int128;
typedef pair<int,int> pii;
const int maxn = 2e5 + 10;
struct Node {
int to;
int weight;
bool operator >(const Node& other) const {
return weight > other.weight;
}
};
vector<Node> g[maxn];
int dist[maxn];
int vis[maxn];
int n,m;
priority_queue<pii,vector<pii>,greater<pii> > q;
void Prim() {
dist[1] = 0;
q.push({0,1});
long long total_w = 0;
int e_cnt = 0;
while (!q.empty()) {
auto [d,u] = q.top();
q.pop();
if(vis[u] == 1) continue;
vis[u] = 1;
total_w += d;
e_cnt ++;
for(auto& e : g[u]) {
int v = e.to,w = e.weight;
if(!vis[v] && w < dist[v]) {
dist[v] = w;
q.push({dist[v],v});
}
}
}
if(e_cnt <= n - 1) {
cout << "orz\n";
} else {
cout << total_w << "\n";
}
}
void solve()
{
memset(dist,0x3f3f3f3f,sizeof(dist));
memset(vis,0,sizeof(vis));
cin >> n >> m;
for(int i = 1; i <= m; i ++) {
int u,v,w;
cin >> u >> v >> w;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
Prim();
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
int t = 1;
while(t --) {
solve();
}
//system("pause");
return 0;
}
没问题!Prim 算法 就是专门为 邻接表 设计的。
它的逻辑和 Dijkstra 最短路非常像,核心思想是:“以此为据点,蚕食周围最近的领土”。
Prim 算法核心逻辑(配邻接表)
-
准备工作:
dist[i]:表示节点i距离“已生成的树” 最近的距离(注意:不是离起点的距离,而是离树集合的距离)。初始化为无穷大。vis[i]:标记节点i是否已经加入生成树。- 优先队列:用来快速找到当前离树最近的那个点。
-
开始种树:
- 随便选一个点(比如 1 号点),
dist[1] = 0,扔进队列。
- 随便选一个点(比如 1 号点),
-
循环扩张:
- 从队列里弹出一个离树最近的点
u。 - 如果
u已经在树里了(vis[u] == true),跳过。 - 标记
u进树,把它的距离加到总花费里。 - 关键一步(松弛):扫描
u的所有邻居v。- 如果
v还没进树,并且边(u, v)的权重 <dist[v](说明通过u连这个邻居更省钱),那就更新dist[v],并把{权值, v}扔进队列。
- 如果
- 从队列里弹出一个离树最近的点
Prim 算法代码模板 (C++)
#include <iostream>
#include <vector>
#include <queue>
#include <cstring> // for memset
using namespace std;
const int MAXN = 5005;
const int INF = 0x3f3f3f3f;
// 邻接表结构:存 {目标点, 权值}
struct Node {
int to;
int weight;
// 优先队列默认是大根堆,我们需要小根堆(权值小的在前)
// 所以重载小于号时反着写
bool operator>(const Node& other) const {
return weight > other.weight;
}
};
// 邻接表
vector<Node> g[MAXN];
// dist[i] 表示点 i 距离“生成树集合”的最短距离
int dist[MAXN];
// vis[i] 表示点 i 是否已经加入生成树
bool vis[MAXN];
int n, m;
int prim() {
// 1. 初始化
memset(dist, 0x3f, sizeof(dist)); // 全部设为无穷大
memset(vis, false, sizeof(vis));
// 小根堆:存 {权值, 点ID},权值越小越先出来
// 注意:这里利用 std::pair 的默认比较顺序(先比first),
// 所以我们存 {dist, u},这样不用自己写结构体重载了,更方便
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
// 2. 从起点 1 开始
dist[1] = 0;
pq.push({0, 1}); // {距离, 点ID}
int total_weight = 0;
int count = 0; // 记录加入了多少个点
while (!pq.empty()) {
// 取出当前离树最近的点
auto [d, u] = pq.top();
pq.pop();
// 如果这个点已经加入树了(懒惰删除机制),跳过
if (vis[u]) continue;
// 3. 正式把 u 加入生成树
vis[u] = true;
total_weight += d;
count++;
// 4. 考察 u 的所有邻居
for (auto& edge : g[u]) {
int v = edge.to;
int w = edge.weight;
// 如果 v 还没进树,且这条边比 v 目前记录的连接距离更短
if (!vis[v] && w < dist[v]) {
dist[v] = w; // 更新 v 到树的最短距离
pq.push({dist[v], v});
}
}
}
// 如果加入的点不到 n 个,说明图不连通
if (count < n) return -1;
return total_weight;
}
int main() {
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
// 无向图,双向加边
g[u].push_back({v, w});
g[v].push_back({u, w});
}
int result = prim();
if (result == -1) cout << "orz" << endl; // 连不通
else cout << result << endl;
return 0;
}
代码对比:Prim vs Dijkstra
你会发现 Prim 和 Dijkstra 的代码长得几乎一模一样!
唯一的、最关键的区别在第 4 步(松弛逻辑):
- Dijkstra (求最短路):
if (dist[u] + w < dist[v]) { // 累加:起点到u + u到v dist[v] = dist[u] + w; } - Prim (求最小生成树):
if (w < dist[v]) { // 不累加:只看这条边 u到v 有多短 dist[v] = w; }
总结
- 邻接表 \(\to\) 用 Prim。
- 边数组 \(\to\) 用 Kruskal。
- Prim 适合稠密图(边多,邻接表存起来不浪费)。
- Kruskal 适合稀疏图(边少,排序快)。
要想下次还能写出这种“逻辑清晰、代码复用率高”的代码,你需要训练一种“抽象与解耦”的思维方式。
简单来说,就是“一眼看穿题目在演戏,直接把它的面具撕下来”。
这里有 4 个具体的思维训练技巧,帮你复盘这次的成功经验:
1. 拒绝“CV大法”:学会函数复用 (Abstraction)
痛点:
很多初学者遇到这道题,第一反应是写两个 find 函数:find1 给男人用,find2 给女人用;写两个 union 函数:union1 和 union2。
这不仅代码长,而且改一个 bug 要改两处(比如你刚才那个 Union 里写死 fa1 的错误,如果拆成两个函数反而不容易发现)。
你的高光时刻:
你写了 find(x, fa[]) 和 Union(x, y, fa[], sz[])。
这一招叫“传参泛化”。
下次怎么做?
- 警觉信号:当你发现自己在复制粘贴一段代码,并且只是把里面的
a数组改成b数组时,立刻停下来! - 解决方案:把变化的数组(
fa,sz)提取出来,作为函数的参数传进去。 - 好处:逻辑只有一份,改一处全好,且代码极度简洁。
2. 数据“标准化”:把异常输入变成标准输入
痛点:
题目给你负数、给你字符串、给你坐标……如果你直接在逻辑里处理(比如在 Union 里判断正负),代码会变得很脏。
你的高光时刻:
你用 abs() 或者 -x 把女人的负数编号,强行变成了正数下标。
这一招叫“数据归一化”。
下次怎么做?
- 思维习惯:不管题目给的数据多奇葩(负数、很大的离散数、字符串名字),先在输入阶段把它转化成
1到N的整数。 - 例子:
- 给负数?取绝对值。
- 给字符串名字("Alice", "Bob")?用
map<string, int>映射成 1, 2, 3。 - 给二维坐标
(x, y)?用(x-1)*m + y映射成一维编号。
- 目的:让你的核心算法(并查集、DFS、BFS)永远只处理最干净的
1~N整数。
3. 剥离“故事外壳”:建立数学模型
痛点:
题目说“情侣”、“异性”、“朋友的朋友”。如果在代码变量命名里还纠结 boy_fa, girl_fa,思维容易受干扰。
本质还原:
- 朋友的朋友 \(\rightarrow\) 连通块(并查集)。
- 最多配对多少对 \(\rightarrow\) 木桶效应(取最小值)。
- 两家公司互不干扰 \(\rightarrow\) 两个独立的图。
下次怎么做?
- 读题时在草稿纸上画图,把所有“人”画成“点”,把“关系”画成“线”。
- 一旦画出了图,你就会发现:这不就是求两个连通块的大小吗?
- 一旦建模完成,直接套模板,别管他是男是女还是外星人。
4. 积累“功能级”模板:Size 数组
经验:
这次你学会了:并查集不只是用来查亲戚的,更是用来维护“集合属性”的。
下次怎么做?
把并查集看作一个“挂件系统”。
- 基础款:只维护连通性(
fa数组)。 - 升级款:需要知道集合人数?挂一个
sz数组(初始化1,合并相加)。 - 豪华款:需要知道集合权值总和?挂一个
sum数组。 - 旗舰款:需要知道元素到根的距离?挂一个
dist数组(带权并查集)。
总结:
下次看到题目,先问自己三个问题:
- 这是什么板子? (并查集+Size)
- 数据怎么洗干净? (负数转正,用两套数组)
- 核心逻辑能复用吗? (写通用的
find/union函数)
想清楚这三点,你写出的代码就会像今天这样,既短小精悍,又逻辑严密。加油!

浙公网安备 33010602011771号