最小生成树(MST):Prim / Kruskal

* 假设T1集合是已加入最小生成树中的点,T2集合是剩下的待加入T2的点 
* 我们要做的是把T2集合中离T1最近的那个点,加入T1 
* 所以我们需要知道: 
* 集合T2内各顶点到集合T1的距离 
*  
* 为此,我们用两个数组: 
* cost[i]:用来表示T2中点i到T1的距离; 
* pre[i]:用来表示T2中点i和T1中哪个点最近(为了输出加入时的路径,不输出路径则不需要此数组) 
* visited[i],代表i加入了T1集合  
*  
* NO.1 
* 选取起始点v,将v加入T1,并对剩余各点进行距离初始化: initCost() 
* NO.2   
* 选择一个在T2集合中,距离T1集合最近的点k: findNext() 
* NO.3 
* 将k加入到集合T1中:visited[k] = 1
* NO.4   
* 更新剩余T2中的各点到T1集合的距离 
* (如果因为新加入的点k而使得T2中的点到T1的距离缩小,将此点的前导点改为k) 
*if     edge[k][i]<cost[i] 
*then   cost[i]=edge[k][i];  pre[i]=k; 

* 这就完成了一次操作,加入一条边。 
* 由于MST一定是n-1条边,进行n-1次后就完成了MST。

#include<stdio.h>
#include<string.h>

#define INF 0xfffffff						// infinity
#define MAXSIZE 20

int num, n;                                 // num: line; n: vertex
int edge[MAXSIZE][MAXSIZE];                 // 邻接矩阵
int cost[MAXSIZE], pre[MAXSIZE], visited[MAXSIZE];     // 距离标记;前导点标记
                                            // 所有数组从1开始

void Prim(int u);

int main()
{
    int i, j;
    int row, col, value;
    
    memset(edge, 0, sizeof(edge));			//void * memset (void * p,int c,size_t n);
    
    scanf("%d%d", &n, &num);
    
    for(i = 0; i < num; i++)				// 构造邻接矩阵
    {
        scanf("%d%d%d", &row, &col, &value);
        edge[row][col] = edge[col][row] = value;
    }
    
    for(i = 1; i <= n; i++)				    //将邻接矩阵对角线赋为0
        for(j = 1; j <= n; j++)				//原来就为0的赋为INF
            if(i == j)
                edge[i][j] = 0;
            else if( edge[i][j] == 0 )
                edge[i][j] = INF;
            
            Prim(1);				    	//Prim算法,不妨从顶点1开始
            
            return 0;
}

//NO.1 对距离进行初始化
void initCost(int u)
{
    int i;
    for(i = 1; i <= n; i++)
    {
        cost[i] = edge[u][i];
        pre[i] = u;
    }
    cost[u] = 0;
    visited[u] = 1;
}

//NO.2 找到不在T1中,边的权值最小的那个点
int findNext()
{
    int i;
    int mincost = INF, v = -1;
    for(i = 1; i <= n; i++)
    {
        if(visited[i] != 1 && cost[i] < mincost)
        {
            mincost = cost[i];
            v = i;
        }
    }
    return v;
}

//NO.4 加入后,对距离进行更新
void updateCost(int v)
{
    int i;
    for(i = 1; i <= n; i++)
    {
        if(visited[i] != 1 && edge[v][i] < cost[i])
        {
            cost[i] = edge[v][i];
            pre[i] = v;
        }
    }
}

//在dijkstra算法中逆向打印点v到源点的路径
//在prim算法中不需要
void print_path(int v) //打印最短路的路径(反向)
{
    while(v != pre[v]) //前驱
    {    
        printf("%d<--", v);
        v = pre[v];
    }
    printf("%d\n", v);
}

void Prim(int u)
{
    int i, j;
    int v = -1;
    int weight = 0;
    
    initCost(u);                            //NO.1 对距离进行初始化
    
    for(i = 1; i < n; i++)					//循环n-1次,加入n-1个顶点
    {
        v = findNext();             		//NO.2 找到不在T1中,边的权值最小的那个点
        if(v != -1)						    //如果找到那个点v
        {
            printf("%d--(%d)-->%d\n", 
                pre[v], cost[v], v);     //打印加入的流程
            visited[v] = 1;				    //NO.3 将v点加入T1
             weight += cost[v];
            
            updateCost(v);					//NO.4 对距离进行更新
        } 
    }
    printf("The sum-weight of the MST is %d\n", weight);

    //just for fun
    print_path(7);                          //比如打印点7到源点的逆向路径玩玩儿=.=
}





相对于dijkstra而言,Prim最小生成树和Dijkstra最短路径的微小的差别为:“权值最低”的定义不同!

Prim的“权值最低”是相对于U中的任意一点而言的,也就是把U中的点看成一个整体,每次寻找V - U中跟U的距离最小(也就是跟U中任意一点的距离最小)的一点加入U;而Dijkstra的“权值最低”是相对于v0而言的,也就是每次寻找V-U中跟v0的距离最小的一点加入U

因此对于最短路径,只要将上面MST的路径距离更新的代码改成以下部分(多加了lowcost[v]):

if(visited[i] != -1 && edge[v][i] + cost[v] < cost[i])
{
    cost[i] = edge[v][i] + cost[v];
    pre[i] = v;
}

就OK了!


相对于Prim的复杂度O(n*n),用Kruskal是个更简单也更好的选择,它的复杂度完全由排序决定。如果是用快排,则Kruskal的复杂度可以简化为O(n*logn)。

