先上代码。
cpp
/*
1.边权可能为 0, max设为负数
2.赋值max/se_max赋初始值时[0]也需要赋值:[1]的父亲就是父亲初始值0
3.se_max维护时≠max
4.更新max时也要修改se_max
5.倍增已跳到同一个节点:直接返回答案
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n,m;//n 表示点数,m 表示边数
ll find_fa[100005];//表示并查集的父亲
ll fa[100005][18]/*树上倍增数组*/,maxn[100005][18];//max树上倍增
ll lmaxn[100005][18]/*min树上倍增*/,deep[100005];//最小生成树上节点的深度
ll sum,calc,ans=LLONG_MAX;
//sum:最小生成树边权和 calc:所有可以换成某条边的边边权最大值 ans:记录答案
vector<pair<ll,ll> >g[100005];//最小生成树的建树
struct edge
{//表示一条边
ll u,v,w;
bool operator<(const edge &node)const
{//定义如何比较两条边
return w<node.w;
}
}a[300005];
int find(int x){return (x==find_fa[x])?x:find_fa[x]=find(find_fa[x]);}//并查集
void kruskal()
{//求出最小生成树并建树
for(int i=1; i<=m; i++)
{
if(find(a[i].u)==find(a[i].v)) continue;
find_fa[find(a[i].u)]=find(a[i].v);
g[a[i].u].push_back({a[i].v,a[i].w});
g[a[i].v].push_back({a[i].u,a[i].w});
sum+=a[i].w;
}
}
void dfs(int u,int fat,int dep)
{//记录最小生成树上所有点的父亲节点以及该点到其父亲边的边权
for(int i=0; i<g[u].size(); i++)
{
if(g[u][i].first==fat) continue;
fa[g[u][i].first][0]=u;
maxn[g[u][i].first][0]=g[u][i].second;
deep[g[u][i].first]=dep+1;
dfs(g[u][i].first,u,dep+1);
}
}
void init()
{//倍增的预处理部分
for(int i=1; i<=17; i++)
{//注意这里必须要先枚举跳多少节点
for(int j=1; j<=n; j++)
{
//更新最大值和次大值,这里要注意千万不要让次大值和最大值相等
maxn[j][i]=maxn[j][i-1];
lmaxn[j][i]=lmaxn[j][i-1];
if(maxn[fa[j][i-1]][i-1]>maxn[j][i])
{
lmaxn[j][i]=maxn[j][i];
maxn[j][i]=maxn[fa[j][i-1]][i-1];
}
if(maxn[fa[j][i-1]][i-1]>lmaxn[j][i]&&maxn[fa[j][i-1]][i-1]<maxn[j][i]) lmaxn[j][i]=maxn[fa[j][i-1]][i-1];
if(lmaxn[fa[j][i-1]][i-1]>lmaxn[j][i]) lmaxn[j][i]=lmaxn[fa[j][i-1]][i-1];
fa[j][i]=fa[fa[j][i-1]][i-1];//更新往上跳的父亲节点
}
}
}
int getmax(ll u,ll v,ll forbid)
{//求出所有可以换成某条边的边边权最大值
ll maxans=-1;
if(deep[u]<deep[v]) swap(u,v);
for(int i=17; i>=0; i--)
{
if(deep[fa[u][i]]>=deep[v])
{
if(maxn[u][i]!=forbid) maxans=max(maxans,maxn[u][i]);//注意不要让答案和最小生成树边权和相等,下面同理
else maxans=max(maxans,lmaxn[u][i]);
u=fa[u][i];
}
}
for(int i=17; i>=0; i--)
{
if(fa[u][i]!=fa[v][i])
{
if(maxn[u][i]!=forbid) maxans=max(maxans,maxn[u][i]);
else maxans=max(maxans,lmaxn[u][i]);
if(maxn[v][i]!=forbid) maxans=max(maxans,maxn[v][i]);
else maxans=max(maxans,lmaxn[v][i]);
u=fa[u][i];
v=fa[v][i];
}
}
if(u==v) return maxans;//注意已经跳到同一个节点了就直接返回最大值
if(maxn[u][0]!=forbid) maxans=max(maxans,maxn[u][0]);
else maxans=max(maxans,lmaxn[u][0]);
if(maxn[v][0]!=forbid) maxans=max(maxans,maxn[v][0]);
else maxans=max(maxans,lmaxn[v][0]);
return maxans;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1; i<=n; i++) find_fa[i]=i;
memset(maxn,-0x3f,sizeof(maxn));
memset(lmaxn,-0x3f,sizeof(lmaxn));
for(int i=1; i<=m; i++) cin>>a[i].u>>a[i].v>>a[i].w;
sort(a+1,a+m+1);
kruskal();
dfs(1,0,0);
init();
for(int i=1; i<=m; i++)
{
calc=getmax(a[i].u,a[i].v,a[i].w);
if(calc>=0)ans=min(ans,sum-calc+a[i].w);
}
cout<<ans;
}
/*
1.边权可能为 0, max设为负数
2.赋值max/se_max赋初始值时[0]也需要赋值:[1]的父亲就是父亲初始值0
3.se_max维护时≠max
4.更新max时也要修改se_max
5.倍增已跳到同一个节点:直接返回答案
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n,m;//n 表示点数,m 表示边数
ll find_fa[100005];//表示并查集的父亲
ll fa[100005][18]/*树上倍增数组*/,maxn[100005][18];//max树上倍增
ll lmaxn[100005][18]/*min树上倍增*/,deep[100005];//最小生成树上节点的深度
ll sum,calc,ans=LLONG_MAX;
//sum:最小生成树边权和 calc:所有可以换成某条边的边边权最大值 ans:记录答案
vector<pair<ll,ll> >g[100005];//最小生成树的建树
struct edge
{//表示一条边
ll u,v,w;
bool operator<(const edge &node)const
{//定义如何比较两条边
return w<node.w;
}
}a[300005];
int find(int x){return (x==find_fa[x])?x:find_fa[x]=find(find_fa[x]);}//并查集
void kruskal()
{//求出最小生成树并建树
for(int i=1; i<=m; i++)
{
if(find(a[i].u)==find(a[i].v)) continue;
find_fa[find(a[i].u)]=find(a[i].v);
g[a[i].u].push_back({a[i].v,a[i].w});
g[a[i].v].push_back({a[i].u,a[i].w});
sum+=a[i].w;
}
}
void dfs(int u,int fat,int dep)
{//记录最小生成树上所有点的父亲节点以及该点到其父亲边的边权
for(int i=0; i<g[u].size(); i++)
{
if(g[u][i].first==fat) continue;
fa[g[u][i].first][0]=u;
maxn[g[u][i].first][0]=g[u][i].second;
deep[g[u][i].first]=dep+1;
dfs(g[u][i].first,u,dep+1);
}
}
void init()
{//倍增的预处理部分
for(int i=1; i<=17; i++)
{//注意这里必须要先枚举跳多少节点
for(int j=1; j<=n; j++)
{
//更新最大值和次大值,这里要注意千万不要让次大值和最大值相等
maxn[j][i]=maxn[j][i-1];
lmaxn[j][i]=lmaxn[j][i-1];
if(maxn[fa[j][i-1]][i-1]>maxn[j][i])
{
lmaxn[j][i]=maxn[j][i];
maxn[j][i]=maxn[fa[j][i-1]][i-1];
}
if(maxn[fa[j][i-1]][i-1]>lmaxn[j][i]&&maxn[fa[j][i-1]][i-1]<maxn[j][i]) lmaxn[j][i]=maxn[fa[j][i-1]][i-1];
if(lmaxn[fa[j][i-1]][i-1]>lmaxn[j][i]) lmaxn[j][i]=lmaxn[fa[j][i-1]][i-1];
fa[j][i]=fa[fa[j][i-1]][i-1];//更新往上跳的父亲节点
}
}
}
int getmax(ll u,ll v,ll forbid)
{//求出所有可以换成某条边的边边权最大值
ll maxans=-1;
if(deep[u]<deep[v]) swap(u,v);
for(int i=17; i>=0; i--)
{
if(deep[fa[u][i]]>=deep[v])
{
if(maxn[u][i]!=forbid) maxans=max(maxans,maxn[u][i]);//注意不要让答案和最小生成树边权和相等,下面同理
else maxans=max(maxans,lmaxn[u][i]);
u=fa[u][i];
}
}
for(int i=17; i>=0; i--)
{
if(fa[u][i]!=fa[v][i])
{
if(maxn[u][i]!=forbid) maxans=max(maxans,maxn[u][i]);
else maxans=max(maxans,lmaxn[u][i]);
if(maxn[v][i]!=forbid) maxans=max(maxans,maxn[v][i]);
else maxans=max(maxans,lmaxn[v][i]);
u=fa[u][i];
v=fa[v][i];
}
}
if(u==v) return maxans;//注意已经跳到同一个节点了就直接返回最大值
if(maxn[u][0]!=forbid) maxans=max(maxans,maxn[u][0]);
else maxans=max(maxans,lmaxn[u][0]);
if(maxn[v][0]!=forbid) maxans=max(maxans,maxn[v][0]);
else maxans=max(maxans,lmaxn[v][0]);
return maxans;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>m;
for(int i=1; i<=n; i++) find_fa[i]=i;
memset(maxn,-0x3f,sizeof(maxn));
memset(lmaxn,-0x3f,sizeof(lmaxn));
for(int i=1; i<=m; i++) cin>>a[i].u>>a[i].v>>a[i].w;
sort(a+1,a+m+1);
kruskal();
dfs(1,0,0);
init();
for(int i=1; i<=m; i++)
{
calc=getmax(a[i].u,a[i].v,a[i].w);
if(calc>=0)ans=min(ans,sum-calc+a[i].w);
}
cout<<ans;
}
这是我见到的最长的图论竞赛代码。(截至2025.12)
借鉴了一篇CSDN,可惜不是正解,但还是非常感谢。^_^
回到正题。
一、基本概念
1.1 什么是最小生成树(MST)?
-
对于带权连通无向图,生成树是包含所有顶点的无环连通子图
-
最小生成树 是所有生成树中边权之和最小的
1.2 什么是次小生成树(SMST)?
-
严格次小生成树 :边权之和严格大于 最小生成树,但在所有生成树中第二小
-
非严格次小生成树 :边权之和不小于最小生成树
示例图:(鸣谢Deep***k)
5
A ----- B
| \ |
3| 1\ |2
| \ |
C ----- D
4
最小生成树:A-C(3), C-D(4), D-B(2) 总权值 = 9
次小生成树:A-B(5), A-C(3), C-D(4) 总权值 = 12
二、算法思路
2.1 核心思想
次小生成树 = 最小生成树 - 一条树边 + 一条非树边
但必须保证:
1. 加入的非树边(u,v)会形成环
2. 删除的树边在环中,且尽可能大
3. 新边权 > 删除边权(严格次小)
2.2 算法步骤
Step 1: 使用Kruskal或Prim求最小生成树MST,记录总权值sum
Step 2: 在MST上预处理树上路径的最大值和次大值
Step 3: 枚举每条非树边(u,v,w)
Step 4: 找到MST中u到v路径上的最大边权max_w
如果max_w == w,则找次大边权sec_w
Step 5: 计算新生成树权值 = sum - max_w/sec_w + w
Step 6: 取所有结果的最小值
三、代码实现详解
3.1 数据结构定义
cpp
// 边结构体
struct edge
{
int u, v, w;
bool operator<(const edge &node)const
{
return w < node.w;
}
}a[300005];
// 关键数组
int fa[100005][18]; // 倍增父亲节点,fa[i][j]表示i向上跳2^j的节点
int maxn[100005][18]; // 路径最大值,maxn[i][j]表示i到fa[i][j]路径的最大边权
int lmaxn[100005][18]; // 路径次大值(严格小于最大值)
int deep[100005]; // 节点深度
vector<pair<int,int>> g[100005]; // MST的邻接表
// 边结构体
struct edge
{
int u, v, w;
bool operator<(const edge &node)const
{
return w < node.w;
}
}a[300005];
// 关键数组
int fa[100005][18]; // 倍增父亲节点,fa[i][j]表示i向上跳2^j的节点
int maxn[100005][18]; // 路径最大值,maxn[i][j]表示i到fa[i][j]路径的最大边权
int lmaxn[100005][18]; // 路径次大值(严格小于最大值)
int deep[100005]; // 节点深度
vector<pair<int,int>> g[100005]; // MST的邻接表
3.2 关键步骤表格说明
| 步骤 | 函数名 | 功能 | 时间复杂度 |
|---|---|---|---|
| 1 | kruskal() | 求最小生成树并建树 | O(m log m) |
| 2 | dfs() | 初始化深度、父亲、边权 | O(n) |
| 3 | init() | 倍增预处理最大/次大值 | O(n log n) |
| 4 | getmax() | 查询路径上≠forbid的最大值 | O(log n) |
| 5 | 主循环 | 枚举所有边,计算次小生成树 | O(m log n) |
3.3 倍增预处理详解
为什么需要次大值?
假设路径上的边权为:3, 5, 5, 7
最大值 = 7
严格次大值 = 5(注意:不是第二个5,因为要严格小于最大值)
当非树边权值w=7时,我们不能用最大值替换(否则还是MST)
需要用次大值5替换,得到sum - 5 + 7 > sum
预处理递推关系
cpp
for (int i = 1; i <= MAXN; i++) {
for (int j = 1; j <= n; j++) {
// 从j跳2^(i-1)到中间点mid
int mid = fa[j][i-1];
// 更新最大值
maxn[j][i] = max(maxn[j][i-1], maxn[mid][i-1]);
// 更新次大值(关键!必须严格小于最大值)
lmaxn[j][i] = max(lmaxn[j][i-1], lmaxn[mid][i-1]);
if (maxn[j][i-1] < maxn[j][i] && maxn[j][i-1] > lmaxn[j][i])
lmaxn[j][i] = maxn[j][i-1];
if (maxn[mid][i-1] < maxn[j][i] && maxn[mid][i-1] > lmaxn[j][i])
lmaxn[j][i] = maxn[mid][i-1];
fa[j][i] = fa[mid][i-1]; // 更新父亲
}
}
for (int i = 1; i <= MAXN; i++) {
for (int j = 1; j <= n; j++) {
// 从j跳2^(i-1)到中间点mid
int mid = fa[j][i-1];
// 更新最大值
maxn[j][i] = max(maxn[j][i-1], maxn[mid][i-1]);
// 更新次大值(关键!必须严格小于最大值)
lmaxn[j][i] = max(lmaxn[j][i-1], lmaxn[mid][i-1]);
if (maxn[j][i-1] < maxn[j][i] && maxn[j][i-1] > lmaxn[j][i])
lmaxn[j][i] = maxn[j][i-1];
if (maxn[mid][i-1] < maxn[j][i] && maxn[mid][i-1] > lmaxn[j][i])
lmaxn[j][i] = maxn[mid][i-1];
fa[j][i] = fa[mid][i-1]; // 更新父亲
}
}
3.4 路径查询算法(LCA思路)
查询u到v路径上≠forbid的最大边权:
1. 先将u和v调整到同一深度
while (deep[u] > deep[v]) {
用maxn[u][i]更新答案
u = fa[u][i]
}
2. 同时向上跳,直到到达LCA
while (u != v) {
如果fa[u][i] != fa[v][i],则更新两边的最大值
然后u和v同时向上跳
}
3. 注意:每次比较都要跳过等于forbid的值
再次鸣谢Deep***k.

