Appearance
Kruskal
将边集升序后,使用并查集贪心合并顶点不在同一个连通块中的边即可。
排序必须按边权升序。并查集需要检查全部
点击展开代码
cpp
sort(edge.begin(), edge.end());
long long ans = 0;
int cnt = 0;
for (auto [u, v, w] : edge) {
if (dsu.same(u, v))
continue;
dsu.merge(u, v);
ans += w;
cnt++;
}
if (cnt != n - 1) {
// 图不连通,不存在生成树
return;
}上方代码默认 edge 的比较规则以边权为第一关键字。最终必须恰好选出
Prim
朴素维护
Prim 本质上求的是一棵根向叶子的有向树,用
每次扩展一个
下面模板默认 dis 初始化为 inf、vis 初始化为 inf,说明原图不连通。
点击展开代码
cpp
long long ans = 0;
dis[1] = 0;
for (int _ = 1; _ <= n; _++) {
int mini = 0;
for (int i = 1; i <= n; i++) {
if (!vis[i] && (!mini || dis[i] < dis[mini]))
mini = i;
}
if (!mini || dis[mini] == inf) {
// 图不连通,不存在生成树
return;
}
vis[mini] = 1;
ans += dis[mini];
for (auto [v, w] : p[mini]) {
if (!vis[v])
dis[v] = min(dis[v], w);
}
}堆优化
时间复杂度同 Dijkstra,使用二叉堆维护
下面的 q 为按候选边权从小到大取出的优先队列。根节点同样计入
点击展开代码
cpp
long long ans = 0;
dis[1] = 0;
q.push({1, 0});
for (int _ = 1; _ <= n; _++) {
while (q.size() && vis[q.top().v])
q.pop();
if (q.empty()) {
// 图不连通,不存在生成树
return;
}
auto [now, __] = q.top();
q.pop();
vis[now] = 1;
ans += dis[now];
for (auto [v, w] : p[now]) {
if (!vis[v]) {
if (dis[v] > w) {
q.push({v, w});
dis[v] = w;
}
}
}
}其他数据结构优化
容易发现,堆优化 Prim 只是用二叉堆优化了取最小值的过程。
那么同样地,完全可以使用任意其他可以维护最值的数据结构来优化 Prim,如线段树、set 等。
同时,根据其他数据结构的功能,还可以扩展出其他存图方式,本质上只要能维护 dis 数组即可。
Boruvka
每轮先根据轮开始时的并查集,为每个当前连通块记录一条最轻出边,再统一尝试合并。若原图连通,每个尚未完成的连通块都有出边,合并后连通块数量至少减半,因此最多进行
点击展开代码
cpp
int cnt = n - 1;
long long ans = 0;
dsu.init(n);
while (cnt) {
vector<int> minn(n + 1, inf);
vector<pii> mini(n + 1, {0, 0});
for (auto [u, v, w] : edge) {
int x = dsu.find(u), y = dsu.find(v);
if (x == y)
continue;
if (minn[x] > w) {
minn[x] = w;
mini[x] = {u, v};
}
if (minn[y] > w) {
minn[y] = w;
mini[y] = {u, v};
}
}
bool flag = 0;
for (int i = 1; i <= n; i++) {
auto [u, v] = mini[i];
if (!u || dsu.same(u, v))
continue;
dsu.merge(u, v);
ans += minn[i];
cnt--;
flag = 1;
}
if (!flag) {
// 图不连通,不存在生成树
return;
}
}Boruvka 本质上只要找到当前每个连通块向外的最小边即可,如果条件允许,也不见得一定要遍历所有边。
次小生成树
固定一棵最小生成树后,至少存在一棵最优的候选次小生成树,可以通过加入一条非树边、再从形成的环上删除一条树边得到。
设最小生成树权值为
若只要求得到一棵不同的生成树,并允许其权值仍等于
- 当路径最大边权
时,删除 ; - 当
时,删除路径上第二大的不同边权 ;若不存在 ,这条非树边不能产生严格候选。
因此倍增查询通常维护路径上前两大的不同边权,而不是只维护一个最大值。若允许负边权,空值必须初始化为 -inf,不能用
瓶颈路
可以证明,最小生成树上
反之,最大瓶颈路指
Kruskal 重构树
在 Kruskal 运行过程中,对于树边
原图顶点
最小树形图
本文讨论以
朱刘算法
点击展开代码
cpp
bool zl(int n, int r, long long &res) {
res = 0;
while (1) {
vector<node> pre(n + 1);
for (int i = 1; i <= n; i++)
pre[i].w = inf;
for (auto u : edge) {
if (u.v == r)
continue;
if (u.w <= pre[u.v].w) {
pre[u.v] = {u.u, u.w};
}
}
for (int i = 1; i <= n; i++) {
if (i == r)
continue;
if (pre[i].w == inf)
return 0;
res += pre[i].w;
}
stack<int> q;
vector<bool> vis(n + 1);
vector<int> num(n + 1), col(n + 1);
int id = 0;
for (int i = 1; i <= n; i++) {
int now = i;
if (col[now] || i == r)
continue;
while (now && !vis[now] && !col[now]) {
q.push(now);
vis[now] = 1;
now = pre[now].v;
}
if (vis[now]) {
id++;
while (q.top() != now) {
int cur = q.top();
q.pop();
num[cur] = id;
vis[cur] = 0;
col[cur] = 2;
}
q.pop();
num[now] = id;
vis[now] = 0;
col[now] = 2;
}
while (q.size()) {
int cur = q.top();
q.pop();
num[cur] = ++id;
vis[cur] = 0;
col[cur] = 1;
}
}
bool flag = 0;
for (int i = 1; i <= n; i++)
if (col[i] == 2) {
flag = 1;
break;
}
if (!flag)
break;
vector<Edge> tmp;
for (auto u : edge) {
if (num[u.u] == num[u.v])
continue;
tmp.push_back({num[u.u], num[u.v], u.w - pre[u.v].w});
}
edge = tmp;
r = num[r];
n = id;
}
return 1;
}每轮必须先确认所有非根点都存在入边;若某点没有入边,则根无法到达该点,树形图不存在。上方接口返回是否有解,权值通过 res 输出。
时间复杂度:
左偏树优化。
时间复杂度:
