图论 —— 生成树 —— 次小生成树
【概述】
对于给定的无向图 G=(V,E),设 T 是图 G 的一个最小生成树,那么,对于除 T 外的第二小的生成树 T' 即为图的次小生成树。
简单来说,最小生成树是生成树的最小解,次小生成树是生成树的次小解,它有可能和最小生成树的值一样,但肯定不能比最小生成树的值要小。
一般来说,求最小生成树的算法是 Prim 或 Kurskal,那么对于次小生成树,同样可以使用这两种算法来解。
对于求次小生成树来说,两种算法的思路都是相同的。首先求出最小生成树,再枚举每条不在最小生成树上的边,并把这条边放到最小生成树上面,此时一定会形成环,那么在这条环路中取出一条除新加入的边外的最长路,最终得到的权值就是次小生成树的权值。
【Prim 求解次小生成树】
使用 Prim 求解次小生成树需要使用一个二维数组 maxDis[i][j] 来表示最小生成树中 i 到 j 的最远距离,其是使用动态规划的思想来计算的,例如:当前节点为 x,其父节点为 per[x],根节点为 root,那么 maxDis[root][x] = max(maxDis[root][per[x]] , maxDis[per[x]][x]);
此外,还需要一个二维数组 connect[i][j] 表示最小生成树中这条边有没有被用到,剩下的就是模拟算法中所说的删边以及添边的操作。
int n,m,G[N][N];
bool vis[N],connect[N][N];
int dis[N],maxDis[N][N],per[N];
int prim(){
memset(maxDis,0,sizeof(maxDis));
memset(vis,false,sizeof(vis));
for(int i=1;i <=n; i++){
dis[i]=G[1][i];
per[i]=1;//父节点都是根节点
}
vis[1]=true;
dis[1]=0;
int res=0;
for(int i=1; i<n; i++){
int index=-1,temp=INF;
for(int j=1; j<=n; j++){
if(!vis[j] && dis[j]<temp){
index=j;
temp=dis[j];
}
}
if(index==-1)
return res;
vis[index]=1;
connect[index][per[index]]=false;//边已在最小生成树中
connect[per[index]][index]=false;//边已在最小生成树中
res+=temp;
maxDis[per[index]][index]=temp;//更新点之间的最大值
maxDis[index][per[index]]=temp;//更新点之间的最大值
for(int j=1; j<=n; j++){
if(j!=index && vis[j]){//更新已遍历过的节点
maxDis[index][j]=max(maxDis[j][per[index]],dis[index]);
maxDis[j][index]=max(maxDis[j][per[index]],dis[index]);
}
if(!vis[j] && G[index][j]<dis[j]){
dis[j]=G[index][j];
per[j]=index;
}
}
}
return res;
}
int main(){
scanf("%d%d",&n,&m);
memset(G,INF,sizeof(G));
memset(connect,false,sizeof(connect));
for(int i=0; i<m; i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
G[u][v]=w;
G[v][u]=w;
connect[u][v]=true;
connect[v][u]=true;
}
int res=prim();//求次小生成树
bool flag=false;
for(int i=1; !flag && i<=n; i++){//枚举点
for(int j=1 ; j<=n; j++){//枚举边
//某边未被使用或i~j的最大值大于图中最大值,次小生成树存在
if(connect[i][j]==false || G[i][j]==INF)
continue;
//边长度相同时表示最小生成树相同,次小生成树不存在
if(G[i][j]==maxDis[i][j]){
flag=true;
break;
}
}
}
if(flag)
printf("Not Unique!\n");
else
printf("%d\n",res);
return 0;
}
【Kurskal 求解次小生成树】
Kruskla 算法中枚举的边权值会依次增大,那么就会给计算提供一定的便利,但因为 Kruskal 的实现方式和 Prim 有所不同,因此 Kruskal 需要存储当前最小生成树中的节点,然后再去更新 maxDis 数组
int n,m;
struct Node{
int u,v,w;
bool vis;
bool operator <(const Node &rhs)const{
return w<rhs.w;
}
} node[N];
vector<int>G[1000];
int father[1000],maxDis[1000][1000];
int Find(int x){
return x==father[x]?x:father[x]=Find(father[x]);
}
void Kruskal(){
sort(node,node+m);
for(int i=0; i<=n; i++){//初始化
G[i].clear();
G[i].push_back(i);
father[i]=i;
}
int mst=0,k=0;//k为当前生成树中的点
for(int i=0; i<m; i++){//枚举边
if(k==n-1)//等于n-1个点
break;
int x=Find(node[i].u),y=Find(node[i].v);
if(x!=y){
k++;
mst+=node[i].w;
node[i].vis=true;//边已用过,标记
int lenX=G[x].size();
int lenY=G[y].size();
for(int j=0; j<lenX; j++){//更新两点之间距离的最大值
for(int k=0; k<lenY; k++){
maxDis[G[x][j]][G[y][k]]=node[i].w;//因为后面的边会越来越大,所以这里可以直接等于当前边的长度
maxDis[G[y][k]][G[x][j]]=node[i].w;
}
}
father[x]=y;
for(int j=0; j<lenX; j++)
G[y].push_back(G[x][j]);
}
}
int cimst=INF;//次小生成树权值
for(int i=0; i<m; i++)
if(!node[i].vis)
cimst=min(cimst,mst+node[i].w-maxDis[node[i].u][node[i].v]);
if(cimst>mst)
printf("%d\n",mst);
else
printf("Not Unique!\n");
}
int main(){
scanf("%d%d",&n,&m);
for(int i=0; i<m; i++){
scanf("%d%d%d",&node[i].u,&node[i].v,&node[i].w);
node[i].vis=false;
}
Kruskal();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效