图算法笔记-数据结构钟宏老师

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++;
}
}
}

五.DFS的应

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值