算法专题——最小生成树
最小生成树的概念
prim算法以及kruskal算法,原生问题中是求解将n个节点连接并花费最小而形成的树的算法。还可以求解最大的边权最小的问题。
相关题目后续补充
求最小生成树的算法
Prim算法
总是链接离中心最近的一个节点到树中。
O(n^2)
原理解释:
-
反证法:将当前与外界直接相连的权值最小的一条边不并入树中,而是通过其他点将两个连通块连接构成了一棵树。在树构成完成之后,将该边并入树中,根据树的性质可以得到一个环,由于一开始两个连通块并不连通,一定是通过一条比该边权值更大的边进行连通的,所以该环一定会出现比该边权值更大的边,于是可以将环中权值最大的边去掉,仍然可以得到一棵树并且整棵树权值不会减小,于是得证该边一定可以并入该最小生成树中。
-
连通性:将树与外界分开,将外界所有节点视为已经连通的连通块,如果最后要构成一棵树的话,显然需要一条边将“树”和“外界”进行连通。那么最优解显然就是权值最小的那一条边。
伪代码:
for(i : n) {
找到距离树最近的,且还未访问的节点。
将该点插入树中,vis = true,答案 += 该边权值
更新距离
}
代码:
int gra[MAXN][MAXN];
int dist[MAXN];
bool vis[MAXN];
bool Prim() {
memset(dist, 0x3f, sizeof dist);
memset(vis, false, sizeof vis);
dist[1] = 0;
for (int i = 1; i <= n; i++) {
int minidx = -1;
for (int j = 1; j <= n; j++)
if (!vis[j] && (minidx == -1 || dist[minidx] > dist[j]))
minidx = j;
if (dist[minidx] == INF) return false;
vis[minidx] = true;
ans += dist[minidx];
for (int j = 1; j <= n; j++)
dist[j] = min(dist[j], gra[minidx][j]);
}
return true;
}
kruskal算法
类似于Bellman-Ford算法,去中心化,最终得到一棵树。直接存储边的结构体实现。通过并查集实现。
O(ElogE)
,如果不用排序可以优化到O(E)
原理解释:
- 反证法:和prim算法的证明差不多,大致环节就是,如果不加该边→成树→加上该边得到环→替换掉一条权值更大的边→更优解→该边可以出现在最后的最小生成树中。
- 最优解:总是将权值最小的边并入树中,如果成环了,那么由于已经并入的都是比该边权值更小的边,所以环上一定不存在可替换的边,将该边舍去,如果不会成环,那就将其视为可以作为最小生成树的一条边。
特点:
计算中的任意过程拿出来都是正确的,可以中途结束,同样可以中途开始。是一个逐渐连接各个连通块,逐渐减少连通块个数的方法,因此可以可以求解类似最小生成森林的问题。
关键词:连通块,最小边。
伪代码:
对边从小到大进行排序
并查集数组初始化
for (i : 边的数量) {
得到当前下标对应的边
if(边的起点与边的终点不在一个集合里) {
答案 += 该边权值;
合并起点终点
连通块数量++
}
}
代码:
const int MAXN = 110, MAXM = 1000;
int n, m;
struct Edge{
int u, v, val;
bool operator< (const Edge& a) const {return val < a.val;}
}edge[MAXM];
int fa[MAXN];
void Find(int a) {return fa[a] == a ? a : fa[a] = Find(fa[a]);}
int Kurskal() {
sort(edge, edge + m);
for (int i = 1; i <= n; i++) fa[i] = i;
int cnt = 0, ans = 0;
for (int i = 0; i < m; i++) {
int fu = Find(edge[i].u), fv = Find(edge[i].v);
if (fu != fv) {
fa[fu] = fv;
ans += edge[i].val;
if (++cnt == n - 1) break;
}
}
//if (cnt < n - 1) ans = -1; //不能得到树
return ans;
}
拓展应用
- 虚拟原点:可以完成点权转换为边权,将一些特殊的东西一般化。
- 最小边:Kurskal算法的灵活应用需要关注算法过程中的连通块和最小边,这是一个很重要的性质。
- 次小生成树:最小生成树的一类问题,最小生成树的邻集。
次小生成树
定义:第二小的生成树,不严格的话可以等于最小生成树,严格的话一定小于最小生成树。
求解方法:
- 先求最小生成树,枚举删去最小生成树中的边求解。
O(mlogm + m)
- 先求最小生成树,枚举非树边,将该边加入树中,替换掉环上刚好比该边小(根据性质,一定是环上权值最大)的一条边。
O(mlogm + n^2 + m)(得到最小生成树 + 得到maxlen[i][j] + 枚举每一条非树边)
,如果使用LCA倍增优化得到maxlen[i][j]
,可以达到O(mlogm)
最小生成树问题分析
最小生成树模板
没啥好说的。超绝可爱乐正绫!我最喜欢乐正绫!
Watering Hole
使用虚拟原点连接一条边到各个农场,表示建造水井的花费,没什么好说的。放个涩图。
Artic Network
两个思维点,一个是无线网络和有线网络的使用(贪心),一个是求的是最大值而不是总和(Kruskal拓展)。
对于无线网络和有线网络,根据贪心的思想可以得到基本上给多少无线网络的基站就用多少基站,知道可以全部免费为止,例如给出了m个无线基站,就可以让最小生成树的权值之和减去m - 1
条边。
对于求取的东西不是边权之和的问题,实际上根据Kruskal算法的证明过程,可以得到Kruskal总是将边权尽量小的边并入最小生成树中,不难发现以最小生成树的方法其最终也能找到树中最大边权值的最小值为多少。
即用Kruskal求一颗边数为n - m
的最小生成树,并得到该树中最后一条并入的边(最大边)的权值为多少。
局域网
略。来点涩图(只有我一个人觉得这张图很涩吗...)
严格次小生成树
字面义,求一个图的严格次小生成树,其中次小生成树的定义为:边权之和严格小于最小生成树,也仅小于最小生成树的生成树。
次小生成树并没有什么专门的算法去求解,可以考虑模拟的思想求解,可以发现,次小生成树是在最小生成树的基础上删掉一条边又增加一条边得到的,这一点不难证明,我们来简单证明一下。
先用一条更大的边替换原来最小生成树的一条边,得到一颗新树;我们再找一条大的边替换新树上的一条边,首先将该边加入该树中,可以得到一个环,下面进行分类,如果该环包含第一条加入的边,因为第二条边的权值一定大于环上除第一条加入的边的权值的其他所有边,那么显然需要将第一条加入的边替换掉,或者直接不加入第二条边才可以更加接近次小生成树的求解;而如果该环不包括第一条加入的边,该边仍然不小于该环上的任何一条边的权值,不难得到仍然是要么留第一条边,要么留第二条边,才能更加接近次小生成树的求解。
综上次小生成树仅与最小生成树差一条边。同时通过以上证明我们也发现了次小生成树的得到方法,就是枚举所有没有并入最小生成树的边,并入最小树中,替换掉环上边权严格小于该边的最大边,得到一颗新树,取所有生成树的最小值即可。
那如何求得环上边权严格小于新并入边的最大边呢?不难发现可以用LCA,最近公共祖先的思想,将树化为链,可以很快得到我们要的边。
综上该题的思路大致为,Kruskal处理得到最小生成树,DFS预处理存储最小边和次小边的边权,枚举所有边得到次小生成树。
见下面完整代码:
//https://www.luogu.com.cn/problem/P4180
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<map>
#include<queue>
#include<stack>
#include<deque>
#include<cctype>
#include<ctime>
#include<set>
using namespace std;
//STL库常用(vector, map, set, pair)
#define pii pair<int,int>
#define pdd pair<double,double>
#define F first
#define S second
//常量
#define PI (acos(-1.0))
#define INF 0x3f3f3f3f
//必加
#define debug cout
#define Debug(x) cout << #x << " = " << x << endl;
typedef unsigned long long ull;
typedef long long ll;
typedef double db;
// 快读快输
template<typename T> inline T read() {T x=0; bool f=0; char c=getchar();while(!isdigit(c)){if(c =='-')f= 1; c=getchar();}while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=getchar();}return f?~x+1:x;}
template<typename T> inline void print(T k){ int num = 0,ch[20]; if(k == 0){ putchar('0'); return ; } (k<0)&&(putchar('-'),k = -k); while(k>0) ch[++num] = k%10, k /= 10; while(num) putchar(ch[num--]+48); }
//图论
const int GRAPHN = 1e5 + 10, GRAPHM = 6e5 + 10;
int h[GRAPHN], e[GRAPHM], ne[GRAPHM], wei[GRAPHM], idx;
void AddEdge(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;}
void AddEdge(int a, int b, int c) {e[idx] = b, ne[idx] = h[a], wei[idx] = c, h[a] = idx++;}
void AddEdge(int h[], int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;}
void AddEdge(int h[], int a, int b, int c) {e[idx] = b, ne[idx] = h[a], wei[idx] = c, h[a] = idx++;}
// 数学公式
template<typename T> inline T gcd(T a, T b){ return b==0 ? a : gcd(b,a%b); }
template<typename T> inline T lowbit(T x){ return x&(-x); }
template<typename T> inline bool mishu(T x){ return x>0?(x&(x-1))==0:false; }
template<typename T1,typename T2, typename T3> inline ll q_mul(T1 a,T2 b,T3 p){ ll w = 0; while(b){ if(b&1) w = (w+a)%p; b>>=1; a = (a+a)%p; } return w; }
template<typename T,typename T2> inline ll f_mul(T a,T b,T2 p){ return (a*b - (ll)((long double)a/p*b)*p+p)%p; }
template<typename T1,typename T2, typename T3> inline ll q_pow(T1 a,T2 b,T3 p){ ll w = 1; while(b){ if(b&1) w = (w*a)%p; b>>=1; a = (a*a)%p;} return w; }
template<typename T1,typename T2, typename T3> inline ll s_pow(T1 a,T2 b,T3 p){ ll w = 1; while(b){ if(b&1) w = q_mul(w,a,p); b>>=1; a = q_mul(a,a,p);} return w; }
template<typename T> inline ll ex_gcd(T a, T b, T& x, T& y){ if(b == 0){ x = 1, y = 0; return (ll)a; } ll r = exgcd(b,a%b,y,x); y -= a/b*x; return r;/*gcd*/ }
template<typename T1,typename T2> inline ll com(T1 m, T2 n) { int k = 1;ll ans = 1; while(k <= n){ ans=((m-k+1)*ans)/k;k++;} return ans; }
template<typename T> inline bool isprime(T n){ if(n <= 3) return n>1; if(n%6 != 1 && n%6 != 5) return 0; T n_s = floor(sqrt((db)(n))); for(int i = 5; i <= n_s; i += 6){ if(n%i == 0 || n%(i+2) == 0) return 0; } return 1; }
/* ----------------------------------------------------------------------------------------------------------------------------------------------------------------- */
const int MAXN = 1e5 + 10, MAXM = 3e5 + 10;
int n, m;
struct Node {
int u, v;
int val;
bool isTree;
Node(int _u = 0, int _v = 0, int _val = 0, int _isTree = false) : u (_u), v(_v), val(_val), isTree(_isTree) {}
bool operator< (const Node& a) const {return val < a.val;}
}edge[MAXM];
int tol;
int fa[MAXN];
ll ans;
int depth[MAXN], fat[MAXN][30], d1[MAXN][30], d2[MAXN][30];
void addEdge(int u, int v, int val) {
edge[tol].u = u;
edge[tol].v = v;
edge[tol++].val = val;
}
int Find(int u) {return u == fa[u] ? u : fa[u] = Find(fa[u]);}
void Kurskal() {
for (int i = 1; i <= n; i++) fa[i] = i;
int cnt = 0;
for (int i = 0; i < tol; i++) {
int u = edge[i].u, v = edge[i].v, val = edge[i].val;
int fu = Find(u), fv = Find(v);
if (fu != fv) {
edge[i].isTree = true;
fa[fu] = fv;
ans += val; //将贡献计入
if (++cnt == n - 1) return ;
}
}
}
void DFS(int u, int fath) {
depth[u] = depth[fath] + 1; //得到深度
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!depth[v]) { //当前可以访问
fat[v][0] = u; //fa数组更新
for (int j = 1; j <= 20; j++) {
fat[v][j] = fat[fat[v][j - 1]][j - 1];
}
d1[v][0] = wei[i];
d2[v][0] = -INF;
for (int j = 1; j <= 20; j++) {
d1[v][j] = d2[v][j] = -INF;
int tmp[4] = {d1[v][j - 1], d2[v][j - 1], d1[fat[v][j - 1]][j - 1], d2[fat[v][j - 1]][j - 1]};
for (int k = 0; k < 4; k++) {
if (tmp[k] > d1[v][j]) d2[v][j] = d1[v][j], d1[v][j] = tmp[k];
else if (tmp[k] != d1[v][j] && tmp[k] > d2[v][j]) d2[v][j] = tmp[k];
}
}
DFS(v, u);
}
}
}
pii Solve(int u, int v) {
pii MAX;
MAX.F = MAX.S = -INF;
if (depth[u] < depth[v]) swap(u, v);
for (int i = 20; i >= 0; i--) {
if (depth[fat[u][i]] >= depth[v]) {
int tmp[2] = {d1[u][i], d2[u][i]};
for (int i = 0; i < 2; i++) {
if (tmp[i] > MAX.F) MAX.S = MAX.F, MAX.F = tmp[i];
else if (tmp[i] != MAX.F && tmp[i] > MAX.S) MAX.S = tmp[i];
}
u = fat[u][i];
}
}
if (u == v) return MAX;
for (int i = 20; i >= 0; i--) {
if (fat[u][i] != fat[v][i]) {
int tmp[4] = {d1[u][i], d2[u][i], d1[v][i], d2[v][i]};
for (int i = 0; i < 4; i++) {
if (tmp[i] > MAX.F) MAX.S = MAX.F, MAX.F = tmp[i];
else if (tmp[i] != MAX.F && tmp[i] > MAX.S) MAX.S = tmp[i];
}
u = fat[u][i]; v = fat[v][i];
}
}
int tmp[2] = {d1[u][0], d1[v][0]};
for (int i = 0; i < 2; i++) {
if (tmp[i] > MAX.F) MAX.S = MAX.F, MAX.F = tmp[i];
else if (tmp[i] != MAX.F && tmp[i] > MAX.S) MAX.S = tmp[i];
}
return MAX;
}
int main() {
clock_t c1 = clock();
#ifdef LOCAL
freopen("E:\\Cpp\\1.in", "r", stdin);
freopen("E:\\Cpp\\1.out", "w", stdout);
#endif
//--------------------------------------------
scanf("%d%d", &n, &m);
int u, v, val;
for (int i = 1; i <= m; i++) {
scanf("%d%d%d", &u, &v, &val);
addEdge(u, v, val);
}
sort(edge, edge + m);
Kurskal();
memset(h, -1, sizeof h);
for (int i = 0; i < tol; i++) {
u = edge[i].u, v = edge[i].v, val = edge[i].val;
if (edge[i].isTree) {
AddEdge(u, v, val);
AddEdge(v, u, val);
}
}
DFS(1, 0);
ll res = (ll)0x3f3f3f3f3f3f3f3f;
for (int i = 0; i < tol; i++) {
if (!edge[i].isTree) {
int u = edge[i].u, v = edge[i].v;
pii tmp = Solve(u, v);
if (tmp.F != edge[i].val) { //如果不相等, 可以直接操作
res = min(res, ans + edge[i].val - tmp.F);
}
else if(tmp.S != -INF) { //相等的话, 看有没有严格次小值
res = min(res, ans + edge[i].val - tmp.S);
}
}
}
printf("%lld\n", res);
//--------------------------------------------
#ifdef LOCAL
cerr << "Time Used:" << clock() - c1 << "ms" << endl;
#endif
return 0;
}