求最小生成树,稠密图(prim算法),稀疏图(kruskal算法),两种结合(稀疏图中部分稠密)

稠密图(prim算法):

题目:

稠密图的定义:

对于稠密图,边的数量接近于顶点数量的平方,即 m≈O(n^2)。在这种情况下,邻接矩阵表示法非常高效,可以快速访问和更新边权重。Prim算法在稠密图中的时间复杂度为 O(n2),适合处理这种边数较多的情况。

Prim算法的基本思想:

  1. 初始化:将每个点的距离初始化为正无穷,并将第一个点的距离初始化为0。使用一个数组 st 来记录每个点是否已经在最小生成树集合中。
  2. 迭代构建最小生成树
    • 在每一步中,找到一个不在集合中的、离集合距离最近的点 t,将 t 加入到集合中,并用 t 来更新其他点到集合的距离。
    • 如果当前选择的点的距离仍然是正无穷,说明最小生成树不存在,因为有些点无法连接到集合中。
  3. 更新距离:每次找到新的点 t 后,用 t 更新其他未在集合中的点的距离,距离更新的依据是通过 t 到这些点的边的权重。

变量说明:

  • n:图的顶点数。
  • m:图的边数。
  • g[N][N]:邻接矩阵表示的图,其中 g[i][j] 表示顶点 i 到顶点 j 的边权重。
  • dist[N]:记录每个点到最小生成树集合的最小距离。
  • st[N]:记录每个点是否在最小生成树集合中。

代码流程

  1. 初始化图的邻接矩阵:将所有边权重初始化为正无穷(使用 0x3f3f3f3f 表示)。
  2. 读入图的边信息:根据输入更新邻接矩阵 g
  3. Prim算法核心部分
    • 初始化距离数组 dist 和集合标记数组 st
    • 使用双重循环构建最小生成树:每次找到一个离集合最近的点 t,将其加入集合,并更新其他点到集合的距离。
  4. 输出结果:如果构建的最小生成树的总权重为正无穷,则输出 "impossible",否则输出最小生成树的总权重。

代码:

// 与朴素版Dijkstra()算法相似;
// 初始化每个点的距离为正无穷;
// 找到t:不在集合中离集合距离最近的那个点;  
// 用t来更新其他点到集合的距离;
#include <iostream>
#include <cstring>
using namespace std;
int n,m;
const int N=510;
const int INF=0x3f3f3f3f;
int g[N][N]; // 稠密图; 
int dist[N]; 
bool st[N]; // 判断是否在集合中;

int prim()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0; // 第一个点必须在集合中;
    int res=0; // 最小生成树的距离之和;
    for(int i=1;i<=n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
        {
            if(st[j]==0&&(t==-1||dist[j]<dist[t])){
                t=j;
            }
        }
        st[t]=true;
        if(dist[t]==INF) return INF; // 如果除了第一个点外的到这个集合距离是正无穷,说明最小生成树不存在;
        res+=dist[t];

        for(int j=1;j<=n;j++){
            dist[j]=min(dist[j],g[t][j]);// 与朴素版Dijkstra不同: g[t][j]就是到集合的距离;
            // 不要求是到起点的距离;
        }
    }
    return res;
}
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);// 防止重边取最小的;
     }
     int t=prim();
     if(t>INF/2) cout<<"impossible";
     else  cout<<t;

}

稀疏图(kruskal算法):

题目:

稀疏图的定义:

对于稀疏图,边的数量远小于顶点数的平方,因此Kruskal算法的效率更高。排序操作和并查集的使用使得Kruskal算法的时间复杂度为 O(mlog⁡m+nα(n)),其中 α(n) 是阿克曼函数的反函数,近似为常数。相比于Prim算法在稀疏图中可能的 O(n2) 时间复杂度,Kruskal算法更高效。

Kruskal算法的基本思想:

  1. 排序:将所有边按权重从小到大排序。
  2. 初始化并查集:用并查集(Disjoint Set Union,DSU)来管理集合,用来判断两个顶点是否属于同一个连通分量。
  3. 逐边检查:按权重从小到大依次检查每条边,如果这条边连接的两个顶点属于不同的集合,则将它们合并,并将这条边加入最小生成树。
  4. 终止条件:当加入的边数等于 n−1 时,算法结束,因为最小生成树已经包含 n−1 条边。

变量说明:

  • n:图的顶点数。
  • m:图的边数。
  • p[N]:并查集数组,用于管理集合。
  • Edge:结构体,表示图中的一条边,包含两个顶点 ab 以及权重 c

代码流程:

  • 初始化并查集:每个顶点自成一个集合。
  • 边排序:按权重从小到大对所有边排序。
  • 边检查和合并:逐边检查并用并查集管理连通性。
  • 输出结果:如果成功构建最小生成树,输出其总权重,否则输出 "impossible"。

代码:

// krusal()算法: O(nlogn);  排序O(nlogn)+并查集(O(n));
// 将边按照从小到大进行排序;
// 如果a点和b点不在一个集合中,就将a,b加入集合;
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
const int N=2e5+10; const int INF=0x3f3f3f3f;
int p[N];
struct Edge
{
    int a,b,c;
}edge[N];

bool cmp(struct Edge x,struct Edge y)
{
    return x.c<y.c;
}
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int krusal()
{
    for(int i=1;i<=n;i++) p[i]=i;
    sort(edge,edge+m,cmp);

    int res=0,cnt=0;
    for(int i=0;i<m;i++)
    {
        auto t=edge[i];
        int a=t.a;
        int b=t.b;
        int c=t.c;
        int x=find(a); int y=find(b);
        if(x!=y)
        {
            p[x]=y;
            res+=c;
            cnt++;
        }
    }
    if(cnt!=n-1) return INF;
    else return res;
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<m;i++) cin>>edge[i].a>>edge[i].b>>edge[i].c;
    int t=krusal();
    if(t>INF/2) cout<<"impossible";
    else   cout<<t;
    return 0;

}

