6.1 什么是图
图是描述多对多关系的结构,图包含顶点和边,顶点用V(Vertex)表示,边用E(Edge)表示,双向边用(v,w)圆括号括住的顶点对表示,单向边用<v,w>表示。
操作集:
Graph Create();
Graph InsertVertex(Graph G, Vertex V);
Graph InsertEdge(Graph G, Edge E);
void DFS(Graph G, Vertex V); //从V出发深度优先遍历图G
void BFS(Graph G, Vertex V); //从V出发广度优先遍历图G
void ShortestPath(Graph G, Vertex V, int Dist[]);//计算图G中顶点V到其他任意顶点的最短距离。
void MST(Graph G);//计算图G的最小生成树
怎么在程序中表示图:
邻接矩阵:
对于一个图有N个节点,节点从0到N-1进行编号,用一个二维数组存储G[i,j],如果第i个元素和第j个元素之间有边,那么就把G[i,j]的值设置为1,否则设置为零。
对于无向图(即两个节点之间的边没有方向,G[i,j]和G[j,i]等效),我们可以省略一半的存储空间,用一个(N*(N-1)/2)大小的一维数组来存储,要找i,j之间的边可以用(i*(i+1)/2+j)来索引。有向图不可省略。
邻接矩阵查找某个节点邻接的节点很方便,只需扫描第i行的元素是否为1即可,对于有权图,把数组值改为权值即可,对于有向图,ij表示从i到j,ji表示从j到i。
邻接矩阵存储稀疏图(节点很多边很少)会非常浪费空间,比较适合存稠密图。
邻接表:
稀疏图用邻接矩阵会浪费很多空间,那么我们可以用邻接表来存储稀疏图,邻接表是一个链表类型的数组,有N个节点就有N个元素,每个元素的值都是一个链表头,这个链表链接着这个元素对应节点的所有的边,无所谓顺序 ,一个接一个把边存下来,邻接表存稀疏矩阵比较好,但是终究不如邻接矩阵方便。
6.2 图的遍历
图的遍历有两种方式:DFS(深度优先搜索)和BFS(广度优先搜索)。
- DFS:就是从某一个节点开始,依次访问它的邻接点,每个邻接点都递归的调用DFS方法。
void DFS(Vertex V){
V.Visited = true;
//对于邻接表来说,访问V的每一个邻接点就是找到V对应的链表依次访问
//对于邻接矩阵来说,需要访问V对应的那一行里所有的非零(或无穷)项
for(V的每一个邻接点W){
if(W没有被访问过){
DFS(W);
}
}
}
- BFS:广度优先搜索,类似于树的层序遍历,用队列实现。
void BFS(Vertex V){
V.Visited = true;
Queue Q;
AddQ(Q,V);
while(!IsEmpty(Q)){
V = Delete(Q);
for(V的每一个邻接点W){
if(W没有被访问){
V.Visited = true;
AddQ(Q,W);
}
}
}
}
两种遍历方法各有优劣,不同的情况适用不同的方法。
有的时候图并不是联通的,这时候要遍历,需要把图中的每个没有被访问的节点都调用一次BFS或DFS,就可以把每个节点都访问到。(两种遍历方法是检索数据的方式,并不是说找不到这些数据了,而是按照某种特定方法来遍历,达到某些目的)。
7.1 最短路径问题
最短路径问题分为:单源最短路径和多源最短路径。
单源最短路径:
单源无权图的算法思想:从源点开始一圈一圈往外扩展,依次找到与源点距离为1的,与源点距离为2的节点,对广度优先搜索(BFS)稍作修改即可。
void Unweighted( Vertex S ){
int Dest[N] = {-1};
Vertex Path[];
EnQueue( S,Q );
Dest[S] = 0;
while( !IsEmpty Q ){
V = DelQueue(Q);
for(V的每一个邻接点W){
if( Dist[W] == -1){
//到W的距离等于到V的距离加1
Dist[W] = Dist[V]+1;
//到达W的最短路径必须经过V
Path[W] = V;
}
}
}
}
- 单源有权图的算法(dijkstra算法):有一个集合S,它里面收录了源点和已经找到最短路径的点,按照距离非递减的顺序依次把所有的点都收录到S里面。
//需要把Dist初始化为正无穷,Path初始化为-1
void Dijkstra(VerTex V){
Dest[V] = 0;
Collected[V] = true;
for(V的每一个邻接点W){
//E<V,W>bi表示V到W的距离,也就是权重。
Dest[W] = E<V,W>;
}
while(1){
V = 还未收录的节点的Dest最小的节点。
if(所有的节点都被收录)break;
Collected[V] = true;
for(V的每一个邻接点){
if( Collected[W] == false ){
if(Dist[V] + E<V,W> < Dist[W]){
Dist[W] = Dist[V] + E<V,W>;
Path[W] = V;
}
}
}
}
}
多源最短路径:
方法一:可以直接把单源最短路径的方法对每一个节点都调用一遍(对稀疏图效果好)。
方法二:Floyd算法
bool Floyd( MGraph Graph, WeightType D[][MaxVertexNum], Vertex path[][MaxVertexNum] )
{
Vertex i, j, k;
/* 初始化 */
for ( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ ) {
D[i][j] = Graph->G[i][j];
path[i][j] = -1;
}
for( k=0; k<Graph->Nv; k++ )
for( i=0; i<Graph->Nv; i++ )
for( j=0; j<Graph->Nv; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j];
if ( i==j && D[i][j]<0 ) /* 若发现负值圈 */
return false; /* 不能正确解决,返回错误标记 */
path[i][j] = k;
}
return true; /* 算法执行完毕,返回正确标记 */
}
8.1 最小生成树问题
最小生成树: 一个图构成最小生成树,图里面的每一个联通的节点都必须包含在生成树里面,生成树的边最少即(N-1)条,不构成回路,而且选出的边的权重和必须最小。
有两种算法:Prim算法和KrusKal算法
- Prim算法:从一个根节点开始,依次选出可以选的边,选择的边要是权重最小的,且不会构成回路。适用于边比节点多很多。
void prim(VerTex V){
//根节点的parent就是-1,非根节点的parent是它的父元素。
parent[V] = -1;
//根节点的dest为0,根节点的邻接点dest为边的距离,其余的dest都是无穷大。
dest[V] = 0;
for(V的每一个邻接点W){
dest[W] = E<V,W>;
}
while(1){
V = 全部节点中dest最小的非零节点;
if(没有这样的节点V)
break;
//将V收录进MST
dest[V] = 0;
for(V的每一个邻接点W){
//如果W没有被收录,即W的dest不为零
if(dest[W] != 0 && E<W,V> < dest[W]){
dest[W] = E<W,V>;
parent[W] = V;
}
}
}
if(MST中的节点不足|V|个)
Error("生成树不存在!")
}
- KrusKal算法:每次选择权重最小的,不会构成回路的边。适用于边和节点属于同一数量级的
void kruskal(VerTex V){
//MST初始为空。
MST = {};
while(MST中的边不到N-1条 && 边集E中还有边存在){
//最小堆
//个人思路:构造一个存储边的结构体,包含这条边两端的节点信息
从E中取出一条权重最小的边E<V,W>;
将E<V,W>从边集中删除;
//并查集检查选中边的两个节点是否在同一个集合中
//个人思路:将边加入MST中时,同时将边的两个节点标记为已经加入,判断是否构成回路就判断这条边两个节点是否都已经加入。
if(E<V,W>添加到MST中不会构成回路)
将E<V,W>加入MST中
}
if(MST中的边不到N-1条)
Error("生成树不存在");
}
8.1 拓扑排序
拓扑序: 如果图中从V到W有一条有向路径,则V一定排在W之前。满足此条件的顶点序列称为一个拓扑序。获得一个拓扑序的过程就是拓扑排序。AOV如果有合理的拓扑序,则必定是一个有向无环图(DAG)。
void topSort(){
for(图中的每一个顶点V)
if(Indegree[V] == 0)
InQueue(Q,V)
while(!IsEmpty(Q)){
V = Dequeue(Q);
输出V,或者记录V的输出序号。
for(V的每一个邻接点W)
if(--Indegree[W] == 0)
InQueue(Q,W)
}
if(输出的个数不足|V|个)
Error("图中有回路");
}