大話西遊之王道考研數據結構第八講---圖以及最小生成樹

                                                     第八講--圖以及最小生成樹

複習

1.樹有哪幾種表示方法

2.樹、森林、二叉樹之間轉換中左右孩子的定義

3.54 32 47 49 51 48構造二叉排序樹,並刪除47,然後計算成功和失敗的平均查找長度。

4.a:30 s:31 b:20 c:10 x:3 q:7 d:6 構造哈夫曼樹並計算WPL

5.爲什麼哈夫曼樹中沒有一個碼是其他碼的前綴

6.n個結點構造哈夫曼以後,有多少個結點

一、圖

1.1 圖的一些定義

圖一般可以分爲有向圖和無向圖(樹可以是空樹,但是圖不能是空圖)。有向圖也就是圖中的邊是有方向的,邊集合一般是E={<1,2>,<2,1>,<3,1>}。無向圖最也就是圖中的邊是沒有方向的,邊集合一般是E={<1,2>,<3,2>,<3,1>}

(定義可以看書上,沒什麼好講的)。

1.什麼是完全圖。含有n個頂點的無向完全圖有幾條邊?

2.什麼是連通圖,什麼是極小連通子圖,什麼是極大連通子圖,這是針對有向圖還是無向圖。

3.什麼是強聯通圖,什麼是強聯通分量,這是針對有向圖還是無向圖

4.如果一個圖有n個頂點,並且有小於n-1條邊,那麼這個圖是連通的還是不連通的?

5.連通圖的生成樹是包含圖中全部頂點的一個極小連通子圖。也就是用最少的邊把所有頂點連起來,那麼最小的邊數是多少(如果有n個頂點)。

6.頂點的度,針對有向圖是什麼概念,針對無向圖是什麼概念。

7.無向圖全部頂點的度之和和邊數的關係是什麼

1.2 圖的存儲結構

圖主要的有兩種存儲結構,鄰接矩陣和鄰接鏈表(十字鏈表等那些野路子就不要記了)。看過書可能感覺得到,最近學到的線性表、棧、隊列、樹的存儲結構一般都是由兩種,一種是用數組存,一種是用鏈表存。

舉個栗子:

我們有如下關係:唐僧認識猴哥,猴哥認識玉皇 ,猴哥認識閻王,如來佛祖認識玉皇。

我們一共出現了唐僧、猴哥、玉皇,閻王,如來五個人。

圖也是如此,領接矩陣就是數組,只不過是二維數組,領接鏈表就是鏈表,只不過一個結點一個鏈表。

先看下領接矩陣:

#define MAX_VERTEX_NUM 20
typedef struct
{
    char vexs[MAX_VERTEX_NUM];//點集
    int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邊集
    int vex_num,arc_num;
}MGraph,*Graph;

很簡單,arcs[2][3] 就是存的第3個結點和第4個結點(下標從0開始),當結點沒有權重這些東西時候(權重在最小生成樹和最短路里面用到),arcs[2][3] = 1表示 第3個結點和第4個結點是連通的,如果是無向圖的話,必還有arcs[3][2] = 1。有向圖的話就可以反向沒有。

這是一個無向圖,A認識B,肯定有B認識A(假設不存在A是你,B是王力宏,這種你認識他,他不認識你的情況)。主對角線都是0,這是我們的規定,方便統計。

再看下領接鏈表:

typedef char VertexType;
typedef int EdgeType;
#define MaxVex 100
typedef struct EdgeNode //邊表結點
{
	int adjvex; //鄰接點域,存儲鄰接頂點對應的下標
	EdgeType weight; //用於存儲權值,對於非網圖可以不需要
	struct EdgeNode *next; //鏈域,指向下一個鄰接點
}EdgeNode;
typedef struct VertexNode  //頂點表結點
{
	VertexType data;  //頂點域,存儲頂點信息
	EdgeNode *firstedge; //邊表頭指針
}VertexNode,AdjList[MaxVex];
 
typedef struct
{
	AdjList adjList;
	int numVertexes,numEdges; //圖中當前頂點數和邊數
}GraphAdjList;

這個就比較麻煩了,考試不會考這些~看看就行,主要說下鄰接鏈表的一些概念:

領接表需要一個存結點的地方,和存邊的地方,好的一點是鄰接表裏面邊是增加一個邊才擴展一個空間的,就不會像鄰接矩陣一樣一開始就把所有可能存在的邊的空間申請好,因此對於稀疏矩陣,用鄰接鏈表存儲會比較好。那麼在有N個邊的情況下(有向圖裏面1->2 2->1算兩條邊,無向圖算一條邊),無向圖所需存儲空間爲O(|V| + 2|E|)(無向圖一條邊需要存兩次),有向圖所需存儲空間爲O(|V| + |E|)

1.3 圖的遍歷

二叉樹的遍歷一般有四種(前序,中序,後序,層序)。因爲圖中和一個結點連接的其他結點之間沒有順序關係,所以圖的遍歷可以分爲深度優先遍歷(DFS,相當於二叉樹的前中後序遍歷)和廣度優先遍歷(BFS,相當於二叉樹的層序遍歷)。

