第八講--圖以及最小生成樹
複習
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種~