//Kruskal
#include<stdio.h>
#include<stdlib.h>

#define N 1010

typedef struct node
{
    int begin, end;                                 //这里的begin和end其实并没有“有向图”的概念,还是代表无向图
    int value;
}Node;

Node edge[N];                                       //存储边的信息
int root[N];                                        //存储每个点所在的集合的根

int rising(const void *a, const void *b)
{
    return ((Node *)a)->value - ((Node *)b)->value; //注意强制转型的优先级没有->高
}

int find(int point)                             //并查集的“查”
{
    if(root[point] == -1)                       //如果根结点信息为-1,则自己就是自己所在集合的根
        return point;                                   //所以返回自己
    return root[point] = find(root[point]);     //否则压缩路径,并返回point所在集合的根=.=话说这一句信息量好大
}

//话说刚刚的return一次性将压缩路径和返回集合所在的根都解决了,相当于下面这个函数

/*
int pre[1000 ];
int find(int x)                  //查找根节点
{ 
    int r=x;
    while ( pre[r ] != r )       //找到根节点 r
          r=pre[r ];
 
    int i=x , j ;
    while( i != r )              //路径压缩
    {
         j = pre[ i ];           // 在改变上级之前用临时变量  j 记录下他的值 
         pre[ i ]= r ;           //把上级改为根节点
         i=j;
    }
    return r ;
}
*/

void join(int x, int y)                         //并查集的“并”
{
    if( find(x) != find(y) )         //如果两个点xy所在集合的根不一样,
        root[ find(x) ] = find(y);   //则将一个点合并到另一个集合(实际操作是置两个点的根为同一个根)
}

int main()
{
    int n, m, i, sum = 0;

    for(i=0; i<N; i++)                          //刚开始将所有的点的根初始化为-1
        root[i] = -1;
    scanf("%d%d", &n, &m);
    for(i=0; i<m; i++)
        scanf("%d%d%d", &edge[i].begin, &edge[i].end, &edge[i].value);

    qsort(edge, m, sizeof(Node), rising);       //按照边的权值快排nlogn

    for(i=0; i<m; i++)
    {
        if( find(edge[i].begin) != find(edge[i].end) )//如果两个点不在一个集合,意味着连接两点不会成环
        {
            join(edge[i].begin, edge[i].end);           //将一个点并到另一个点所在的集合
            sum += edge[i].value;
        }
    }
    printf("%d", sum);
    return 0;
}



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Prim算法: ```java import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.PriorityQueue; import java.util.Set; public class PrimMST { public static List<Edge> primMST(Graph graph) { List<Edge> result = new ArrayList<>(); Set<Integer> visited = new HashSet<>(); PriorityQueue<Edge> pq = new PriorityQueue<>((a, b) -> a.weight - b.weight); visited.add(0); for (Edge e : graph.edges[0]) { pq.offer(e); } while (!pq.isEmpty() && visited.size() < graph.vertices) { Edge e = pq.poll(); if (visited.contains(e.to)) { continue; } visited.add(e.to); result.add(e); for (Edge next : graph.edges[e.to]) { if (!visited.contains(next.to)) { pq.offer(next); } } } return result; } } ``` Kruskal算法: ```java import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class KruskalMST { public static List<Edge> kruskalMST(Graph graph) { List<Edge> result = new ArrayList<>(); UnionFind uf = new UnionFind(graph.vertices); List<Edge> edges = new ArrayList<>(); for (int i = 0; i < graph.vertices; i++) { edges.addAll(graph.edges[i]); } // sort edges by weight Collections.sort(edges, Comparator.comparingInt(a -> a.weight)); for (Edge e : edges) { int root1 = uf.find(e.from); int root2 = uf.find(e.to); if (root1 != root2) { uf.union(root1, root2); result.add(e); } } return result; } } ``` 测试代码: ```java import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.util.List; public class TestMST { @Test public void testPrim() { Graph g = new Graph(5); g.addEdge(0, 1, 2); g.addEdge(0, 3, 6); g.addEdge(1, 3, 8); g.addEdge(1, 2, 3); g.addEdge(1, 4, 5); g.addEdge(2, 4, 7); g.addEdge(3, 4, 9); List<Edge> result = PrimMST.primMST(g); Assertions.assertEquals(result.size(), 4); Assertions.assertTrue(result.contains(new Edge(0, 1, 2))); Assertions.assertTrue(result.contains(new Edge(1, 2, 3))); Assertions.assertTrue(result.contains(new Edge(1, 4, 5))); Assertions.assertTrue(result.contains(new Edge(0, 3, 6))); } @Test public void testKruskal() { Graph g = new Graph(5); g.addEdge(0, 1, 2); g.addEdge(0, 3, 6); g.addEdge(1, 3, 8); g.addEdge(1, 2, 3); g.addEdge(1, 4, 5); g.addEdge(2, 4, 7); g.addEdge(3, 4, 9); List<Edge> result = KruskalMST.kruskalMST(g); Assertions.assertEquals(result.size(), 4); Assertions.assertTrue(result.contains(new Edge(0, 1, 2))); Assertions.assertTrue(result.contains(new Edge(1, 2, 3))); Assertions.assertTrue(result.contains(new Edge(1, 4, 5))); Assertions.assertTrue(result.contains(new Edge(0, 3, 6))); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值