1.图的相关概念
图的定义:由一个顶点集V和一个边集组成,其中顶点集非空,通常用|V|来表示图中的顶点个数,|V|也叫做图的阶
有向图: E中的元素是有向边(或者称作弧),弧是顶点的有序对,用<v,w>表示,其中v、w是顶点,v称为弧尾,w称为弧头
无向图:E中的元素是无向边,用(v,w)表示
简单图:如果一个图G满足以下条件,那么他就是简单图:
不存在重复的边
不存在顶点到 自身的边
多重图:不是简单图的图
(简单)完全图:任意两个顶点之间都有边的简单无向图或者任意两个顶点之间都有两条方向相反的弧的 简单有向图
子图:取一个图G(V,E)中的若干顶点V'和若干条边E',这个G‘=(V',E')称为G的子图,如果 V'=V,那么这个子图叫做G的生成子图
连通:在无向图中,如果从v到w有路径存在,则称w和v是连通的
连通图:如果无向图图中任意两个顶点都是连通的,则称此图为连通图,否则称为非连通图
极大连通子图/连通分量:无向图中的极大连通子图称为连通分量,要求连通顶点的边最大
极小连通子图:要求连通子图中顶点的边数最少
强连通图:在有向图中,如果任意两个顶点之间都有路径,那么称这个图为强连通图
强连通分量:有向图中的极大强连通子图称为有向图的强连通分量
生成树:包含图中全部顶点的一个极小连通子图
生成森林:一个图中的连通分量构成了生成森林
顶点的度:以该顶点为端点的边的数目
出度:有向图中以该顶点为弧尾的边的条数
入读:在有向图中以该顶点为弧头的边的条数
边的权:图中每条边可以赋予一个有含义的数值,这个数值称为这条边的权值,边上带有权值的图称为网
稠密图:边多的图
稀疏图:边少的图,稠密图和稀疏图都只是相对的概念
路径:v到w的路径指的是从v到w的路径上的顶点序列
简单路径:没有顶点重复的路径称为简单路径
简单回路:除了首位定点外,没有顶点重复的路径称为简单回路
距离:路径的带权长度(无权图则每条边权值看作1)
有向树:一个顶点入读为0,其余顶点入度都是1的有向图
2.图的存储
邻接矩阵法:
/**@数据结构:(有向带权)图 **@作者:9761滴 **@存储结构:邻接矩阵(Adjacent Matrix) **/ //本文件中实现了图的 /* 1.Adjacent(G,x,y) //判断图G是否存在边(x,y)或<x,y> 2.Neighbors(G,x) //列出图G中与顶点x邻接的边(无向图) 3.InsertVertex(G,x) //在图G中插入顶点x 4.DeleteVertex(G,x) //从图G中删除顶点x 5.AddEdge(G,x,y) //若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边 6.RemoveEdge(G,x,y) //若无向边(x,y)或有向边<x,y>存在,则向图G中删除该边 7.FirstNeighbor(G,x) //求图G中顶点x的第一个邻接点,若有则返回顶点号,若无或者不存在顶点x则返回-1 8.NextNeighbor(G,x,y) //若y是x的一个邻接点,返回除y外顶点x的下一个邻接点的顶点号, //若y是最后一个顶点,则返回-1 9.Get_edge_value(G,x,y) //获取图G中边(x,y)或者<x,y>对应的权值 10.Set_edge_value(G,x,y,v) //将图G中边(x,y)或者<x,y>对应的权值设置为v *等基本操作/ //以及 /* 1.BFSTraverse O(|V|^2) 2.DFSTraverse O(|V|^2) */ #include<cstdio> #include<limits.h> #include<vector> #include<queue> #define Max_Vertex 100 using namespace std; typedef int ElementType; typedef char VertexType; typedef int EdgeType; typedef struct { VertexType vertex[Max_Vertex]; EdgeType edge[Max_Vertex][Max_Vertex]; int vexnum; //顶点的数量 int arcnum; //边的数量 }MGraph; bool Adjacent(MGraph G,VertexType x,VertexType y){ if(G.edge[x][y]==0||G.edge[x][y]==INT_MAX) return false; else return true; } vector<VertexType> Neighbors(MGraph G,VertexType x){ vector<VertexType> v; for(EdgeType i=0;i<G.vexnum;i++){ if(G.edge[x][i]!=INT_MAX&&G.edge[x][i]!=0) v.push_back(G.vertex[i]); } return v; } bool InsertVertex(MGraph G,VertexType x){ if(G.vexnum==Max_Vertex) return false; G.vertex[G.vexnum++]=x; //从0存起,所以后++ for(EdgeType i=0;i<G.vexnum;i++){ if(i==G.vexnum-1) G.edge[G.vexnum-1][i]=0; else G.edge[G.vexnum-1][i]=INT_MAX; } return true; } //bool DeleteVertex(MGraph G,VertexType x){ // for(int i=0;i<G.vexnum;i++){ // if(G.vertex[i]==x){ // // G.vertex[i]=G.vertex[G.vexnum-1]; // for(int j=0;i<G.vexnum;i++){ // if(G.vertex[j]!=x){ // G.edge[i] // } // } // } // } //} int FirstNeighbor(MGraph G,int x){ if(x>=G.vexnum) return -1; for(int j=0;j<G.vexnum;j++){ if(G.edge[x][j]!=0&&G.edge[x][j]!=INT_MAX) return j; } return -1; } int NextNeighbor(MGraph G,int i,int j){ if(i>=G.vexnum||j>=G.vexnum) return -1; for(int k=j+1;k<G.vexnum;k++){ if(G.edge[i][k]!=0&&G.edge[i][k]!=INT_MAX) return k; } return -1; } bool visited[Max_Vertex]; void visit(VertexType V){ printf("%c ",V); } void BFS(MGraph G,int v){ visit(G.vertex[v]); visited[v]=true; queue<int> q; q.push(v); while(!q.empty()){ v=q.front(); q.pop(); for(int i=FirstNeighbor(G,v);i!=-1;i=NextNeighbor(G,v,i)){ if(visited[i]==false){ // printf("aa%d",i); visit(G.vertex[i]); visited[i]=true; q.push(i); } } printf(" "); } } void BFSTraverse(MGraph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(visited[i]==false){ // printf("一次"); BFS(G,i); printf(" "); } } } void DFS(MGraph G,int v){ visit(G.vertex[v]); visited[v]=true; for(int i=FirstNeighbor(G,v);i!=-1;i=NextNeighbor(G,v,i)){ if(visited[i]==false){ DFS(G,i); } } } void DFSTraverse(MGraph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(visited[i]==false){ DFS(G,i); } } }
邻接表法:
/**@数据结构:(有向带权)图 **@作者:9761滴 **@存储结构:邻接表(Adjacent List) **/ //本文件中实现了图的 /* 1.Adjacent(G,x,y) //判断图G是否存在边(x,y)或<x,y> 2.Neighbors(G,x) //列出图G中与顶点x邻接的边(无向图) 3.InsertVertex(G,x) //在图G中插入顶点x 4.DeleteVertex(G,x) //从图G中删除顶点x 5.AddEdge(G,x,y) //若无向边(x,y)或有向边<x,y>不存在,则向图G中添加该边 6.RemoveEdge(G,x,y) //若无向边(x,y)或有向边<x,y>存在,则向图G中删除该边 7.FirstNeighbor(G,x) //求图G中顶点x的第一个邻接点,若有则返回顶点号,若无或者不存在顶点x则返回-1 8.NextNeighbor(G,x,y) //若y是x的一个邻接点,返回除y外顶点x的下一个邻接点的顶点号, //若y是最后一个顶点,则返回-1 9.Get_edge_value(G,x,y) //获取图G中边(x,y)或者<x,y>对应的权值 10.Set_edge_value(G,x,y,v) //将图G中边(x,y)或者<x,y>对应的权值设置为v *等基本操作/ //以及 /* 1.BFSTraverse O(|V|+|E|) 2.DFSTraverse O(|V|+|E|) */ #include<cstdio> #include<cstdlib> #include<queue> using namespace std; #define MAX_SIZE 100 typedef char VertexType; typedef int EdgeType; typedef struct ArcNode{ int adjvex; ArcNode* next; }ArcNode; typedef struct node{ VertexType data; ArcNode* first; }node; typedef struct ALGraph{ node vertex[MAX_SIZE]; int arcnum; int vexnum; }ALGraph; ArcNode* FirstNeighbor(ALGraph G,int x){ if(x>=G.vexnum) return NULL; return G.vertex[x].first; } ArcNode* NextNeighbor(ALGraph G,ArcNode* x){ return x->next; } bool visited[MAX_SIZE]; void visit(node v){ printf("%d ",v.data); } void BFS(ALGraph G,int v){ visit(G.vertex[v]); visited[v]=true; queue<int> Q; Q.push(v); while(!Q.empty()){ v=Q.front(); Q.pop(); ArcNode *first=FirstNeighbor(G,v); for(;first!=NULL;first=first->next){ if(visited[first->adjvex]==false){ visit(G.vertex[first->adjvex]); visited[first->adjvex]=true; Q.push(first->adjvex); } } } } void BFSTraverse(ALGraph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(!visited[i]) BFS(G,i); } } void DFS(ALGraph G,int v){ visit(G.vertex[v]); visited[v]=true; for(ArcNode* first=FirstNeighbor(G,v);first!=NULL;first=first->next){ if(visited[first->adjvex]==false) DFS(G,first->adjvex); } } void DFSTraverse(ALGraph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(!visited[i]) DFS(G,i); } }
十字链表法:用于存储有向图
十字链表法存储图需要两种类型的结点:弧结点和顶点结点
弧结点:有五个域,尾域(tailvex)指向弧尾结点、头域(headvex)指向弧头结点、链域(hlink)指向弧头相同的下一条弧,链域(tlink)指向弧尾相同的下一条弧、info域指向该弧的权值或相关信息。
顶点结点:有三个域,data域存放顶点相关的数据信息,如顶点名词,firstin和firstout两个域分别指向以该顶点为弧头和弧尾的第一个弧结点
邻接多重表法:用于存储无向图
邻接多重表同样需要两种类型的结点:边结点和顶点结点
边结点:
mark域:标志该边是否被访问过
ivex、jvex域:指向这条边两端的顶点
ilink、jlink域:分别指向下一条依附于ivex、jvex的边
info:存储和边相关的信息
顶点结点:
data域:存数据
firstedge:指向第一条依附于该边的顶点
3.图的遍历
图的遍历分为深度优先和广度优先两种,具体的代码实现会和图的存储结构相关,但是算法的思路是一样的,算法请参考上述图的存储结构中的代码
与树的遍历不同,因为图中某个顶点可能有多条边,从而到达这个顶点的路径不止一条,而树中从根节点出发到每个结点的路径都只有一条,因此在图中需要设置一个visited数组,记录每个结点是否被访问过,避免从不同路径重复访问同一个结点。
其次需要注意的一点是,有些图是非连通图,它有多个极大连通子图(任意一个图都可以分解为一个或者几个极大连通子图),因此在非连通图中,不可能像在树中一样通过一个结点就可以遍历所有结点了,这时候在访问玩一个极大连通子图之和,要选取其他极大连通子图中还没有被访问过的结点,继续遍历,直到所有结点都被访问为止。
广度优先遍历(BFS):
广度优先遍历算法类似于树的层序遍历,从一个结点出发,一层一层的往外扩散。操作如下:
1.首先选取一个结点v,访问它,置visited[v]为true,表明已经访问过,然后将v加入到一个队列中。
2.从队列中弹出队头元素w,访问所有邻接w的但是没有被访问过的结点,同时访问完后将这些结点在visited数组中置为true,并加入队列
3.重复过程二,直到队列为空位置。
4.检测visited数组中所有结点是否已经访问完,如果所有结点都为true,则结束。如果有结点仍未false,说明还有结点未被访问(此图是一个非连通图),选取未访问的结点,重复步骤1 2 3.
深度优先遍历(DFS):
深度优先遍历序列类似于树的先序遍历,从某个结点出发,沿着一条路径依次访问路径上的未被访问过的结点,走到头,然后返回最后一个结点的上一个结点w,从w中选取一条新的路径,如果w没有新的路径了,继续返回w的上一个结点寻找新的路径,重复这个过程,直到所有结点都被访问。该算法通常用递归实现,不好语言描述,请直接参考图的存储结构中的dfs代码实现。
图的遍历的时间复杂度:不管是DFS还是BFS,若采用邻接矩阵存储,则遍历的时间复杂度为O(V^2),若采用邻接表存储,则时间复杂度为(O(V+E))。
4.最小生成树
生成树的定义请往上翻,参考图的相关术语
对于一个连通图来说,总能生成一棵树,而对于一个非连通图来说,能生成一个生成森林。
最小生成树:在一个连通的带权图中,有多个生成树,其中生成树的所有边的权值之和最小的生成树叫做最小生成树
在连通的带权图中,最小生成树可能不唯一,但是最小生成树的权值之和是一样的,如果图中每一条边的权值都不相等,那么最小生成树唯一
构造最小生成树的算法主要有两种,Prim算法和Kruskal算法
Prim算法步骤:
初始化:首先初始化一个空树T,往T中添加一个顶点u
每次从图中选取一个与T相连的权值最小的边,重复这个过程,直到所有顶点都被加入到T中
代码实现如下:(前方高能,看不懂就算了 ,这玩意是有点难)
/* **@Prim算法求最小生成树 **@作者:9761滴 */ #include<cstdio> #include<limits.h> #include<vector> #include<queue> #include<limits.h> #include<stdlib.h> #define MAX_SIZE 100 #define INFINITY INT_MAX using namespace std; typedef int ElementType; typedef char VertexType; typedef int EdgeType; //图-》邻接表 typedef struct ArcNode{ int adjvex; ArcNode* next; }ArcNode; typedef struct node{ VertexType data; ArcNode* first; }node; typedef struct ALGraph{ node vertex[MAX_SIZE]; int arcnum; int vexnum; }ALGraph; //图-》邻接矩阵 typedef struct { VertexType vertex[MAX_SIZE]; EdgeType edge[MAX_SIZE][MAX_SIZE]; int vexnum; //顶点的数量 int arcnum; //边的数量 }MGraph; //边 typedef struct { int v1; int v2; int weight; }Edge; void AddEdge(ALGraph &G,Edge E){ int i,j; for(i=0;i<G.vexnum&&i!=E.v1;i++); for(i=0;i<G.vexnum&&i!=E.v2;i++); ArcNode* first=G.vertex[i].first; if(first==NULL){ first=(ArcNode*)malloc(sizeof(ArcNode)); first->adjvex=E.v2; first->next=NULL; G.vertex[i].first=first; } else{ while(first->next!=NULL){ first=first->next; } ArcNode* temp=(ArcNode*)malloc(sizeof(ArcNode)); temp->adjvex=E.v2; temp->next=NULL; first->next=temp; } G.arcnum++; ArcNode* first2=G.vertex[j].first; if(first2==NULL){ first2=(ArcNode*)malloc(sizeof(ArcNode*)); first2->adjvex=E.v1; first2->next=NULL; G.vertex[i].first=first2; } else{ while(first2->next!=NULL){ first2=first2->next; } ArcNode* temp=(ArcNode*)malloc(sizeof(ArcNode*)); temp->adjvex=E.v1; temp->next=NULL; first2->next=temp; } G.arcnum++; } int findMinDist(MGraph G,int dist[]){ int v; int minDist=INFINITY; for(int i=0;i<G.vexnum;i++){ if(dist[i]!=0&&dist[i]<minDist){ v=i; minDist=dist[i]; } } if(minDist!=INFINITY) return v; else return -1; } //返回权值之和 int Prim(MGraph G){ int totalWeight; int dist[MAX_SIZE],parent[MAX_SIZE]; Edge E; ALGraph MST; int v; int VCount; /* 初始化。默认初始点下标是0 */ for(int v=0;v<G.vexnum;v++){ /* 这里假设若V到W没有直接的边,则Graph->G[V][W]定义为INFINITY */ dist[v]=G.edge[0][v]; parent[v]=0; } /* 最小生成树的初始化 */ MST.vexnum=G.vexnum; MST.arcnum=0; for(int v=0;v<MST.vexnum;v++){ MST.vertex[v].data=v; MST.vertex[v].first=NULL; } /* 其他初始化 */ totalWeight=0; VCount=0; dist[0]=0; VCount++; parent[0]=-1; while(1){ v=findMinDist(G,dist); if(v==-1) break; dist[v]=0; VCount++; E.v1=parent[v]; E.v2=v; E.weight=dist[v]; totalWeight+=dist[v]; AddEdge(MST,E); for(int i=0;i<G.vexnum;i++){ if(G.edge[v][i]<dist[i]&&G.edge[v][i]<INFINITY&&dist[i]!=0){ dist[i]=G.edge[v][i]; parent[i]=v; } } } if(VCount<MST.vexnum){ printf("该图不连通"); return -1; } else return totalWeight; }
Kruskal算法步骤:
初始化:将每个顶点都视作一个独立的树,树中只包含一个结点即根结点,将图中的边按照边的权值递增的次序排列,将这个排列记作E
循环:从E中每次选取最小的边,如果加入到树中会构成回路,则这条边不满足条件,选取下一个比他大的边检测是否构成回路,重复这个过程直到树中包含n-1条边。
算法代码实现如下:(前方同样高能,看不懂就算了,是挺难的)
/* **@Kruskal算法求最小生成树 **@作者:9761滴 */ #include<cstdio> #include<cstdlib> #include<queue> #include<limits.h> using namespace std; #define MAX_SIZE 100 typedef char VertexType; typedef int EdgeType; typedef struct ArcNode{ int adjvex; int weight; ArcNode* next; }ArcNode; typedef struct node{ VertexType data; ArcNode* first; }node; typedef struct ALGraph{ node vertex[MAX_SIZE]; int arcnum; int vexnum; }ALGraph; typedef struct Edge{ int v1; int v2; int weight; }Edge; int findFather(int S[],int a){ int x=a; while(S[a]>0){ a=S[a]; } while(x!=a){ int temp=x; x=S[x]; S[temp]=a; } return a; } bool CheckCycle(int S[],int a,int b){ int root1=findFather(S,a); int root2=findFather(S,b); if(root1!=root2){ if(S[root1]<S[root2]){ S[root1]+=S[root2]; S[root2]=root1; } else{ S[root2]+=S[root1]; S[root1]=root2; } return false; } else return true; } void init(int VSet[],int n){ for(int i=0;i<n;i++){ VSet[i]=-1; } } void PercDown(Edge Edges[],int parent,int n){ int child; Edge temp; temp.weight=Edges[parent].weight; temp.v1=Edges[parent].v1; temp.v2=Edges[parent].v2; for(;parent<n;parent=child){ child=parent*2; if(child!=n&&Edges[child].weight>Edges[child+1].weight){ child++; } if(temp.weight<=Edges[child].weight) break; else{ Edges[parent].v1=Edges[child].v1; Edges[parent].v2=Edges[child].v2; Edges[parent].weight=Edges[child].weight; } } Edges[parent].v1=temp.v1; Edges[parent].v2=temp.v2; Edges[parent].weight=temp.weight; } void CreateEdgeHeap(ALGraph G,Edge EdgeSet[]){ int count=1; for(int i=0;i<G.vexnum;i++){ for(ArcNode* j=G.vertex[i].first;j!=NULL;j=j->next){ if(i<j->adjvex){ EdgeSet[count].v1=i; EdgeSet[count].v2=j->adjvex; EdgeSet[count++].weight=j->weight; } } } for(int i=G.arcnum/2;i>=0;i--){ PercDown(EdgeSet,i,G.vexnum); } } void swap(Edge *a,Edge *b){ Edge *temp=b; b=a; a=temp; } int getEdge(Edge H[],int N){ swap(&H[0],&H[N-1]); PercDown(H,0,N-1); return N-1; } void AddEdge(ALGraph* &G,Edge E){ int i,j; for(i=0;i<G->vexnum&&i!=E.v1;i++); for(i=0;i<G->vexnum&&i!=E.v2;i++); ArcNode* first=G->vertex[i].first; if(first==NULL){ first=(ArcNode*)malloc(sizeof(ArcNode)); first->adjvex=E.v2; first->next=NULL; G->vertex[i].first=first; } else{ while(first->next!=NULL){ first=first->next; } ArcNode* temp=(ArcNode*)malloc(sizeof(ArcNode)); temp->adjvex=E.v2; temp->next=NULL; first->next=temp; } G->arcnum++; ArcNode* first2=G->vertex[j].first; if(first2==NULL){ first2=(ArcNode*)malloc(sizeof(ArcNode*)); first2->adjvex=E.v1; first2->next=NULL; G->vertex[i].first=first2; } else{ while(first2->next!=NULL){ first2=first2->next; } ArcNode* temp=(ArcNode*)malloc(sizeof(ArcNode*)); temp->adjvex=E.v1; temp->next=NULL; first2->next=temp; } G->arcnum++; } ALGraph* CreateGraph(ALGraph graph,int Vnum){ ALGraph* G; G->vexnum=Vnum; G->arcnum=0; for(int i=0;i<Vnum;i++){ G->vertex[i].first=NULL; G->vertex[i].data=graph.vertex[i].data; } return G; } int Kruskal(ALGraph G){ int ECount=0; int NextEdge=G.arcnum; int totalWeight=0; int VSet[G.vexnum]; init(VSet,G.vexnum); ALGraph* MST=CreateGraph(G,G.vexnum); Edge EdgeHeap[G.arcnum]; CreateEdgeHeap(G,EdgeHeap); while(ECount<G.arcnum-1){ NextEdge=getEdge(EdgeHeap,NextEdge); if(NextEdge<0) break; if(CheckCycle(VSet,EdgeHeap[NextEdge].v1,EdgeHeap[NextEdge].v2)==false){ AddEdge(MST,EdgeHeap[NextEdge]); ECount++; totalWeight+=EdgeHeap[NextEdge].weight; } } if(ECount<G.arcnum-1){ printf("非连通图"); return -1; } else return totalWeight; }
5.最短路径
最短路径问题即寻求图中结点之间的最短的路径,一般分为两类,单源最短路径问题和多源最短路径问题。
单源最短路径问题:即求解图中某一个顶点到其他顶点的最短路径,对于无权图,可以使用广度优先算法,对于有权图,使用Dijkstra算法。
无权图的单源最短路径:求无权图中某一个结点v到其他顶点的最短路径,可以用广度优先算法的思想 ,在广度优先算法中,从某一个顶点出发,一层一层的遍历图中的顶点,可以把无权图中的边的权值都看作是1,那么从v出发开始遍历这个图的时候,一个结点位于第几层,那么它到v的距离就是几。
算法实现如下:
#include<cstdio> #include<queue> #include<vector> using namespace std; vector<int> G[10]; int dist[10]; //dist[i]存储i到S的最短距离 int path[10]; void Unweighted(int S){ for(int i=0;i<10;i++){ dist[i]=-1; path[i]=-1; } queue<int> q; q.push(S); dist[S]=0; while(!q.empty()){ int v=q.front(); q.pop(); for(vector<int>::iterator it=G[v].begin();it!=G[v].end();it++){ if(dist[*it]==-1){ dist[*it]=dist[v]+1; path[*it]=v; q.push(*it); } } } }
带权图的单源最短路径:Dijkstra算法用于求解带权图中的最短路径,但是该算法无法解决带有负权值的带权图的最短路径问题。接下来介绍下这个算法:
首先设置一个顶点集合S(我们要求所有结点到结点v最短距离),S中记录已经求得了最短路径的顶点。在初始的时候,我们只把v放入S,并令辅助数组dist[v]=0
dist[]辅助数组:记录每个顶点当前求得的到v的最短距离
path[]数组:path[i]记录从v到i的路径上,i的前一个结点。
步骤:
1.初始化:集合S初始化只包含v,dist的数组中的值dist[i]初始化为 v 到 i 的边的权值,如果 v 和 i 之间不存在边,那么dist[i]=无穷大,如果dist[i]不等于无穷大,令path[i]=v
2.从集合V-S中(V是图的顶点集)选取出dist值最小的结点w,将它加入到S中
3.修改dist数组,因为新加入了结点w,所以有可能存在 v 到 i 的距离小于 v 到 w 的距离+ w到 i 的距离,即加入w结点到集合后,其他结点的最短距离就发生了变化,如果dist[i] 被修改,那么同时修改path[i]=w,表示 v 到 i 的当前最短路径上,i 的前一个结点是w.
4.重复步骤2、3,一共操作n-1次,直到所有顶点都被包含在了S中。
算法代码实现如下:
#include<cstdio> #define INFINITY 99999 #define VertexNum 10 #define Error 99999 int G[VertexNum][VertexNum]; //存储图的边,如果ab两个顶点之间不存在边,则他们边定义为INFINITY int dist[VertexNum]; bool collected[VertexNum]; int path[VertexNum]; int findMinDist(){ int MinV,MinDist; MinV=Error; MinDist=INFINITY; for(int v=0;v<VertexNum;v++){ if(collected[v]==false&&dist[v]<MinDist){ MinDist=dist[v]; MinV=v; } } return MinV; } bool dijkstra(int s){ //源点为S for(int v=0;v<VertexNum;v++){ dist[v]=G[s][v]; if(G[s][v]<INFINITY){ path[v]=s; } else path[v]=-1; //初始化path为-1,后面随着不断的执行,最后只剩下源点path[s]为-1,寻找路径的时候到-1停止,但是如果是一个非连通图, //还要看dist是不是INFINTY,因为不可达的顶点的path也是-1. collected[v]=false; } collected[s]=true; dist[s]=0; while(1){ int v=findMinDist(); if(v==Error) break; collected[v]=true; for(int w=0;w<VertexNum;w++){ if(collected[w]==false&&G[v][w]<INFINITY){ if(G[v][w]<0) return false; //dijkstra算法无法解决带负权值的图的单源最短路径问题 if(G[v][w]+dist[v]<G[s][w]){ dist[w]=G[v][w]+dist[v]; path[w]=v; } } } } return true; }
Dijkstra算法时间复杂度:O(V^2),因为Dijkstra算法的时间复杂度来源于加入N次顶点到集合S中,并且每次都要到长度为N的dist数组中去寻找dist值最小的顶点,所以不管是邻接表存储还是邻接矩阵存储,Dijkstra算法的时间复杂度都是O(V^2).
多源最短路径问题:即求解图中任意两个顶点之间的最短距离,求解这类问题使用Floyed算法。
算法思路:
将图中的所有顶点对之间的距离用一个矩阵表示,记作A-1,按照顶点的序号,从0号顶点开始,加入顶点0(即允许路径通过中间顶点0),得到一个新的距离矩阵A0,新矩阵A0[i][j]表示,i和j之间的可以经过顶点0的最短路径,显然A0[i][j]的值只能是 min{E[i][j],E[i][0]+E[0][j]},以A-1为基,更新每个A[i][j]的值便得到了A0
此时我们得到了一个可以通过顶点0的最短距离矩阵A0,此时我们以A0为基,允许路径通过中间顶点(0,1),更新此时的最短路径,得到一个新的最短距离矩阵A1,显然A1[i][j]=min{A0[i][j],A0[i][0]+A0[0][j]}。重复这个过程,没加入一个新的顶点,就更新依次A,最终待所有顶点都加入之和,得到的矩阵A就是最终的矩阵,此时A[i][j]表示的就是这个图中 i 和 j 之间的最短距离。
算法代码实现如下:
#include<cstdio> #define MAX_SIZE 10 int G[MAX_SIZE][MAX_SIZE]; int path[MAX_SIZE][MAX_SIZE]; void Floyd(){ for(int i=0;i<MAX_SIZE;i++){ for(int j=0;j<MAX_SIZE;j++){ for(int k=0;k<MAX_SIZE;k++){ if(G[j][k]>G[j][i]+G[i][k]){ G[j][k]=G[j][i]+G[i][k]; path[j][k]=i; } } } } } int main(){ return 0; }
Folyed算法时间复杂度:三重for循环,很明显O(N^3)
6.DAG图描述算术表达式
DAG图:有向无环图 (Direct acyclic graph),有向的,没有环的图,考虑树可以描述表达式,同样这样的图也可以描述表达式,事实上它比用树来描述表达式更简洁,因为树中一个结点(相当于表达式中的一个变量或者运算符)只能有一个父节点,因此它不能省略重复的结点,而图中的每个顶点的边都可以有多条,因而DAG图描述表达式会更简介
树来描述表达式
DAG图描述同样的表达式
将一个树描述的表达式改为DAG图描述的表达式的步骤:
(此方法出自王道咸鱼学长)
6.拓扑排序
拓扑排序是DAG图的另一种应用,先了解几个概念:
AOV网:用DAG图表示一个工程,图中的顶点表示活动,图中的有向边表示活动之间的先后关系,即<V,W>表示活动V必须在W之前进行,这种有向图称为顶点表示活动的网络(Activity On Vertex Network)
拓扑排序:一个DAG图的顶点组成的满足以下条件的序列叫做一个拓扑排序:
1.每个顶点只出现一次
2.若顶点A在序列中排在顶点B前面,则在图中不存在从顶点B到顶点A的路径
拓扑排序算法步骤:
1.从AOV网中选取一个没有前驱的顶点并输出
2.从网中删除该顶点和所有以它为起点的有向边
3.重复1和2,直到当前AOV网为空,或者当前网中不存在无前驱的顶点为止,后者说明图中有环,不是DAG图
拓扑排序算法实现:
#include<cstdio> #include<queue> using namespace std; #define MAX_SIZE 16 int G[MAX_SIZE][MAX_SIZE]={0}; bool TopologicalSort(int topOrder[]){ //如果这是一个有向无环图,则返回true,否则返回false int cnt=0; queue<int> q; int indegree[MAX_SIZE]={0}; //初始化各个顶点的入度 for(int i=1;i<MAX_SIZE;i++){ for(int j=1;j<MAX_SIZE;j++){ if(G[j][i]!=0) indegree[i]++; } } indegree[0]=1; for(int i=1;i<MAX_SIZE;i++){ if(indegree[i]==0) //indegree数组保存每个顶点的入度,对于逆拓扑排序,实现方式类似,只是这里会用一个outdegree数组来保存每个顶点的出度 q.push(i); } while(!q.empty()){ int v=q.front(); q.pop(); topOrder[cnt++]=v; printf("%d ",v); for(int i=1;i<MAX_SIZE;i++){ if(G[v][i]!=0){ indegree[i]--; if(indegree[i]==0) q.push(i); } } } if(cnt==MAX_SIZE-1) return true; else return false; }
拓扑排序的时间复杂度:邻接表存储的时候O(V+E),邻接矩阵存储的时候O(V^2).
DFS算法实现逆拓扑排序:(邻接矩阵实现)
void DFS(MGraph G,int v){ visit(G.vertex[v]); visited[v]=true; for(int i=FirstNeighbor(G,v);i!=-1;i=NextNeighbor(G,v,i)){ if(visited[i]==false){ DFS(G,i); } } printf("%d",v); } void DFSTraverse(MGraph G){ for(int i=0;i<G.vexnum;i++){ visited[i]=false; } for(int i=0;i<G.vexnum;i++){ if(visited[i]==false){ DFS(G,i); } } }
7.关键路径
在DAG中,以顶点表示事件,以有向边表示活动(注意区分事件和活动),以边上的权值表示完成活动的开销(如完成活动所需时间),称之为AOE网(Activity On Edge,用边表示活动的网络)
在AOE网中,只有某顶点所代表的事件发生后,从该顶点出发的个有向边所代表的活动才能开始
只有在进入某顶点的各有向边所代表的活动都结束时,该顶点所代表的事件才能发生。
在AOE网中,仅有一个入度为0的顶点,称为开始顶点(源点),表示整个工程的开始;网中也仅存在一个出度为0的顶点,称为结束点(汇点),表示整个工程的结束
关键路径:在一个AOE网中,从源点到汇点的最长路径(如果把边上的权值看作完成一个活动的时间,那么这个最长路径是完成整个工程必要的时间,是完成整个工程的最短时间)。
再搞清楚几个概念:
ve(k):事件k的最早发生时间,计算方法:
ve(源点)=0 ; ve(k)=max{ve(j)+weight(j,k)} : ve(k)等于所有k的前驱结点中,(不妨设这个满足条件的结点为 j ) j 的最早发生时间+从 j 到 k的路径活动时间中 最大的那个
注意计算的时间要按照拓扑排序的顺序计算
vl(k):事件k的最迟发生时间,计算方法:
vl(汇点)=ve(汇点) ;vl(k)=Min{vl (j) -weight(k,j)} : k为 j 的前驱,即 k 的最迟发生时间是k的所有后继结点 j - 从k到j的路径活动时间中 最小的那个
注意计算vl的时候,按照逆拓扑排序的顺序计算
e(i):活动 ai的最早开始时间:表示活动弧 ai 的起点所表示的时间的最早发生时间,若<k,j>表示弧,那么这条弧的最早开始时间e<k,j>=ve(k)
l(i) :活动 ai的最迟开始时间:活动弧ai 的终点所表示的时间的最迟发生时间与该活动所需时间之差,若<k,j>表示弧,则 l <k,j>=vl (j) -weight(k,j).
一个活动ai的最迟开始时间 l (i)和其最早开始时间e(i)的差额 d(i)=l(i)-e(i) : 指的是该活动完成的时间余量,即再不增加整个工程的情况下,该活动 ai可以拖延的时间,若一个活动的时间余量为0,则说明该活动必须如期完成,否则会拖延整个工程。
求关键路径的步骤:
1.从源点出发,令ve(源点)=0,按照拓扑排序有序求其余顶点的最早发生时间ve
2.从汇点出发,令vl(汇点)=ve(汇点),按逆拓扑排序求其余顶点的最迟发生时间
3.根据个顶点的ve值求所有弧的最早开始时间 e
4.根据个顶点的vl值求所有弧长的最晚开始时间 l
5.根据每条弧的e 和 l 求每个活动的活动时间余量 d,找出所有d=0的活动构成关键路径。