数据结构(16)-图之最小生成树

构造连通网的最小代价生成树称为最小生成树,也是一个图的极小连通子图,包含原图的所有顶点,且所有边的权值之和最小。

由于图的极小连通子图不一定是闭环的,而是一个树形结构,所以我们将其称为最小生成树。同一个图的最小生成树是不唯一的。

找到最小生成树,有两种经典的算法,普里姆算法和克鲁斯卡尔算法。

普里姆算法(Prim)

普里姆算法是以图的顶点为基础,从一个初始顶点开始,找到其他顶点权值最小的边,并把该顶点加入到已知顶点的集合中。当全部顶点都加入到集合时就完成了。普里姆算法的本质,是基于贪心算法。贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。

下面我们结合一个例子来看看普里姆算法,在下图中找出最小生成树:

prim示例.png
  1. 首先,我们选择一个顶点v_0,然后从这个顶点开始,此时,我们只能选择去v_1v_5,由于11>10,所以我们选择去v_1
  2. v_1出发,我们可以选择去v_2v_8v_6v_5,由于去v_5的权值11最小,我们选择去v_5
  3. v_5出发,我们可以选择去v_2v_8v_6v_4,去v_8的权值12最小,我们选择去v_8
  4. 以此类推,即可得到最小生成树。
    需要注意的是我们每次找的路径点并不是当前顶点的邻接点,而是当前顶点能到的顶点。

首先我们需要生成一个数组来装我们保存的顶点在顶点组中的下标,还需要生成一个数组来保存顶点之间的权值用以比较权值的大小进而选择权值最小的路径。代码实现如下:

void primMinTree(MGraph graph) {
    // 存储已经加入最小生成树的顶点
    int adj[MAX_VEX_COUNT];
    // 存储当前顶点对应权值 当某一个顶点被加入最小生成树 就把其对应值置为0 便于区分
    int adjWeights[MAX_VEX_COUNT];
    // 最小生成树权值之和
    int sum = 0;
    
    // 先假定一个顶点作为起点
    adj[0] = 0;
    adjWeights[0] = 0;
    for (int i = 1; i < graph.vertexNum; i++) {
        adj[i] = 0;
        adjWeights[i] = graph.arc[0][i];
    }
    
    // 记录每一次遍历的最小权值
    int min = 0;
    for (int i = 1; i < graph.vertexNum; i++) {
        min = INT_INFINITY;
        int minIndex = 0;
        for (int j = 0; j < graph.vertexNum; j++) {
            // 找到当前顶点对应的权值列表中最小权值 及其对应的下标
            if (adjWeights[j] != 0 && min > adjWeights[j]) {
                min = adjWeights[j];
                minIndex = j;
            }
        }
        
        sum += min;
        
        printf("(V%d, V%d)=%d\n", adj[minIndex], minIndex, min);
        
        // 将当前顶点对应的下标设置为0 用以判断某个顶点是否已经被我们选中,即加入了adj数组
        adjWeights[minIndex] = 0;
        
        
        // 更新下一个结点对应的权值列表
        // 遍历邻接矩阵minIndex这一行的权值
        for (int j = 1; j < graph.vertexNum; j++) {
            if (adjWeights[j] != 0 && graph.arc[minIndex][j] < adjWeights[j]) {
                adjWeights[j] = graph.arc[minIndex][j];
                adj[j] = minIndex;
            }
        }
    }
    
    printf("\n==%d==\n", sum);
}

// 输出结果如下:
(V0, V1)=10
(V0, V5)=11
(V1, V8)=12
(V8, V2)=8
(V1, V6)=16
(V6, V7)=19
(V7, V4)=7
(V7, V3)=16

==99==

由代码可以看出,m各结点,n条边,普里姆算法的时间复杂度为O(n^2),其更适合解决边的绸密度更高的连通网。

克鲁斯卡尔算法(Kruskal)

普里姆算法是以顶点为起点,克鲁斯卡尔算法则是以边为目标构建,因为权值在边上,我们可以直接去找最小权值的边来构建生成树,但是这样需要注意的是防止边形成环。

使用克鲁斯卡尔算法,我们就需要用到图的存储结构中的边集数组结构:

