20250814 - 最小生成树 总结

20250814 - 最小生成树

前言

最小生成树可太难了!!!!!!!!!!!!!!

概念

在无向图里选取 \(n-1\) 条边,使这 \(n-1\) 条边和 \(n\) 个点恰好能够组成一颗树,这样的树就被称为原图的生成树。

所以,生成树里边权最小的就是 最小生成树

方法

1. Dijkstra prim 算法

类似 Dijkstra,都是以点核心的算法,该算法的基本思想是从一个结点开始,不断加点,直到构成一棵树。

具体来说,每次要选择距离最小的一个结点,以及用新的边更新其他结点的距离。

其实跟 Dijkstra 算法一样,每次找到距离最小的一个点,可以暴力找也可以用 \(priority\) _ \(queue\) 维护。

代码:

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
const int MAXN = 5000 + 7;
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;

void init(){

  return;
}
int n,m;
vector<pair<int,int>>e[MAXN];
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q; // 太长了!!!!
vector<int>dis,vis;
int prim(){
  dis = vector<int>(n + 1,INF);
  vis.resize(n + 10);
  dis[1] = 0;
  q.push({0,1});
  int ans = 0,tot = n;
  while(!q.empty()){
    int u = q.top().second,d = q.top().first;
    q.pop();
    if(vis[u]) continue;
    vis[u] = 1;
    ans += d;
    tot--;
    for(auto y : e[u]){
      int v = y.first,w = y.second;
      if(w < dis[v]){
        dis[v] = w;
        q.push({dis[v],v});
      }
    }
  }
  if(tot == 0)
    return ans;
  return -1;
}
int main(){
  scanf("%d%d",&n,&m);
  for(int i = 1;i <= m;i++){
    int x,y,w;
    scanf("%d%d%d",&x,&y,&w);
    e[x].push_back({y,w});
    e[y].push_back({x,w});
  }
  int ans = prim();
  if(ans == -1)
    puts("orz");
  else  
    printf("%d",ans);
  return 0;
}

时间复杂度:\(O((n+m)log_2m)\)

注意:千万不要和 Dijkstra 搞混了!!!

kruskal 算法

类似 bellman-ford,都是以边核心的算法,该算法的基本思想是从一条边开始,不断加边,直到构成一棵树。

但如何判断两个点是否在一个集合呢?传递闭包,bfs 并查集!!!

代码:

#include <bits/stdc++.h>

using namespace std;
#define ll long long
const int MAXH = 2e5 + 7;
const int mod = 1e9 + 7;
int n,m;
struct node{
	int x,y,w;
	bool operator < (const node &x) const{
		return w < x.w;
	}
}e[MAXH];
int fa[MAXH];
int find(int x){
	if(fa[x] == x)
		return x;
	return fa[x] = find(fa[x]);
}
int kruskal(){
	for(int i = 1;i <= n;i++)
		fa[i] = i;
	sort(e+1,e+m+1);
	int tot = n,ans = 0;
	for(int i = 1;i <= m;i++){
		int x = find(e[i].x),y = find(e[i].y);
		if(x != y){
			ans += e[i].w;
			tot--;
			fa[x] = y;
		}
	}
	if(tot != 1)
		return -1;
	else
		return ans;
}
signed main()
{
	cin >> n >> m;
	for(int i = 1;i <= m;i++){
		cin >> e[i].x >> e[i].y >> e[i].w;
	}
	int ans = kruskal();
	if(ans == -1){
		cout << "orz" << endl;
	}else{
		cout << ans << endl;
	}
	return 0;
}

时间复杂度:\(O(mlog_2m)\)