這兩個代碼不太會考,主要考給一個圖,能不能把這兩種遍歷寫出來,這裏我們也把這兩個遍歷的代碼貼上來,感興趣的可以看看:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<queue>
#include<iostream>
#define Max 9999999
using namespace std;
int Graph[100][100];
int visit[100];
int n,m;
void InitGraph()
{
    for(int i = 0;i<n;i++)
    {
        for(int j = 0;j<n;j++)
        {
            if(i == j)
                Graph[i][j] = 0;
            else
                Graph[i][j] = Max;
        }
    }
}
void DFS(int v)
{
    printf("%d ",v);
    visit[v] = 1;
    for(int i =1;i<=n;i++)
    {
        if(visit[i] == 0)
            DFS(i);
    }
}
int main()
{
    int a,b,v;
    queue<int> q;
    scanf("%d",&n);
    InitGraph();
    scanf("%d",&m);
    for(int i= 0;i<m;i++)
    {
        scanf("%d%d%d",&a,&b,&v);
        Graph[a][b] = v;
        Graph[b][a] = v;
    }
    memset(visit,0,m*sizeof(int));
    for(int i = 1;i<=n;i++)
    {
        if(visit[i]==0)
        {
            DFS(i);
        }
    }
    printf("\n");
    memset(visit,0,(m+1)*sizeof(int));
    //下面是用到隊列的層序遍歷
    q.push(1);
    visit[1] = 1;
    while(!q.empty())
    {
        int t = q.front();
        printf("%d ",t);

        q.pop();
        for(int i = 1;i<=n;i++)
        {
            if(visit[i] == 0 && Graph[t][i] != 0 && Graph[t][i]!=Max)
            {
                q.push(i);
                visit[i] = 1;
            }

        }
    }
    printf("\n");
	return 0;
}

我們主要練習一下這兩個遍歷的結果:

遍歷時候,我們需要規定遍歷的起始點,這裏我們定爲0,(同一個結點連接的兩個結點,按照編號小的先遍歷,這是一般規矩,不按這個規則也不能算錯)這是一個有向圖。

深度遍歷結果爲:0,1,3,5,2,4

廣度遍歷結果爲:0,1,2,3,5,4

如果是無向圖呢?

深度遍歷結果爲:0,1,3,4,5,2

廣度遍歷結果爲:0,1,2,3,5,4

1.4 判斷一個圖是否是連通的

通過任何一個結點,都可以找到其他所有的結點,這樣的圖就是連通的。判斷連通時候,可以弄一個表,類似圖的領接矩陣一樣,如果連通就畫個對號,最後如果除去主對角線元素以外,都有對號就說明是連通的。

二、最小生成樹(必考大題)

前面的多是概念,看看書應該能懂。最小生成樹是必考大題。我當初學的時候,老是把最小生成樹和最短路弄混,尤其最小生成樹又一般分爲Kruskal和Prim兩種算法,最短路又有Dijkstra算法。就感覺很混,我在把最短路講完以後,對這倆東西做一個對比。

最小生成樹的意思是,現在處於祖國剛剛成立,國家還不是很富有,但是我們又希望走社會主義現代化道路,帶領人民實現共同富裕,俗話說要致富先修路,我們國家準備修一個鐵路網,連接祖國各地的主要城市,也就是圖中的這幾個城市。不同城市之間,修鐵路所要花費在圖中標明瞭(武漢到北京是1塊錢......西安到拉薩是3塊錢)。因爲我們還很窮,不能把這些鐵路都修好,只能修最少的鐵路,花最少的錢,使得每個城市都能到達另一個城市。那麼最少修幾條鐵路?一共花多少錢?

Prim算法,這是一個沒有大局觀念的拓荒者,

1.他總是會從一個結點開始,作爲他率先征服的領地,然後每次看看當前能修的鐵路里面,哪個鐵路造價最小(鐵路的一端是拓荒者所處的位置A,另一端是還未能抵達的城市B),最小的我們就修,然後把B加入拓荒者所征服的版圖內。

2.更新拓荒者能夠拓荒的鐵路造價表,因爲新加入的B->C,他有可能會比A->C的造價小,所以需要更新。

3.重複1-2直到沒有新的領地。

比如,最開始我們把北加入到征服的領地中,然後更新鐵路造價表,發現武的造價最低(紅色的),然後把武加入到已征服的領地中,更新鐵路造價表(因爲武漢可以達到拉薩和福州,而且比原來的造價小,所以更新)。這樣一直做下去就好了。

有一個問題是,圖中綠色的部分,我們知道北京到西安的造價是6,武漢加進來以後,武漢到西安的造價也是6。那麼我們最後修的到底是北京到西安的路呢還是武漢到西安的路呢?這就說明,最小生成樹的結果不止有一種。考試也會考把所有的可能畫出來(這裏起始點是告訴了且是固定的)。

 

可以看到Prim算法,只在乎當前的利益,每次都管當前的最小,最後達到全局最小,這也就是貪心的基本思想,其得到的結果是局部最優。

Kruskal算法,這是一個有大局觀念的規劃者

1.將所有的邊的造價進行排序,1,2,3,4,5,5,6,6,6,6。

2.看一下,當前最小的邊的兩個結點是不是連通的,如果不是連通的,就把這個邊加進來,然後這兩個點設置爲連通。

如果是連通的,看檢查下一條邊

3.直到所有邊都檢查完。

這裏檢查連通性,我們需要用到並查集算法,這是算法書裏講到的東西,建議可以把這個東西弄懂,有一個博客講的非常清楚,看一看絕對懂,我整個寫這一系列博客的靈感也來源於這篇博客:並查集。我就不再這篇博客面前班門弄斧了~。大家可以看看這個,說不定你會愛上算法~

最後綠色說明,這時候這四條邊都可以選擇。之所以在第四步->第五步時候,爲什麼不考慮造價爲5的結點?因爲北京-青島和武漢-青島,之前就是可以到達的,所以不能加造價爲5的結點。

最後,Kruskal的結果有4種~

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章