给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树。
设最小生成树的边权之和为 sum,严格次小生成树就是指边权之和大于 sum 的生成树中最小的一个。
输入格式
第一行包含两个整数 N 和 M。
接下来 M 行,每行包含三个整数 x,y,z,表示点 x 和点 y 之前存在一条边,边的权值为 zz。
输出格式
包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)
数据范围
N≤105,M≤3×105
输入样例:
5 6 1 2 1 1 3 2 2 4 3 3 5 4 3 4 3 4 5 6
输出样例:
11
若不知道最小生成树可以看看这篇:(18条消息) 次小生成树模板 :《信息学奥赛一本通》 , USACO 秘密的牛奶运输_wsh1931的博客-CSDN博客
时间复杂度O(m)
步骤:
1:先求出最小生成树,并记录树中的总权值sum。
2:预处理三个数组记录的是树边,fa[i][j], d1[i][j], d2[i][j]。fa[i][j]表示从点 i 跳2^j 步之后所跳到达的点为fa[i][j]。d1[i][j], d2[i][j]分别为从 i 点开始跳2 ^ j步之后所到达的点fa[i][j]中路径的最大值与次大值
1:先求出最小生成树。
3:枚举每条非树边a, b,权值为w。在最小生成树中找出 a 到 b 的最大的权值假设为d,找出sum + w - d他的值为大于sum的最小值即为严格最小生成树
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
int p[N];
int depth[N];
int fa[N][17];
int d1[N][17], d2[N][17];
int h[N], e[M], ne[M], w[M], idx;
struct Edge
{
int a, b, w;
bool used;
bool operator < (const Edge &W) const
{
return w < W.w;
}
}edge[M];
void add(int a, int b, int c)//邻接表
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx ++ ;
}
int find(int x)//并查集
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void bfs()
{
queue<int> q;
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;//设置边界,若不了解看推荐的博客
q.push(1);//这里以任意点为根节点都可以,我这里假设的是以1为根节点
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)//若j还未被更新
{
depth[j] = depth[t] + 1;
fa[j][0] = t;//j往上跳2^0为点t
q.push(j);
d1[j][0] = w[i], d2[j][0] = -INF;//最大值为w[i],要求次大值没有初始化为-INF.
for (int k = 1; k <= 16; k ++ )
{
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
//[j, j + 2 ^ k]步直接的最大值必在以上四个值中
d1[j][k] = -INF, d2[j][k] = -INF;//要求的是最大值所以初始化为负无穷
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
if (d > d1[j][k])//若可以更新最大值
{
d2[j][k] = d1[j][k];
d1[j][k] = d;
}
else if (d2[j][k] < d && d < d1[j][k]) d2[j][k] = d;//若可以更新次大值,注意要严格小于最大值
}
}
}
}
}
}
LL lca(int a, int b, int w)
{
if (a == b) return INF;
if (depth[a] < depth[b]) swap(a, b);//保证a所在的位置比b深好进行下面操作
int cnt = 0;
static int distance[N * 2];//要记录次大值与最大值所以大小为2 * N;用static是为了节省空间
for (int k = 16; k >= 0; k -- )//将a节点跳到与b同一层的深度
if (depth[fa[a][k]] >= depth[b])//若a跳2^k的深度大于b的深度则将a跳过去
{
distance[cnt ++ ] = d1[a][k];//记录[a, a + 2 ^ k]路径之间的值
distance[cnt ++ ] = d2[a][k];//要先纪律顺序不能反否则a会被更新
a = fa[a][k];//将a跳到fa[a][k]
}
if (a != b)//说明他们跳到同一深度后还不是同一节点
{
for (int k = 16; k >= 0; k -- )//将a, b跳到a, b的最近公共祖先的下一层
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];//记录a, b的最大与次大值
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k];
b = fa[b][k];
}
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d2[a][0];
distance[cnt ++ ] = d1[b][0];
distance[cnt ++ ] = d2[b][0];
}
LL max1 = -INF, max2 = -INF;//找出最大值与次大值
for (int i = 0; i < cnt; i ++ )
{
if (distance[i] > max1)//若最大值可被更新
{
max2 = max1;
max1 = distance[i];
}
else if (max2 < distance[i] && distance[i] < max1) max2 = distance[i];//次大值可被更新
}
if (w > max1) return w - max1;//找出符合条件的w - max1的最小值然后加上main函数里的sum即为
if (w > max2) return w - max2;//最小生成树
return INF;//说明不存在可以被更新次小生成树。
}
LL kruscal()
{
LL res = 0;
sort(edge, edge + m);
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i ++ ) p[i] = i;
for (int i = 0; i < m; i ++ )
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int fa = find(a), fb = find(b);
if (fa != fb)
{
p[fa] = fb;
res += w;
edge[i].used = true;
add(a, b, w), add(b, a, w);
}
}
return res;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruscal();//求最小生成树
bfs();//预处理fa, d1, d2数组
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].used)//枚举分数边
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));//找出大于sum的最小值即次小生成树
}
cout << res << endl;
return 0;
}