两种结合(稀疏图中部分稠密):

题目:

分析:

观察可知这道题目要求的是最小生成树,由于是多组询问,所以需要分类讨论,令点集S的长度为cnt,当cnt<sqrt(n),利用prim算法,当cnt>=sqrt(n)时利用kruskal算法。还有一个问题,利用什么来存图?可以利用map<int,int>mp[N]来存图。假设我们有 N 个节点,且每个节点都有一个与其他节点的关联度信息,我们可以使用 map<int, int> mp[N] 来存储这些信息。其中,mp[i] 存储节点 i 与其他节点的关联度。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct Node{
    int s,e,v;
}e[N]; // 定义边结构体,存储边的起点s、终点e和权重v。
int f[N]; // 并查集父节点数组。
int a[N]; // 查询中的节点数组。
bool vis[N]; // 节点访问标志。
map<int,int> w[N]; // 邻接表,存储边的权重。
int dis[N]; // Prim算法中的距离数组。

bool cmp(struct Node A,struct Node B)
{
	return A.v<B.v; // 比较函数,用于边权重的排序。
}

int find(int x){
    if(f[x]!=x) f[x]=find(f[x]); // 并查集查找函数。
    return f[x];
}

bool merge(int x,int y)
{
    x=find(x),y=find(y);
    if(x==y)return 0; // 如果两个节点已经在同一集合中,则返回false。
    f[y]=x;
    return 1;
}

int main(){
    int n,m,q;
    scanf("%d%d%d",&n,&m,&q); // 输入节点数、边数和查询数。
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&e[i].s,&e[i].e,&e[i].v);
        if(w[e[i].s].count(e[i].e))
		{
            w[e[i].s][e[i].e]=min(w[e[i].s][e[i].e],e[i].v);
            w[e[i].e][e[i].s]=min(w[e[i].e][e[i].s],e[i].v);
        }
		else
		{
            w[e[i].s][e[i].e]=e[i].v;
            w[e[i].e][e[i].s]=e[i].v;
        }
    }
    sort(e+1,e+m+1,cmp); // 按照边的权重从小到大排序。

    while(q--){
        int k;
        scanf("%d",&k);
        if(k<=sqrt(n)){
            // 使用Prim算法
            for(int i=1;i<=k;i++){
                scanf("%d",&a[i]);
                dis[i]=1e9+7;
                vis[a[i]]=0;
            }
            long long res=0;
            int now=1;
            dis[1]=0; vis[a[1]]=1;
            for(int i=1;i<k;i++){
                for(int j=1;j<=k;j++){
                    if(vis[a[j]])continue;
                    if(w[a[now]].count(a[j])){
                        dis[j]=min(dis[j],w[a[now]][a[j]]);
                    }
                }
                long long temp=1e9+7;
                int nxt=-1;
                for(int j=1;j<=k;j++){
                    if(vis[a[j]])continue;
                    if(dis[j]<temp){
                        temp=dis[j];
                        nxt=j;
                    }
                }
                if(nxt==-1){
                    res=-1;
                    break;
                }
                res+=temp;
                vis[a[nxt]]=1;
                now=nxt;
            }
            printf("%lld\n",res);
            for(int i=1;i<=k;i++){
                vis[a[i]]=0;
            }
        } else {
            // 使用Kruskal算法
            long long res=0;
            for(int i=1;i<=k;i++){
                scanf("%d",&a[i]);
                f[a[i]]=a[i];
                vis[a[i]]=1;
            }
            for(int i=1;i<=m;i++){
                if(vis[e[i].s]&&vis[e[i].e]){
                    if(merge(e[i].s,e[i].e))
                        res+=e[i].v;
                }
            }
            for(int i=1;i<=k;i++){
                if(find(a[i])!=find(a[1])){
                    res=-1;
                    break;
                }
            }
            printf("%lld\n",res);
            for(int i=1;i<=k;i++){
                vis[a[i]]=0;
            }
        }
    }
    return 0;
}

详细步骤:

  1. 输入处理:程序首先读取节点数、边数和查询数,然后读取每条边的信息,存储在边数组e和邻接表w中。
  2. 边排序:对所有边按照权重从小到大排序,以便后续Kruskal算法使用。
  3. 查询处理
    • 小规模查询:使用Prim算法
      • 初始化距离数组dis和访问标志数组vis
      • 从任意一个节点开始(这里从a[1]),逐步将最近的未访问节点加入集合,并更新其他节点的最短距离。
      • 重复直到所有节点都被访问或无法再找到未访问节点。
    • 大规模查询:使用Kruskal算法
      • 初始化并查集数组f和访问标志数组vis
      • 遍历所有边,尝试将每条边加入最小生成树,如果两个节点不在同一集合中,则合并它们的集合并累加边的权重。
      • 检查所有节点是否在同一集合中,如果有节点不在同一集合中,说明这些节点无法构成连通图,返回-1。
  4. 输出结果:输出每个查询的结果。

注意事项:

  • 代码中使用scanfprintf进行输入输出以提高效率。
  • sort函数用于边的排序,排序后方便Kruskal算法逐一处理边。
  • 使用并查集(Disjoint Set Union,DSU)实现Kruskal算法中的合并和查找操作。
  • 稠密图使用Prim算法,稀疏图使用Kruskal算法,两者结合可以处理不同规模的查询。
  • 42
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值