mst[讲课留档]

最小生成树(Minimum Spanning Tree)

(1)概念

我们知道, 是有 n n n个结点, n − 1 n-1 n−1条边的无向无环的连通图。

一个连通图的生成树是一个极小的连通子图,它包含图中全部的 n n n个顶点,但只有构成一棵树的 n − 1 n-1 n−1条边。

最小生成树就是一个带权图,每个边都有一个边权,一颗生成树的权值是该树边所有边权的和, M S T MST MST就是所有生成树中最小的一个。

(2)Prim算法(遍历点的算法)

普里姆算法在找最小生成树时,将顶点分为两类,一类是在查找的过程中已经包含在生成树中的顶点(假设为 A 类),剩下的为不在生成树中的(假设为 B 类)。

对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A 类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。所走过的顶点和边就是该连通图的最小生成树。

cpp 复制代码
int dis[N];
int mp[N][N];
bool vis[N];

void work() {
	int n, m;cin >> n >> m;
	int ans = 0;
    memset(vis, 0, sizeof vis);
    memset(mp, 0x3f3f3f3f, sizeof mp);
    for (int i = 0; i < n; ++i) {
    	int u, v, w;cin >> u >> v >> w;
        mp[u][v] = w;
        mp[v][u] = w;
    }
    for (int i = 0; i < n; ++i) {
        dis[i] = 0x3f3f3f3f;
    }
    dis[0] = 0;
    vis[0] = 1;
    for (int i = 1; i < n; ++i) {
        dis[i] = min(dis[i], mp[0][i]);
    }
    for (int i = 1; i < n; ++i) {
        //这里的外层循环是循环遍数,与i值无关
        double minn = 0x3f3f3f3f;
        int pos = -1;
        for (int j = 1; j < n; ++j) {
            //每次循环找出与已排联通块距离最近的点
            if (!vis[j] && minn > dis[j]) {
                pos = j;
                minn = dis[j];
            }
        }
        ans += minn;
        vis[pos] = 1;
        for (int j = 0; j < n; ++j) {
            //刷新未连接点的距离最小值
            dis[j] = min(dis[j], mp[pos][j]);
        }
    }
    cout << ans << '\n';
}

正确性显然。

复杂度是 O ( n 2 ) O(n^2) O(n2)级别的,但是我们可以使用堆优化降到 O ( n l o g n ) O(nlogn) O(nlogn),之后讲最短路的时候会讲堆优化。

(3)Kruskal算法(遍历边的算法)

克鲁斯卡尔算法(Kruskal)是一种使用贪婪方法的最小生成树算法。 该算法初始将图视为森林,图中的每一个顶点视为一棵单独的树。 一棵树只与它的邻接顶点中权值最小且不违反最小生成树属性(不构成环)的树之间建立连边。

利用并查集可以快速实现查找两个点是否已经连接

cpp 复制代码
int n, m;
int f[105];
struct road {
	int a, b, v;
} arr[305];

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

void join(int a, int b) {
	if (find(a) != find(b))
		f[find(a)] = find(b);
}

bool cmp(road a, road b) {
	return a.v < b.v;
}

void work() {
	cin >> n >> m;
	int a, b, c;
	for (int i = 1; i <= n; ++i) {
		f[i] = i;
	}
	int ans = 0;
	for (int i = 0; i < m; ++i) {
		cin >> arr[i].a >> arr[i].b >> arr[i].v;
	}
	sort(arr, arr + m, cmp);
	//先按路的权值排序,如果两点的祖先不是一个就加上然后合并。
	for (int i = 0; i < m; ++i) {
		if (find(arr[i].a) != find(arr[i].b)) {
			ans += arr[i].v;
			join(arr[i].a, arr[i].b);
		}
	}
	cout << ans << '\n';
	return 0;
}

正确性证明:

给一带权连通的树一定会有至少一棵生成树,那么这些生成树中间必然会会存在至少一棵最小生成树。

