www.luogu.com.cn

概念/定义

一个连通图的生成树是一个极小的连通子图,它包含图中全部的 nn 个顶点,但只有构成一棵树的 n1n-1 条边。而最小生成树就是一个带权图的生成树,并且使得原图中边的权值最小的生成树 ,所谓最小是指边的权值之和小于或者等于其它生成树的边的权值之和。

思路(Kruskal适用于稀疏图,Prim适用于稠密图)

Kruskal 算法

整体思路:

Kruskal 是一种使用贪心 + 排序+并查集的最小生成树算法,通过每两个点之间的最小距离累加并且将两个点合并,最终生成一种生成树的算法。

时间复杂度:

O(mlogm)O(m \log m)(其中 mm 点表示边数)。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
struct Edge
{
int u, v, w; // 存下入边,出边,权值
bool operator < (const Edge &rhs) const // 重载运算符,将权值为第一关键词非降序排序
{
if (w != rhs.w) return w < rhs.w;
if (u != rhs.u) return u < rhs.u;
return v < rhs.v;
}
};
vector <Edge> graph;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int pre[N];

int rt(int x)
{
return pre[x] = (pre[x] == x ? x : rt(pre[x]));
}

inline void merge(int x, int y)
{
pre[rt(x)] = rt(y);
}

inline bool check(int x, int y)
{
return rt(x) == rt(y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main()
{
// ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;

for (int i = 1; i <= m; i++)
{
int u, v, w; cin >> u >> v >> w;
graph.push_back({u, v, w}); // 存边
}
sort(graph.begin(), graph.end()); // 排序

ll ans = 0;
for (int i = 1; i <= n; i++) pre[i] = i; // 并查集初始化

for (auto t : graph)
{
auto &[a, b, val] = t;
if (check(a, b)) continue; // 如果已经联通,则跳过
ans += val; // 直接加上这两条边的权值
merge(a, b); // 合并该两条边
}

bool tag = true;
for (int i = 1; i < n; i++)
if (rt(i) != rt(i + 1)) tag = false; // 判断是否可以形成连通图
if (tag == false) cout << "orz\n";
else cout << ans;

return 0;
}

Prim 算法

整体思路:

Prim 的思想是将任意节点作为根,再找出与之相邻的所有边中最短的一条边,再将新节点更新并以此节点作为根继续搜,同时维护一个数组作用为已用点到未用点的最短距离,并且一个 boolbool 数组来记录没有存入生成树的边。实现操作类似 Dijkstra 算法。

核心思路:对于任意一个顶点 vv,连接到该顶点的所有边中的一条最短边必然属于最小生成树。

时间复杂度:

朴素:O(n2)O(n^2),堆优化:O(nlogn)O(n \log n)(其中 nn 表示点数)。

代码实现:

1
2
3
4
5
struct Edge
{
int ver, val;
bool operator < (const Edge &rhs) const { return val > rhs.val; }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void prim()
{
memset(dist, 0x3f, sizeof dist); // 将 dist 数组初始化为无穷大
bitset<N> intree;
priority_queue<Edge> heap;

heap.push({1, 0}); // 先将第一个点加入队列当中
dist[1] = 0;

while (!heap.empty())
{
auto [ver, val] = heap.top(); heap.pop(); //取出队列当中最短的一个边

if (intree[ver]) continue;
intree[ver] = true;

ans += val; // 加上权值并且标记掉

for (int i = h[ver]; ~i; i = ne[i]) // 遍历该点的所有出点
{
int j = e[i];
if (!intree[j] && w[i] < dist[j]) // 如果存在没有标记且离该点更近的点
dist[j] = w[i], heap.push({j, w[i]}); // 更新dis数组 将这个点加入队列当中
}
}

bool tag = true; // 判断是否可以形成连通图
for (int i = 1; i <= n; i++) if (!intree[i]) tag = false;
if (tag == true) cout << ans << '\n';
else cout << "orz\n";
}