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 算法核心逻辑(配邻接表)

  1. 准备工作

    • dist[i]:表示节点 i 距离“已生成的树” 最近的距离(注意:不是离起点的距离,而是离树集合的距离)。初始化为无穷大。
    • vis[i]:标记节点 i 是否已经加入生成树。
    • 优先队列:用来快速找到当前离树最近的那个点。
  2. 开始种树

    • 随便选一个点(比如 1 号点),dist[1] = 0,扔进队列。
  3. 循环扩张

    • 从队列里弹出一个离树最近的点 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 函数:union1union2
这不仅代码长,而且改一个 bug 要改两处(比如你刚才那个 Union 里写死 fa1 的错误,如果拆成两个函数反而不容易发现)。

你的高光时刻
你写了 find(x, fa[])Union(x, y, fa[], sz[])
这一招叫“传参泛化”

下次怎么做?

  • 警觉信号:当你发现自己在复制粘贴一段代码,并且只是把里面的 a 数组改成 b 数组时,立刻停下来!
  • 解决方案:把变化的数组(fa, sz)提取出来,作为函数的参数传进去。
  • 好处:逻辑只有一份,改一处全好,且代码极度简洁。

2. 数据“标准化”:把异常输入变成标准输入

痛点
题目给你负数、给你字符串、给你坐标……如果你直接在逻辑里处理(比如在 Union 里判断正负),代码会变得很脏。

你的高光时刻
你用 abs() 或者 -x 把女人的负数编号,强行变成了正数下标。
这一招叫“数据归一化”

下次怎么做?

  • 思维习惯:不管题目给的数据多奇葩(负数、很大的离散数、字符串名字),先在输入阶段把它转化成 1N 的整数
  • 例子
    • 给负数?取绝对值。
    • 给字符串名字("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 数组(带权并查集)。

总结:
下次看到题目,先问自己三个问题:

  1. 这是什么板子? (并查集+Size)
  2. 数据怎么洗干净? (负数转正,用两套数组)
  3. 核心逻辑能复用吗? (写通用的 find/union 函数)

想清楚这三点,你写出的代码就会像今天这样,既短小精悍,又逻辑严密。加油!

posted @ 2026-01-25 11:15  EcSilvia  阅读(3)  评论(0)    收藏  举报