在图论中,最小生成树(Minimum Spanning Tree, MST)是一个非常重要的概念。对于给定的带权无向连通图,最小生成树是一棵包含图中所有顶点且边权之和最小的树。它在网络设计、电路布线等实际应用中具有广泛的意义。本文将详细介绍两种常见的最小生成树算法:Prim算法和Kruskal算法,并提供C++实现代码。
一个连通图的生成树是指一个连通子图,它包含图中的所有顶点,但只有足以构成一棵树的边(即没有回路)。对于有nnn个顶点的连通图,其生成树有n−1n-1n−1条边。
在带权图中,最小生成树是所有生成树中边权之和最小的那棵。最小生成树可能不唯一,但其边权之和一定是最小的。
Prim算法是一种贪心算法,其基本思想是从一个任意选择的起始顶点开始,逐步将距离当前生成树最近的顶点加入到生成树中,直到所有顶点都被包含为止。具体来说,算法维护一个优先队列(或最小堆),用来存储尚未访问的顶点及其与当前生成树的最短距离。每次从优先队列中取出距离最小的顶点,并将其加入到生成树中,同时更新与其相邻顶点的距离。
以下是Prim算法的C++实现代码:
#include
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int INF = 0x3f3f3f3f;
const int MAXN = 5005;
int n, m, u, v, w, cnt, len;
ll sum;
bool vis[MAXN]; // 记录点是否已经在图中
int dis[MAXN][MAXN]; // 距离矩阵
vector<int> tos[MAXN];
struct cmp {
bool operator()(pii a, pii b) {
return a.first > b.first;
}
};
int main() {
cin >> n >> m;
memset(dis, 0x3f, sizeof dis);
while (m--) {
cin >> u >> v >> w;
dis[u][v] = min(w, dis[u][v]);
dis[v][u] = dis[u][v];
tos[u].push_back(v);
tos[v].push_back(u);
}
// 任选一点作为起点,以1为例
cnt = 1;
vis[1] = true;
len = tos[1].size();
priority_queue<pii, vector<pii>, cmp> pq;
for (int i = 0; i < len; i++) {
pq.push(pii(dis[1][tos[1][i]], tos[1][i]));
}
// 加完了所有点或已经用完了所有边退出
while (cnt != n && !pq.empty()) {
pii tmp = pq.top();
pq.pop();
v = tmp.second;
if (!vis[v]) {
cnt++;
vis[v] = true;
sum += tmp.first;
len = tos[v].size();
for (int i = 0; i < len; i++) {
if (!vis[tos[v][i]]) {
pq.push(pii(dis[v][tos[v][i]], tos[v][i]));
}
}
}
}
if (cnt == n) {
cout << sum << endl;
} else {
cout << "orz\n"; // 表示无法生成最小生成树
}
return 0;
}
dis
数组用于存储图的邻接矩阵,表示顶点之间的边权。tos
数组用于存储每个顶点的邻接顶点列表。vis
数组用于标记顶点是否已经被访问过。pq
是一个优先队列,用于存储尚未访问的顶点及其与当前生成树的最短距离。Kruskal算法也是一种贪心算法,其基本思想是按照边权值从小到大依次选择边,如果这条边的两个端点不在同一个连通块中,就把这条边加入到最小生成树的边集合中。具体来说,算法使用并查集(Disjoint Set Union, DSU)来维护顶点的连通性。
以下是Kruskal算法的C++实现代码:
#include
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int MAXN = 5005;
int n, m, u, v, w, cnt;
ll sum;
int pre[MAXN];
struct edg {
int u, v, w;
};
struct cmp {
bool operator()(edg e1, edg e2) {
return e1.w > e2.w;
}
};
int findroot(int x) {
int r = x, tmp;
while (r != pre[r]) {
r = pre[r];
}
while (r != pre[x]) {
tmp = pre[x];
pre[x] = r;
x = tmp;
}
return r;
}
void join(int x, int y) {
int fx = findroot(x), fy = findroot(y);
if (fx != fy) {
pre[fx] = fy;
}
}
int main() {
cin >> n >> m;
vector<edg> edges;
while (m--) {
cin >> u >> v >> w;
edges.push_back({u, v, w});
}
sort(edges.begin(), edges.end(), cmp());
for (int i = 1; i <= n; i++) {
pre[i] = i;
}
for (auto &e : edges) {
u = e.u;
v = e.v;
w = e.w;
if (findroot(u) != findroot(v)) {
cnt++;
sum += w;
join(u, v);
}
if (cnt == n - 1) {
break;
}
}
if (cnt == n - 1) {
cout << sum << endl;
} else {
cout << "orz\n"; // 表示无法生成最小生成树
}
return 0;
}
edges
向量用于存储图的所有边。pre
数组用于实现并查集,表示每个顶点的父节点。findroot
函数用于查找顶点的根节点,并进行路径压缩。join
函数用于合并两个顶点的连通分量。最小生成树是图论中的一个重要概念,Prim算法和Kruskal算法是两种常见的求解最小生成树的算法。Prim算法适合稠密图,而Kruskal算法适合稀疏图。在实际应用中,可以根据图的具体情况选择合适的算法。通过本文的介绍和C++实现代码,希望读者能够更好地理解和应用这两种算法。
定义:反阿克曼函数α(x)\alpha(x)α(x)定义为最大的整数mmm,使得Ackermann函数A(m,m)≤xA(m, m)\leq xA(m,m)≤x。由于阿克曼函数的增长速度极快(比指数级还快),其反函数α(x)\alpha(x)α(x)的增长则非常缓慢。
增长特性:对于可以想象到的nnn,α(n)\alpha(n)α(n)通常都是在555之内的,这意味着反阿克曼函数增长极为缓慢,几乎可以看作是一个常数。
要找到阿克曼函数的反函数,即α(n)\alpha(n)α(n),可以通过观察阿克曼函数的增长模式来设计一个近似算法。为了达到O(log2n)O(\log_2 n)O(log2n)的时间复杂度,可以采用二分查找的思想。首先猜测一个中间值mmm,然后检查A(m,m)A(m, m)A(m,m)是否小于或等于nnn。如果大于,则在左半区间继续搜索;如果小于或等于,就在右半区间继续搜索。每次都将搜索范围减半,直到找到满足条件的最大mmm或者搜索范围变得足够小。