假设 T T T是用 k r u s k a l kruskal kruskal求出来的最小生成树,而 U U U是这个图的最小生成树,要证 U = T U = T U=T。

然而如果 T ≠ U T \neq U T=U,那么至少存在一条边在 T T T中,不在 U U U中,假设存在 k k k条边存在 T T T中不存在 U U U中。

接下来进行 k k k次变换:

每次将在 T T T中不在 U U U中的最小的边 f f f拿出来放到 U U U中,那么 U U U中必然形成一条唯一的环路,我们取出这个环路上最小的且不在 T T T中的边 e e e放回到 T T T中,这样的边 e e e一定是存在的,因为之前的 T T T不存在环路(如果 e e e在 T T T中那么就可能和 f f f形成环路)。

现在证明 f f f和 e e e权值的关系:

假设 f < e f < e f<e,那么后来形成的 U U U是权值之和更小了,与 U U U是最小生成树矛盾。 实际上,不可能在 U U U中拿到大于 f f f的边,因为把 f f f拿走后,如果小于 f f f的边都不成立,至少 f f f是一个符合条件的边会被那回。

假设 f > e f > e f>e,那么根据 k r u s k a l kruskal kruskal的做法, e e e是在 f f f之前被取出来的边但是被舍弃了,一定是因为 e e e和比 e e e小的边形成了环路,而比 e e e小的边都是存在 U U U中的,而 e e e和这些边并没有形成环路,于假设矛盾。

于是 f f f一定和 e e e相等的, k k k次变换后, T T T和 U U U的权值之和是相等的。

最小生成树的值也是相等的。

复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)级别的,一般有mst就用这个。

cpp 复制代码
int f[N];
struct road {
    int a, b, v;
} arr[N];
int tot = 0;

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

void join(int a, int b) {
    if (find(a) != find(b)) f[find(a)] = find(b);
}

bool cmp(road a, road b) {
    return a.v < b.v;
}

void work() {
    int a, b;cin >> a >> b;
    for (int i = 1; i <= b; ++i) {
        f[i] = i;
        arr[i] = {0, i, a};
    }
    tot = b;
    for (int i = 1; i <= b; ++i) {
        for (int j = 1; j <= b; ++j) {
            int x; cin >> x;
            if (x == 0) continue;
            else arr[++tot] = {i, j, x};
        }
    }
    ll ans = 0;
    sort(arr + 1, arr + 1 + tot, cmp);
    for (int i = 1; i <= tot; ++i) {
        if (find(arr[i].a) != find(arr[i].b)) {
            ans += arr[i].v;
            join(arr[i].a, arr[i].b);
            b--;
        }
        if (b == 0) break;
    }
    cout << ans << '\n';
}
相关推荐
山脚ice10 分钟前
【CT】LeetCode手撕—704. 二分查找
算法·leetcode
贱贱的剑42 分钟前
【算法】选择排序
算法·rust·排序算法
瑜陀42 分钟前
2024.06.30 刷题日记
数据结构·算法·leetcode
Star Patrick44 分钟前
*算法训练(leetcode)第二十天 | 39. 组合总和、40. 组合总和 II、131. 分割回文串
c++·算法·leetcode
光久li44 分钟前
【算法刷题 | 动态规划14】6.28(最大子数组和、判断子序列、不同的子序列)
算法·动态规划
飘然渡沧海1 小时前
gbk,ucs-2转中文
java·开发语言·算法
raykingl1 小时前
154. 寻找旋转排序数组中的最小值 II(困难)
java·python·算法·二分查找
raykingl1 小时前
69. x 的平方根(简单)
java·python·算法·二分查找
阳光男孩011 小时前
力扣974.和可被K整除的子数组
数据结构·算法·leetcode
may-daydayup1 小时前
【代码随想录】【算法训练营】【第53天】 [739]每日温度 [496]下一个更大元素I [503]下一个更大元素II
算法