typedef struct {
    // 边起点(默认为下标小的一头)顶点下标
    int begin;
    // 边终点(默认为下标大的一头)顶点下标
    int end;
    int weight;
} EdgeSet;

下面我们使用上述例子来看看克鲁斯卡尔算法的计算规则:

kruskal示例.png
void swapEdgeSet(EdgeSet *edges,int i, int j) {
    int tempValue;
    
    //交换edges[i].begin 和 edges[j].begin 的值
    tempValue = edges[i].begin;
    edges[i].begin = edges[j].begin;
    edges[j].begin = tempValue;
    
    //交换edges[i].end 和 edges[j].end 的值
    tempValue = edges[i].end;
    edges[i].end = edges[j].end;
    edges[j].end = tempValue;
    
    //交换edges[i].weight 和 edges[j].weight 的值
    tempValue = edges[i].weight;
    edges[i].weight = edges[j].weight;
    edges[j].weight = tempValue;
}

int partition(EdgeSet edges[], int startIndex, int endIndex) {
    // 先把一个位置的元素设置为基准元素
    int standardWeight = edges[startIndex].weight;
    int left = startIndex;
    int right = endIndex;
    
    while (left != right) {
        // 控制right指针比较并左移
        while (left < right && edges[right].weight > standardWeight) {
            right -= 1;
        }
        
        // 控制left指针比较并右移
        while (left < right && edges[left].weight <= standardWeight) {
            left += 1;
        }
        
        if (left < right) {
            swapEdgeSet(edges, left, right);
        }
    }
    
    swapEdgeSet(edges, left, startIndex);
    return left;
}

void quickSort(EdgeSet edges[], int startIndex, int endIndex) {
    if (startIndex >= endIndex) {
        return;
    }
    
    // 找到基准元素的下标
    int standardIndex = partition(edges, startIndex, endIndex);
    quickSort(edges, startIndex, standardIndex - 1);
    quickSort(edges, standardIndex + 1, endIndex);
}

// 根据顶点f 和 parent数组找到当前顶点的尾部下标; 帮助我们判断2点之间是否存在闭环问题;
int find(int *parent, int f) {
    while (parent[f] > 0) {
        f = parent[f];
    }
    return f;
}

void kruskalMinTree(MGraph graph) {
    
    EdgeSet edges[MAX_VEX_COUNT];
    // 边集数组中 起始下标比结束下标小
    
    int edgeLen = 0;
    for (int i = 0; i < graph.vertexNum - 1; i++) {
        for (int j = i+1; j < graph.vertexNum; j++) {
            if (graph.arc[i][j] < INT_INFINITY) {
                edges[edgeLen].begin = i;
                edges[edgeLen].end = j;
                edges[edgeLen].weight = graph.arc[i][j];
                edgeLen += 1;
            }
        }
    }
   
    quickSort(edges, 0, graph.edageNum - 1);
//    printf("排序后\n");
//    for (int i = 0; i < graph.edageNum; i++) {
//        printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
//    }
    
    
    int sum = 0;
    // 引入一个辅助数组判断是否形成闭环
    int parent[MAX_VEX_COUNT] = {0};
    for (int i = 0; i < graph.edageNum; i++) {
        int b = find(parent, edges[i].begin);
        int e = find(parent, edges[i].end);
        
        // 如果b == e说明形成了闭环 
        // b != e说明该路径不会形成闭环 我们可以使用 
        if (b != e) {
            parent[b] = e;
            printf("(V%d, V%d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
            sum += edges[i].weight;
        }
    }
    
    printf("==%d==", sum);
}

// 控制台输出
(V4, V7) 7
(V2, V8) 8
(V0, V1) 10
(V0, V5) 11
(V1, V8) 12
(V3, V7) 16
(V1, V6) 16
(V6, V7) 19
==99==

由代码可以看出,m各结点,n条边,克鲁斯卡尔算法的时间复杂度为find的时间复杂度由变数n确定为O(loge),外层还有一个循环,加起来为O(e*loge),其更适合于求边稀疏的网的最小生成树。

参考文献:

  • 大话数据结构
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349