最小生成树
一个连通图的生成树
是一个极小的连通子图,它包含图中全部的n个顶点,但只有
构成一棵树的n-1条边。
所谓一个 带权图 的最小生成树,就是原图中边的权值最小的生成树 ,所谓最小是指 边的权值之和小于或者等于其它生成树的边的权值之和。
Prim算法求最小生成树
Prim 算法必须适用于 无向图
,初始化 g 数组时必须初始化为双向
Prim 算法求最小生成树的思想和 Dijkstra 算法求最短路差不多
适用于稠密图求最小生成树,稠密图使用邻接矩阵存储
时间复杂度:O(n ^ 2)
dist[]
数组,记录某个点距离目标连通块的距离
st[]
数组,记录某个点是否在目标连通块中
Prim 算法求最小生成树的步骤如下:
- 先将任意一个点(1)加入到集合中,找到集合外离集合最近的点,如果找到最近的点依然是正无穷,那么就说明无最小生成树,否则就将这个点加入集合,
st[]
改为true
- 如果新加入的点不是第一次加入集合的点,就要将这个点距离集合的点
dist[t]
加到答案 ans 中;如果第一次则不用,因为是以这个点为基准的 - 遍历所有点,使用新加入集合的点
t
更新所有在集合外的点 :g[t][j]
和dist[j]
取最小的 (因为g[t][j]
是 j 到 t 的距离,而 t 已经在集合中了,所以也可以是 j 到集合的距离。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, INF = 0X3f3f3f3f;
int n, m;
int g[N][N];
int st[N]; // 是否在集合中
int dist[N];// 某个点距离集合的距离
int Prim()
{
memset(dist, 0x3f, sizeof dist);
int ans = 0;
for(int i = 0; i < n; i++)
{
int t = -1; //
for(int j = 1; j <= n; j++)
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
if(i && dist[t] == INF) // 如果找到集合外最小的距离依然是 INF, 就直接返回
return INF;
if(i) ans += dist[t]; // 如果不是第一次
for(int j = 1; j <= n; j++)
if(!st[j]) dist[j] = min(g[t][j], dist[j]);
st[t] = true;
}
return ans;
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
while(m--)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
if(a == b) g[a][b] = g[b][a] = 0;
}
int ans = Prim();
if(ans == INF) cout << "impossible";
else cout << ans;
return 0;
}
Kruskal算法求最小生成树
Kruskal 算法适用于 稀疏图
求最小生成树(利用并查集)
因为要排序,所以用 结构体存储图
时间复杂度 :O(m* logm)
步骤:
- 先将 m 条边存在结构体数组中
- 将 m 条边按从小到大排序
- 遍历 m 条边,判断边上的两个点是否连通,如果没有,就将这两个点对应的连通块合并,并将对应的边权加在最小生成树上
- 记录一个 cnt 变量,每在集合中加上一个点,cnt ++,判断 cnt 是否等于 n - 1(加了 n - 1次),如果不等于,则不存在最小生成树(说明有的点没有和其他的连在一起,故不存在)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n, m;
int p[N];
struct g
{
int a, b, w;
}edgs[N];
int Find(int x)
{
if(p[x] != x) p[x] = Find(p[x]);
return p[x];
}
bool cmp(g& g1, g& g2)
{
return g1.w < g2.w;
}
pair<int, int> Kruskal()
{
sort(edgs + 1, edgs + m + 1, cmp);
for(int i = 1; i <= n; i++) p[i] = i; // 将所有的点都各自当作一个集合
int res = 0, cnt = 0;
for(int i = 1; i <= m; i++) // 遍历每条边
{
int a = edgs[i].a, b = edgs[i].b, w = edgs[i].w;
if(Find(a) != Find(b))
{
p[Find(a)] = Find(b); // 将 a 和 b 连通
res += w;
cnt ++; // 相当于加入一个点
}
}
return make_pair(res, cnt);
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
edgs[i] = {a, b, c};
}
auto ans = Kruskal();
if(ans.second != n - 1) cout << "impossible" << endl;
else cout << ans.first;
return 0;
}
最近公共祖先
Tarjan 算法求最近公共祖先
Tarjan 算法用于求并快速查询两个节点的最近公共祖先 LCA
,使用并查集 + DFS 来维护祖先节点
算法步骤:
- 初始化邻接表、并查集,写好需要的 Add函数,Find 函数等
- 初始化 qu 数组,绑定需要查询的两个点与查询的次序
- 进入 Tarjan 函数,将这个节点
x
变为 true,然后通过邻接表,递归遍历它的所有子节点,每次递归结束后,将子节点指向自己p[j] = x;
- 遍历当前节点
x
的查询数组qu
,如果x
对应的另一个查询值已经被访问过了,则另一个查询值的当前根节点
就是他们两个数的最近公共祖先! - 为什么被访问过的那个被绑定的节点的祖宗节点就是他们的最近公共祖先?更远的祖宗还没有用
p[]
连上,所以保证找到的那个, 就是距离最近的
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 500100;
const int M = 1000100;
int n, m, r;
int e[M], ne[M], h[N], idx;
int st[N]; // 一个点只能被遍历一次,
int p[N]; // 并查集
int ans[N]; // 记录查询结果
vector<PII> qu[N]; // a -> {b, i} b -> {a, i}; i 为第几次查询
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int Find(int x)
{
if (p[x] != x) p[x] = Find(p[x]);
return p[x];
}
void Tarjan(int x)
{
st[x] = true;// 进入递归就变 true
for (int i = h[x]; i != -1; i = ne[i]) // 遍历邻接表
{
int j = e[i];
if (!st[j])
{
Tarjan(j);
p[j] = x; // 将孩子 -> 父亲
}
}
// 遍历 x 的 qu 查询数组
for (int i = 0; i < qu[x].size(); i++)
{
int val = qu[x][i].first, id = qu[x][i].second;
if (st[val]) // 如果被搜过, 就直接可以计算出 x-val 的公共祖先
ans[id] = Find(val);
}
}
int main()
{
cin >> n >> m >> r;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++) p[i] = i;
for (int i = 0; i < n - 1; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
add(b, a);
}
for (int i = 1; i <= m; i++)
{
int a, b;
cin >> a >> b;
qu[a].push_back({ b, i });
qu[b].push_back({ a, i });
}
Tarjan(r);
for (int i = 1; i <= m; i++) cout << ans[i] << endl;
return 0;
}