1.基本概念
无向图:(i,j)=(j,i)
有向图:<i,j>=Vi->Vj 规定Vi是tail,Vj是head
限制:不允许loop和multiple edges的出现
完全图:有最大边数量的图
路径长度:路径上边的数量
图的连通:
无向图:任意两地间有路径存在
有向图:两种连通类型
1.强连通:任意两点间有路径存在
2.弱连通:忽略边的方向后是连通的
图的分量:
无向图的(连通)分量:最大连通子图
有向图的强连通分量:最大的强连通子图
树:连通无环图( a graph that is connected and acyclic)
A DAG=a directed acyclic graph
2.图的表示
typedef int vertex
1.邻接矩阵
adj_m[i][j]=1 有<i,j>的边,以i为tail,j为head,边有权则为权值
T和S都是O(N^2)
无向图的邻接矩阵是对称的,因此我们可以通过优化仅存储一半的元素来节省空间
方法:通过一个大小为n*(n+1)/2的一维数组存储矩阵
数组中的元素为{a11,a21,a22,a31,a32,a33......an1,an2.....ann}
adj_m[i][j]对应数组中的下标为i*(i-1)/2+j
邻接矩阵中点的度计算:无向图 deg(i)=Σadj_m[i][j] (j=1,2...n)
有向图 deg(i)=Σadj_m[i][j]+Σadj_m[j][i] (j=1,2...n)
下面是邻接矩阵的代码实现
图的声明:
struct GraphRecord{
int vexnum;
int edgenum;
ElementType *vertices;
int **adjmat; //无权图1/0,有权图值为权
/*
一个*定义一维数组,指针指向一个元素全部为int类型的一维数组
两个*定义二维数组,但该仍指针指向一个一维数组,该数组中的元素全部为int*,即指向一个全部为指针类型的一维数组
*/
}
typedef struct GraphRecord *Graph;
图的创建/初始化
Graph CreateGraph(int N){
Graph p;
int i,j;
p=malloc(sizeof(struct GraphRecord));
p->vexnum=0;
p->edgemnum=0;
p->vertices=malloc(sizeof(ElementType)*(N+1));
p->adjmat=(int**)malloc(sizeof(int*)*(N+1));
for (i=0;i<=N;i++)
p->adjmat[i]=malloc(sizeof(int)*(N+1));
for (i=1;i<=N;i++)
for (j=1;j<=N;j++)
p->adjmat[i][j]=0;
return p;
}
注意里面三个malloc的使用
设置点的值
void SetData(ElementType e, Vertex v, Graph G){
G->vertices[v]=e;
//这个操作在我们本章的学习中基本用不到,我们只注重点边关系而非点的值
}
插入边
void InsertEdge(Vertex v, Vertex w, Graph G){
G->adjmat[v][w]=1;
G->adjmat[w][v]=1;
++G->edgenum;
}
注意边数加一
销毁图
void DestroyGraph(Graph G){
for (i=0;i<=G->vexnum;i++)
free(G->adjmat[i]);
free(G->adjmat);
free(G->veryices);
free(G);
}
2.邻接表
用链表代替邻接矩阵中的每一行
对于无向图来说:S=n个头节点+2e个节点=(n+2e)指针
邻接表中度的计算:
无向图中 deg(i)=该头节点连接的链表中node的个数
有向图中还要计算i的入度
使用图的邻接表进行拓扑排序:
先介绍一些概念
AOV Network(Activity On Vertex):同有向图中的边表示优先关系,有向图中的点表示事件/例程
e.g. C1--->C3 means that C1 is a prerequisite course of C3
可行的AOV网络必须是DAG(directed acyclic graph)
拓扑排序:拓扑顺序是一个图的顶点的线性顺序,对于任意两个顶点i, j,如果i是网络中j的前一个顶点,那么i在线性顺序中在j之前。拓扑排序的序列可能不唯一。
正常思路:首先得到入度数为0的点,将其存入数组中(或直接输出),然后将所有与其邻接的点的入度数减一,循环此过程n次即可形成拓扑序列。对于度数的处理是关键!
伪代码如下:
void Topsort(Graph G){
vertex v,w;
int i;
for (i=1;i<=G->vexnum;i++){ //O(|V|)
v=findnewvertexofindegreezero(); //O(|V|)
if(v==NotAVertex){
Error ( "Graph has a cycle" ); break;
}
topnum[i]=v;
for (each w adjacent from v)
degree[w]--;
}
}
该算法的时间复杂度为O(V^2),并不是很出色。算法复杂度表现的不很理想主要是由寻找入度为0的点时对所有点都(潜在地)进行了遍历,但每次度数改变的点,其实都是很少量的。
下面我们结合队列的结构改善该算法
结合队列的改善:
1.先将入度为0的点存入队列中,然后让其出队存入数组(或者输出),再将所有与其关联的点的入度减去1。
2.将1中入度减一后的点push进入队列,再重复1的过程直到队列为空(所有的点都输出了)
代码实现:
typedef int vertex
typedef struct Node *PtrToNode,*List
struct Node{
vertex adjvex;
PtrToNode next;
}
struct VertexRecord{
ElementType element;
List adjto;
}
struct GraphRecord{
int vexnum;
int edgenum;
struct VertexRecord *vertices;
}
typedef struct GraphRecord *Graph;
vertex* Topsort(Graph G){
Queue Q;
vertex v,w;
PtrToNode p;
int i,*topnum,counter=0;
topnum=malloc(sizeof(int)*(G->vexnum+1));
for (i=1;i<=G->vexnum;i++){
degree[i]=G->vertices[i].element; //见说明
if (degree[i]==0)
Enqueue(i,Q);
}
while(!IsEmpty(Q)){
v=Dequeue(Q);
topnum[++counter]=v;
p=G->vertices[v].adjto;
while(p!=NULL){
if(--degree[p->adjvex]==0)
Enqueue(p->adjvex,G);
p=p->next;
}
}
if (counter!=G->vexnum)
ERROR ("Graph has a cycle")
Disposequeue(Q);
return topnum;
}
队列相关的代码在这里就不写了,关于代码有三点需要说明:
1.这里我们不考虑点的数据类型,统一用连贯的整数表示,所有vertexrecord里面的element没什么用,提交程序时我直接把每个点的element初始化为0,在进行插入操作时用element记录点的入度
2.每次插入一个边<v,w>时,将w插入到与v相邻的第一个位置
3.结构体变量用.运算符来访问结构体成员
指向结构体指针用->来访问结构体成员
这学期指针用多了习惯性用->,结构编译时G->vertices[i]->adjto给我报错了,开始一直没想起来怎么回事...应该用G->vertices[i].adjto
可以看出,改善后代码的时间复杂度为O(|V|+|E|)
3.AOE Network
avtivity on edge network 用于安排工程
先熟悉一些概念
CP(critical path) 从开始点到结束点的最长路径就叫做关键路径(这里的最长指路径上持续时间和最大)
EC[j]:the earliest completion time for node j
LC[j]:the latest completion time for node j
关于AOE网路可以参考这篇博客,写的清晰易懂(图多哈哈哈
AOE网络-关键路径_大力海棠的博客-CSDN博客_aoe网络的关键路径
这里就不多说了
三.最短路径问题
这是图的重要应用,这里主要讨论单源最短路径问题,即从一个点到图中其他各点的最短路径。贪心算法和BFS在本问题中应用广泛。本章的编程练习中基本都需要打印出最短的路径,所以关于怎样储存路径也是需要掌握的一个技巧。
1.无权图(无向为例)
有向无向处理思路都一样,下面的代码按无向图处理
思想:BFS(breadth-first research)广搜。第一次遍历源点开始,遍历其相邻各点,并将每个点距离源点的dis置为1,并且标记为已经探索过,并记录该点的上一个点(即源点)。接着对与源点相邻的点进行遍历,进行上述过程,但dis和上一个点需要进行调整
需要的结构:图Graph(邻接表),表Table(遍历时保存记录每个点的信息),队列Queue
队列在这里作用是分开每次需要遍历的点和已经为遍历,有点动态编程的意思。当然结合邻接表和table也可以找出需要每次遍历的点,但就显得复杂了
Table[ i ].Dist = distance from s to vi /* initialized to be infinity except for s */
Table[ i ].Known = 1 if vi is checked; or 0 if not
Table[ i ].Path = for tracking the path /* initialized to be 0 */
伪代码如下
void unweighted(Table T){
InitializeTable(T);
Queue Q;
Q=CreateQueue(numvertex);
MakeEmpty(Q);
Vertex v,w;
Enqueue(S);
while(IsEmpty(Q)){
v=Dequeue(Q);
T[v].known=true; //注意这个算法源点开始也是未知的,到这里才置为known
for each w adjacent to v
if (T[w].dis==infinity)
T[w].dis=T[v].dis+1;
T[w].path=v;
Enqueue(w,Q);
}
DisposeQueue(Q);
}
说明:known域在这里并没有使用,因为一个顶点一旦被处理后就出队了,因此它不需要再被重新处理的事实就意味着被标记了
2.有权图(有向为例)
Dijkstra(迪杰斯特拉)算法
思想:贪心算法,在每个阶段选择距离源点最近且没有被探索过的点,并更新与之相邻点的距离。注意每次点被选择后置为known。所以如果是无环图的话,这些点可能会被按照拓扑排序的顺序选择,因为每当有一个点被选择时,它距离源点的最短路径就不会被未选择的点所影响了,换言之即不再处于考虑范围中了、
需要的结构:图(邻接表),表Table
伪代码如下
void Dijkstra(Table T){
InitializeTable(T);
vertex v,w;
for(;;){
v=FindsmallestUnknownVertex;
if (v==NotAVertex) break;
eles
T[v].known=true;
for w adjacent to v
if (!T[w].known&&T[w].dist>T[v].dist+Cvw)
T[w].dist=T[v].dist+Cvw;
T[w].path=v;
}
}
四.最小生成树(minimum spanning tree)
在寻找图的最小生成树时,我们同样使用了贪心算法
生成树:A spanning tree of a graph G is a tree which consists of V( G ) and a subset of E( G )
哈密顿通路
最小生成树:1.n-1条边
2.边的权值和最小
3.涵盖所有顶点(spanning)
4.图连通才有最小生成树
5.向一个生成树添加一个非树的边,我们得到环
算法1.Prim算法-grow a tree
类似Dijkstra算法,通过顶点选择边,直到所有顶点都为known
在算法的每一阶段我们都选择边(u,v)使得(u,v)的值是所有u在树上而v不在树上的权值最小的边
算法2.Kruskal算法-maintain a forest
思想:连续按照最小的权值选择边,并且当所选边不产生环时就把它作为选定的边,直到所选边数为n-1
该算法首先要对边排序,我们选择堆排序,因为它的deletemin操作可以很好的适配我们的需求(如果该边被抛弃即形成环时,以后我们都不再需要检验这条边了)
需要的结构:图,堆,不相交集(用于检验是否形成环)
伪代码如下:
void Kruskal(Graph G){
DisjSet S;
build min heap H from the edges of graph G;
int edgesccepted=0;
while(edgsaccepted<numvertex-1){
E=deletemin(H); //E=(U,V)
Uset=Find(U,S);
Vset=Find(V,S);
if(Uset!=Vset){
Union(Uset,Vset);
edgeaccepted++;
}
}
}