洛谷题单指南-最小生成树-P1967 [NOIP 2013 提高组] 货车运输
原题链接:https://www.luogu.com.cn/problem/P1967
题意解读:在n个节点的带权无向图中,有q个询问,求两点之间的路径中,最小边的最大值。
解题思路:
1、初步分析
要求两点之间路径中最小边的最大值,可以将kruskal算法变化一下:
从大到小一次处理每条边,当将边u->v连通,此时如果对于一个询问两点a、b是连通的,那么本次加入的u->v的边权值就是答案。
换句话说,要求的是在图的最大生成树中,两点之间路径上的最小边。
2、朴素想法
枚举法1:用变化的kruskal算法,从大到小处理边,连通一条边后,枚举所有询问,如果询问的两点可以连通,更新该询问的答案。复杂度为O(m*n)。
枚举法2:用变化的kruskal算法,先求最大生成树(由于不一定连通,可以添加一个虚拟0点,和其余点连接一条长度为-1的边),再基于最大生成树的边建图,从每一个点开始DFS求出该点到所有点路径上的最小边。复杂度为O(n^2)。
3、优化思路
可以针对枚举法2进行优化。
类似于求严格次小生成树时采用的优化方式,通过LCA和倍增来计算两点之间路径上的最小边。
设fa[i][j]表示i的第2^j个祖先,d[i][j]表示从i往上跳2^j个祖先的路径上的最小边
初始化:d[i][j] = min(d[i][j - 1], d[fa[i][j - 1]][j - 1])
计算:在LCA的过程中,路径上的最小边就是LCA处理中每段区间的最小边
算法步骤:
1、构建最大生成树
使用 Kruskal 算法对图中的边按权值从大到小排序。注意加入虚拟0点。
使用并查集判断是否将一条边加入生成树,若加入,则更新该边是否属于生成树。
对生成树用邻接表建图。
2、构建 LCA 数据结构
使用 DFS 遍历生成树,计算每个节点的深度、父节点以及到父节点路径上的最小边权。
使用倍增法计算每个节点的 2^i 个祖先和路径上的最小边。
3、处理查询
对每一个查询x y,通过 LCA 算法计算从节点x到节点y的路径上的最小边权,并输出结果。
100分代码:
#include <bits/stdc++.h>
using namespace std;
// 常量定义:最大节点数N,最大边数M,INF为无穷大
const int N = 10005, M = 60005, INF = 0x3f3f3f3f;
// 边的结构体
struct Edge
{
int u, v, w; // 边的两个节点u和v,边的权重w
bool intree; // 标记该边是否在生成树中
bool operator < (const Edge &e) const
{
return w > e.w; // 边按权重从大到小排序
}
} edges[M]; // 边集数组
int cnt; // 边数计数器
int p[N]; // 并查集数组,用于判断两个节点是否在同一集合
int h[N], e[M], w[M], ne[M], idx; // 邻接表,h为邻接表头,e为目标节点数组,w为权重数组,ne为下一条边数组,idx为当前边下标
int depth[N], fa[N][14], d[N][14]; // depth为节点深度,fa[i][j]为节点i的2^j父节点,d[i][j]为节点i到2^j父节点路径上的最小边
int n, m, q; // n为节点数,m为边数,q为询问次数
// 向邻接表添加一条边
void add(int a, int b, int c)
{
e[++idx] = b; // 添加目标节点b
w[idx] = c; // 设置边的权重c
ne[idx] = h[a]; // 记录当前邻接表的头节点
h[a] = idx; // 更新a的邻接表头指针
}
// 并查集查找操作,路径压缩
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]); // 递归查找根节点并进行路径压缩
return p[x]; // 返回节点x的根节点
}
// Kruskal算法求解最大生成树
void kruskal()
{
sort(edges + 1, edges + cnt + 1); // 按边权从大到小排序
for(int i = 1; i <= n; i++) p[i] = i; // 初始化并查集,初始时每个节点为一个集合
for(int i = 1; i <= cnt; i++)
{
int a = find(edges[i].u); // 找到边的两个端点的根节点
int b = find(edges[i].v);
if(a != b) // 如果两个节点不属于同一集合
{
p[a] = b; // 合并两个集合
edges[i].intree = true; // 标记该边加入到生成树中
}
}
}
// 根据Kruskal算法生成的生成树构建邻接表
void build()
{
memset(h, -1, sizeof(h)); // 初始化邻接表,-1表示没有邻接边
for(int i = 1; i <= cnt; i++) // 遍历所有边
{
if(edges[i].intree) // 只考虑在生成树中的边
{
int a = edges[i].u, b = edges[i].v, c = edges[i].w;
add(a, b, c); // 添加边a->b
add(b, a, c); // 添加边b->a(无向图)
}
}
}
// 深度优先搜索(DFS),计算节点的深度、父节点以及最小边
void dfs(int u, int parent, int dep, int len)
{
depth[u] = dep; // 当前节点的深度
fa[u][0] = parent; // 当前节点的父节点
d[u][0] = len; // 当前边的权重(最小边)
for(int i = 1; i <= 13; i++) // 处理每个节点的2^i父节点和最小边
{
d[u][i] = min(d[u][i - 1], d[fa[u][i - 1]][i - 1]); // 更新节点u到2^i父节点路径上的最小边
fa[u][i] = fa[fa[u][i - 1]][i - 1]; // 更新2^i父节点
}
for(int i = h[u]; ~i; i = ne[i]) // 遍历u的所有邻接边
{
int v = e[i];
if(v == parent) continue; // 如果是父节点,跳过
dfs(v, u, dep + 1, w[i]); // 递归深度优先搜索,深度加1,边权为当前边的权重
}
}
// 求解两个节点的最近公共祖先(LCA),并返回路径上的最小边权
int lca(int u, int v)
{
int res = INF; // 初始化最小边为无穷大
if(depth[u] < depth[v]) swap(u, v); // 保证u的深度大于等于v
for(int i = 13; i >= 0; i--) // 从高到低遍历2^i父节点
{
if(fa[u][i] != -1 && depth[fa[u][i]] >= depth[v]) // 如果u的2^i父节点深度不小于v的深度
{
res = min(res, d[u][i]); // 更新最小边权
u = fa[u][i]; // u跳到2^i父节点
}
}
if(u == v) return res; // 如果u和v已经相同,返回当前的最小边权
for(int i = 13; i >= 0; i--) // 遍历2^i父节点
{
if(fa[u][i] != fa[v][i]) // 如果u和v的2^i父节点不同
{
res = min(res, min(d[u][i], d[v][i])); // 更新最小边权
u = fa[u][i]; // u跳到2^i父节点
v = fa[v][i]; // v跳到2^i父节点
}
}
res = min(res, min(d[u][0], d[v][0])); // 最后检查u和v的父节点
return res; // 返回最小边权
}
int main()
{
cin >> n >> m; // 输入节点数n和边数m
for(int i = 1; i <= m; i++) // 输入所有边
{
cnt++;
cin >> edges[cnt].u >> edges[cnt].v >> edges[cnt].w;
}
// 加入虚拟0点与各个点的边,虚拟点与其他节点的权重为-1
for(int i = 1; i <= n; i++)
{
cnt++;
edges[cnt].u = 0;
edges[cnt].v = i;
edges[cnt].w = -1; // 权重设置为-1,表示虚拟点
}
// 运行Kruskal算法,求解最大生成树
kruskal();
// 构建最大生成树的邻接表
build();
// 初始化深度优先搜索,0作为根节点
dfs(0, -1, 1, INF);
// 处理询问
cin >> q;
while(q--) // 处理q个询问
{
int x, y;
cin >> x >> y;
cout << lca(x, y) << endl; // 输出x和y的LCA路径中的最小边权
}
return 0;
}