四、关键问题与解决方案
4.1 问题汇总表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 边权为0 | 初始值设为0会导致误判 | 初始化为负无穷 |
| 次大值等于最大值 | 违反"严格"次小 | 维护时确保严格小于 |
| 答案等于MST权值 | 替换的边权相同 | 检查新边权>旧边权 |
| 父节点初始值 | 1号节点的父亲是0 | 深度和最大值都要正确初始化 |
4.2 示例分析
输入示例:
7 15
...(边列表)
执行过程:
1. Kruskal得到MST边权总和sum
2. 预处理树上路径信息
3. 对每条非树边(a[i].u, a[i].v, a[i].w):
- 查询MST中u到v路径的最大值max_w
- 如果max_w == a[i].w,查询次大值sec_w
- 计算new_sum = sum - max_w/sec_w + a[i].w
- 更新ans = min(ans, new_sum)
五、复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| Kruskal排序 | O(m log m) | O(m) |
| Kruskal建树 | O(m α(n)) | O(n) |
| DFS初始化 | O(n) | O(n) |
| 倍增预处理 | O(n log n) | O(n log n) |
| 查询路径 | O(log n) | O(1) |
| 总复杂度 | O(m log n) | O(n log n) |
六、优化技巧
-
内存优化:如果n很大,可以使用链式前向星代替vector
-
常数优化:预处理log值,避免重复计算
-
剪枝:如果当前非树边权值大于等于ans-sum+max_w,可以跳过
-
并行查询:同时维护严格次大值,避免二次查询
七、常见错误
// 错误1:次大值更新不严格
if (val > lmaxn && val < maxn) // 正确
if (val > lmaxn && val <= maxn) // 错误!不能等于最大值
// 错误2:忘记处理跳到同一节点的情况
if (u == v) return maxans; // 必须及时返回
// 错误3:初始值设置不当
memset(maxn, -0x3f, sizeof(maxn)); // 正确:设为很小的负数
memset(maxn, 0, sizeof(maxn)); // 错误:边权可能为0
八、其他
-
严格第k小生成树:多次替换,使用堆维护候选方案
-
动态最小生成树:支持边权修改,使用LCT维护
-
最小度限制生成树:指定某个点的度数