通常图分为有向图和无向图。
顶点的度是指和该顶点相连的边的条数。特变的对于有向图,顶点的出边条数成为出度,顶点的蠕变条数成为入度。顶点和边均可以由一些属性,称为点权和边权。c++
图可使用两种存储方式:邻接矩阵和邻接表。算法
设图G(V,E)的顶点编号为0-N-1,那么能够令二维数组G[N][N]的两维分别表示图的顶点标号,及若是G[i][j]的值为1,表示i和j之间有变。这个二维矩阵被称为邻接矩阵。而且若是存在边权可让G[N][N]中存放边权。
无向图的邻接矩阵是对称矩阵。数组
设图G(V,E)的顶点标号为0,1,… ,N-1,每一个顶点都有可能有若干条出边,若是把同一个顶点的全部出边放在一个列表中,那么N个顶点就会有N个列表(没有出边,对应空表)。这N个列表被称为图的邻接表。记做Adj[N]。
邻接表实现可使用变长数组vector,开一个vector数组Adj[N], N是顶点个数,每一个顶点对应一个变长数组,存储其出边。
以下所示:
<Vector<int> Adj[N];>
若是节点还有权值,咱们能够定义一个结构体:函数
Struct Node{ Int v; Int w; };
而后vector邻接表中的元素类型就是Node型的。
<Vector<Node> Adj[N];>
测试
是指对图的全部顶点按必定顺序进行访问,遍历方法通常有两种:深度优先搜索(DFS)和广度优先搜索(BFS)。优化
DFS的具体实现,首先介绍两个概念:
连通份量。在无向图中,若是两个顶点之间能够相互到达(能够是经过必定路径间接到达),那么就称这两个顶点连通。若是图G(V,E)的任意两个顶点都连通,则通图G为连通图,不然称G为非连通图,且称其中的极大连通子图为连通份量。
强连通份量。在有向图中,若是俩ing个顶点能够各自经过一条有向路径到达另外一个顶点,就称这两个顶点强连通。若是图G(V,E)的任意两个顶点都强连通,则称图G为强连通图;不然称G为非强连通图,且称其中的极大强连通子图为强连通份量。
若是要遍历一个图就要对全部的连通块进行遍历,若是已知的图是连通图,则只须要一次DFS遍历就能够完成。
DFS的伪代码:(可使用临界矩阵和邻接表实现)spa
DFS(u){//访问顶点u vis[u] = true; //设置u为已访问 for(从u出发能到达的全部顶点v){ //枚举从u出发能够到达的全部顶点v if(vis[v] == false){ DFS(v); } } DFSTrave(G){ //遍历图 for(G 的全部顶点u) //对G的全部顶点u if vis[u] == false //若是u未被访问 DFS(u); //访问u所在的连通块 }
广度优先搜索以“广度”做为关键词,每次以扩散的方式向外访问顶点。和树的遍历同样,使用BFS遍历图须要使用一个队列,经过反复取出队首顶点,将该顶点可到达的不曾加入过队列的顶点所有入队,(而不是未被访问)直到队列为空时遍历结束。
能够查看下面的伪代码,根据思路可使用邻接表和临界矩阵进行实现。code
BFS(u){ //遍历u所在的连通块 queue q;//定义队列q 将u入队; inq[u] = true; while(q 非空){ 取出队首元素u进行访问; for(从u出发可达到的全部顶点v) if( inq[v] == false) { //若是v不曾加入过队列 将v入队; inq[v] = true; } } } BFSTrave(G){ for(G 的全部顶点u) if(inq[u] == false){ //若是u不曾加入过队列 BFS(u); //遍历u所在的连通块 } }
是在一个给定的无向图G(V,E)中求一棵树T,使这棵树拥有图G中的全部顶点,且全部边都来自图G中,而且知足整棵树的边权之和最小。
最小生成树有三个性质须要掌握:
1. 最小生成树是树,所以其边数等于顶点数减一,且树内必定不会有环。
2. 对给定的图G(V,E),其最小生成树能够不惟一,但其边权之和必定是惟一的。
3. 因为最小生成树是在无向图上生成的,所以其根节点能够是这棵树上的任意一个结点。通常为了输出惟一,会指定一个结点做为根节点。
经常使用的算法有:Prim(普利姆算法)和 Kruskal算法(克鲁斯卡尔算法)排序
伪代码以下,其时间复杂度为O(V^2),若是图用邻接表实现,可使用堆优化即便用优先级队列将复杂度下降为O(VlogV + E):队列
G为图,S是以及加入图中的顶点集,数组d为顶点与集合S的最短距离 Prim(G,d[]){ 初始化G[],d[],d[1] = 0; for(循环n次) { u = 使d[u]最小的还未被访问的顶点的标号; 记录u已被访问; for(从 u 出发能到达的全部顶点v){ if(v 未被访问 && 以u为中介点使得v与集合S的最短距离d[v]更优){ 将G[u][v]赋值给d[v]; } } } }
伪代码以下,其时间负责度主要在拍于函数上,是O(ElogE),其中E是图的边数。
int kruskal(){ 令最小生成树的边权之和为ans,最小生成树的当前边数Num_edge; 将全部边按照边权从小到达排序; for(从小到大枚举全部边) { if(当前测试边的两个端点在不一样的连通块中){ //判断是否在一个联通块中可使用并查集 将该测试边加入最小生成树; ans += 测试边的边权; 最小生成树的当前边数num_edge+1; 当前边数num_edge 等于定点数减一时结束循环; } } return ans; }
从上面的时间复杂度分析可知,Prim 算法的时间复杂度与V相关,适合稠密图(顶点少边多),而kruskal算法的实际复杂度与E的数目有关,适合稀疏图(顶点多,边少)。
参考内容:《算法笔记》 胡凡 曾磊主编