注意:千万不要和 Bellman-Ford 搞混了!!!```


Kruskal 重构树

在跑 Kruskal 的过程中我们会从小到大加入若干条边。现在我们仍然按照这个顺序。

首先新建
n 个集合,每个集合恰有一个节点,点权为 0。

每一次加边会合并两个集合,我们可以新建一个点,点权为加入边的边权,同时将两个集合的根节点分别设为新建点的左儿子和右儿子。然后我们将两个集合和新建点合并成一个集合。将新建点设为根。

不难发现,在进行
n-1 轮之后我们得到了一棵恰有
n 个叶子的二叉树,同时每个非叶子节点恰好有两个儿子。这棵树就叫 Kruskal 重构树。


例题:P2245 星际导航

Kruskal 重构树板子题,按照定义模拟即可

代码(调了两个半小时):

#include <bits/stdc++.h>

using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
const int MAXN = 4e5 + 7; // 一定要开 2 倍空间
const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 7;
int n, m, other_id;
//-----
// 下次还是老老实实写倍增吧
vector<array<int, 3>> edges;
vector<int> e[MAXN];
int dep[MAXN];
int key[MAXN];
int dfn[MAXN], id[MAXN], times, dp[MAXN][20], lg[MAXN], f[MAXN], fa[MAXN];
bool vis[MAXN];
//-----
bool cmp(const array<int, 3>& x, const array<int, 3>& y) {
  return x[0] < y[0];
}

int find(int x) {
  if (f[x] == x)
    return x;
  return f[x] = find(f[x]);
}

void dfs(int x) {
  dfn[x] = ++times;
  id[times] = x;
  vis[x] = 1;
  for (auto y : e[x]) {
    if (!vis[y]) {
      fa[y] = x;
      dep[y] = dep[x] + 1;
      dfs(y);
    }
  }
}

int MIN(int x, int y) {
  return dep[x] < dep[y] ? x : y;
}

void init() {
  for (int i = 1; i <= other_id; i++)
    dp[i][0] = id[i];
  for (int j = 1; (1 << j) <= other_id; j++) {
    for (int i = 1; i + (1 << j) - 1 <= other_id; i++) {
      dp[i][j] = MIN(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
  }
  for (int i = 2; i < MAXN; i++)
    lg[i] = lg[i / 2] + 1;
}

int get_lca(int x, int y) {
  if (x == y)
    return x;
  int l = dfn[x], r = dfn[y];
  if (l > r) swap(l, r);
  l++;
  int j = lg[r - l + 1];
  return fa[MIN(dp[l][j], dp[r - (1 << j) + 1][j])];
}
int q;
int main() {
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  edges.resize(m + 10);
  other_id = n;
  
  for (int i = 1; i <= m; i++) {
    int x, y, w;
    cin >> x >> y >> w;
    edges[i] = {w, x, y};
  }
  // 一定要开 2 倍空间
  for (int i = 1; i <= (n << 1); i++) 
    f[i] = i;
  
  sort(edges.begin() + 1, edges.begin() + m + 1, cmp); // 排序
  
  for (int i = 1; i <= m; i++) {
    int x = find(edges[i][1]), y = find(edges[i][2]);
    int w = edges[i][0];
    if (x != y) {
      f[y] = f[x] = ++other_id;
      e[x].push_back(other_id);
      e[other_id].push_back(x);
      e[y].push_back(other_id);
      e[other_id].push_back(y);
      key[other_id] = w;
      // 奇奇妙妙的建图
    }
  }
  
  fa[other_id] = other_id;
  dep[other_id] = 1;
  // 一个坑点:请注意 dfs 时要考虑图不连通的情况,具体来讲就是要对每个连通分量进行一次 dfs(被卡了17次)
  for(int i = other_id;i;i--){
    if(!vis[i]){
      dfs(i);
    }
  }
  
  init();
  
  cin >> q;
  while (q--) {
    int x, y;
    cin >> x >> y;
    if (find(x) != find(y))
      cout << "impossible" << endl;
    else
      cout << key[get_lca(x, y)] << endl;
  }
  
  return 0;
}

总结:下次再也不写 \(O(1)\) \(lca\) 了。

posted @ 2025-08-16 18:44  Ruochen_xia  阅读(16)  评论(0)    收藏  举报