第八章 图

129
第第第 第 第第第 第第第第第第第第

description

第八章 图. 赵建华 南京大学计算机系. 内容. 图的基本概念 图的存储表示 图的遍历与连通性 最小生成树 最短路径 活动网络. 图的基本概念. 定义 : 图是一个二元组: Graph = ( V , E ) 顶点集合 V = { x | x  某个数据对象 } 是有穷非空集合; E = {( x , y ) | x , y  V } 是边 (edge ) 的集合。. 有向图 与无向图. 有向图 顶点对 是有序的。 x 称为 始点 , y 为 终点 。 - PowerPoint PPT Presentation

Transcript of 第八章 图

Page 1: 第八章 图

第八章 图赵建华

南京大学计算机系

Page 2: 第八章 图

•图的基本概念•图的存储表示•图的遍历与连通性 •最小生成树•最短路径 •活动网络

内容

Page 3: 第八章 图

图的基本概念•定义 : 图是一个二元组:

Graph = ( V, E )– 顶点集合 V = { x | x 某个数据对象 } 是有穷非空集合;

– E = {(x, y) | x, y V } 是边 (edge) 的集合。

Page 4: 第八章 图

有向图与无向图 • 有向图

– 顶点对 <x, y> 是有序的。 x 称为始点, y为终点。

– <x,y> 和 <y,x> 是不同的边 (if x!=y) 。• 无向图

– 顶点对 (x, y) 是无序的。– (x, y) 和 (y,x) 是同一条边。

Page 5: 第八章 图

完全图 完全图

有 n 个顶点的无向图有 n(n-1)/2 条边 , 则此图为完全无向图。任意两个结点之间有一条边。

有 n 个顶点的有向图有 n(n-1) 条边 , 则此图为完全有向图。任意两个结点对之间有一条边。

0 0 0 0

1

1

1 12 2

2 265433

Page 6: 第八章 图

• 邻接顶点 – 如果 (u, v) 是 E(G) 中的一条边,则称 u 与 v 互为邻接

顶点。• 子图

– 设有两个图 G = (V, E) 和 G' = (V', E') 。若 V ' V 且 E'E, 则称图 G' 是图 G 的子图。

• 权 – 某些图的边具有与它相关的数 , 称之为权。这种带权图叫做网

络。

子图0

1 2

3

0

1

3

0

1 2

3

0

2

3

Page 7: 第八章 图

• 顶点的度 – 一个顶点 v 的度是与它相关联的边的条数,记作 TD(v) 。– 在有向图中 , 顶点的度等于该顶点的入度与出度之和。

• 顶点 v 的入度 / 出度– 在有向图中,以 v 为终点 / 始点的有向边的条数。记作

ID(v)/OD(v) 。• 路径

– 在图 G = (V, E) 中 , 若从顶点 vi 出发 , 沿一些边经过一些顶点 vp1, vp2, …, vpm,到达顶点 vj。则称顶点序列 (vi vp1 vp2 ... vpm vj) 为从顶点 vi 到顶点 vj 的路径。

– 要求 (vi, vp1) 、 (vp1, vp2) 、 ... 、 (vpm, vj) 应是属于 E 的边。

Page 8: 第八章 图

• 路径长度– 非带权图的路径长度是指此路径上边的条数。– 带权图的路径长度是指路径上各边的权之和。

• 简单路径– 若路径上各顶点 v1, v2, ..., vm 均不 互相重复 , 则称这样的路径为简单路径。

• 回路– 若路径上第一个顶点 v1 与最后一个顶点 vm 重合 , 则称这样的路径为回路或环。

0

1 2

3

0

1 2

3

0

1 2

3

Page 9: 第八章 图

• 连通图与连通分量– 在无向图中 , 若从顶点 v1到顶点 v2有路径 , 则称顶点 v1 与 v2

是连通的。– 如果图中任意一对顶点都是连通的 , 则称此图是连通图。– 非连通图的极大连通子图叫做连通分量。

• 强连通图与强连通分量– 在有向图中 , 若对于每一对顶点 vi 和 vj, 都存在一条从 vi 到 vj

和从 vj 到 vi的路径 , 则称此图是强连通图。– 非强连通图的极大强连通子图叫做强连通分量。

• 生成树– 一个连通图的生成树是其极小连通子图,在 n 个顶点的情

形下,有 n-1 条边。

Page 10: 第八章 图

图的抽象数据类型class Graph {// 对象 : 由一个顶点的非空集合和一个边集合构成// 每条边由一个顶点对来表示。public: Graph(); // 建立一个空的图 void insertVertex (const T& vertex);

// 插入一个顶点 vertex, 该顶点暂时没有边 void insertEdge (int v1, int v2, int weight);

// 在图中插入一条边 (v1, v2, w) void removeVertex (int v);

// 在图中删除顶点 v ,以及所有关联到它的边

Page 11: 第八章 图

void removeEdge (int v1, int v2); // 在图中删去边 (v1,v2)

bool IsEmpty(); // 若图中没有顶点 , 则返回 true, 否则返回 false

T getWeight (int v1, int v2); // 函数返回边 (v1,v2) 的权值

int getFirstNeighbor (int v); // 给出顶点 v 第一个邻接顶点的位置

int getNextNeighbor (int v, int w); // 给出顶点 v 的某邻接顶点 w 的下一个邻接顶点

};

Page 12: 第八章 图

图的存储表示• 图的模板基类

– 模板的数据类型参数表 <class T, class E> ,– T 是顶点数据的类型, E 是边上所附数据的类型。– 可以表示最复杂的情况:即带权无向图– 处理非带权图时,边不附带数据,因此数据类型参数表改为 <class T> 。

• 如果使用的是有向图,也可以对程序做相应的改动。

Page 13: 第八章 图

图的模板基类 const int maxWeight = ……; // 表示的权值const int DefaultVertices = 30; // 最大顶点数 (=n)template <class T, class E>class Graph { // 图的类定义protected: int maxVertices; // 图中最大顶点数 int numEdges; // 当前边数 int numVertices; // 当前顶点数 int getVertexPos (T vertex); // 给出顶点 vertex 在图中位置public:

Page 14: 第八章 图

… … … // 构造函数析构函数

bool GraphEmpty () const // 判图空否int NumberOfVertices () ; // 返回当前顶点数int NumberOfEdges (); // 返回当前边数virtual T getValue (int i); // 取第 i 个顶点的值virtual E getWeight (int v1, int v2); // 取边上权值 // 取顶点 v 的第一个邻接顶点以及下一个邻接结点virtual int getFirstNeighbor (int v);virtual int getNextNeighbor (int v, int w);

// 结点和边的操作virtual bool insertVertex (const T vertex);virtual bool insertEdge (int v1, int v2, E cost);

virtual bool removeVertex (int v); virtual bool removeEdge (int v1, int v2); };

Page 15: 第八章 图

邻接矩阵 (Adjacency Matrix)• 图的邻接矩阵表示包含

– 一个记录各个顶点信息的顶点表(表示集合 V )– 一个表示各个顶点之间关系的邻接矩阵(表示集合 E )

• 设图 A = (V, E) 是一个有 n 个顶点的图 , 图的邻接矩阵是一个二维数组 A.edge[n][n] :

否则

或者如果0

),(,1]j][i[edge.A

EjiEji

注意: edge可以看作是二维的 bitSet表示方法如果图是无向的,那么必然是对称矩阵

Page 16: 第八章 图

• 无向图的邻接矩阵是对称的 ;• 有向图的邻接矩阵可能是不对称的。

0101101001011010

A.edge

0

1 2

3

0

1

2

000101010

A.edge

Page 17: 第八章 图

• 有向图:第 i 行中 1 的个数就是顶点 i 的出度,第 j 列中 1 的个数就是顶点 j 的入度。

• 无向图:第 i 行 ( 列 ) 中 1 的个数就是顶点i 的度。(注意,无向图的邻接矩阵是对称的)

Page 18: 第八章 图

网络(带权图 ) 的邻接矩阵

ji

ji,ji,jiji,ji,jiji

ji若

或且若或且若

,)(,)(),,(

]][[0

EEEEW

A.edge

8

6

3

1

29 5

4

2

0

3

1

068053290410

A.edge

例如:

Page 19: 第八章 图

用邻接矩阵表示的图的类定义template <class T, class E>class Graphmtx : public Graph<T, E> {

… … … … // 输入输出友元private:

T * VerticesList; // 顶点表E **Edge; // 邻接矩阵int getVertexPos (T vertex) ;

// 给出顶点 vertex 在图中的位置

Page 20: 第八章 图

public: … … … … // 构造函数与析构函数的声明

T* getValue (int i); // 获取第 i 个顶点的值 , //i 不合理则返回 NULL ;

E getWeight (int v1, int v2); // 取边 (v1,v2) 上权值

// 取顶点 v 的第一个 / 下一个邻接顶点,用于便利邻接顶点int getFirstNeighbor (int v);int getNextNeighbor (int v, int w);

// 插入 / 删除结点和边 bool insertVertex (const T vertex); bool insertEdge (int v1, int v2, E cost);

bool removeVertex (int v);bool removeEdge (int v1, int v2);

};

Page 21: 第八章 图

template <class T, class E>Graphmtx<T, E>::Graphmtx (int sz) { // 构造函数

maxVertices = sz; numVertices = 0; numEdges = 0;int i, j;VerticesList = new T[maxVertices]; // 创建顶点表

// 申请邻接矩阵的空间,已经默认 E 为 intEdge = (int **) new int *[maxVertices];

for (i = 0; i < maxVertices; i++)Edge[i] = new int[maxVertices];

// 矩阵初始化,按照带权图的方式初始化,不存在任何边for (i = 0; i < maxVertices; i++)

for (j = 0; j < maxVertices; j++)Edge[i][j] = (i == j) ? 0 : maxWeight;

};

Page 22: 第八章 图

template <class T, class E>int Graphmtx<T, E>::getFirstNeighbor (int v) {// 给出顶点位置为 v 的第一个邻接顶点的位置 , // 如果找不到 , 则函数返回 -1 if (v != -1) { for (int col = 0; col < numVertices; col++) if (Edge[v][col] && Edge[v][col] < maxWeight)

return col; } return -1;};

Page 23: 第八章 图

template <class T, class E>int Graphmtx<T, E>::getNextNeighbor (int v, int w) {// 给出顶点 v 的某邻接顶点 w 的下一个邻接顶点 if (v != -1 && w != -1) {

for (int col = w+1; col < numVertices; col++) if (Edge[v][col] && Edge[v][col] < maxWeight) return col; } return -1;};

每次调用时,使用上一次的位置 w作为参数

Page 24: 第八章 图

• 邻接矩阵的改进形式– 把邻接矩阵的各行分别组织为一个单链表。– 同一个顶点发出的边链接在同一个边链表中;– 每一个链结点代表一条边(边结点)。结点中有另一顶点的下标 dest 和指针 link 。

– 对于带权图,边结点中还保存权值 cost 。• 顶点表:

– 第 i 个顶点中保存该顶点的数据,以及它对应边链表的头指针 adj 。

邻接表 (Adjacency List)

边集 E 按照始结点,分为 n个集合;每个集合用单链表表示

Page 25: 第八章 图

A

B C

D

data adj

A

B

C

D

0

1

2

3

dest link dest link

1 3

0 2

1

0

无向图的邻接表

• 统计某顶点对应边链表中结点个数,可得该顶点的度。

• 某条边 (vi, vj) 在邻接表中有两个边结点,分别在第 i 个顶点和第 j 个顶点对应的边链表中。

Page 26: 第八章 图

data adj

A

B

C

0

1

2

dest link

dest link

邻接表 ( 出边表 )

data adj

A

B

C

0

1

2

dest link

逆邻接表 ( 入边表 )

1

0 2

0

1

1

有向图的邻接表和逆邻接表

A

B

C

Page 27: 第八章 图

BB

AA

CC

DD6

95 2

8

data adj

A

B

C

D

0

1

2

3

dest cost link

1 5 3 6

2 8

3 2

1 9

( 出边表 )( 顶点表 )

网络 ( 带权图 ) 的邻接表

• 统计出边表中结点个数,得到该顶点的出度;• 统计入边表中结点个数,得到该顶点的入度。

Page 28: 第八章 图

• 在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。

• 设图中有 n 个顶点, e 条边,– 表示无向图时需要 n 个顶点结点, 2e 个边结点;– 表示有向图时,(不考虑逆邻接表)需要 n 个顶点结点, e 个边结点。

• 当 e 远远小于 n2 时,可以节省大量的存储空间。

• 把同一个顶点的所有边链接在一个单链表中,也使得某些操作更为便捷。( firstNeighbor ,nextNeighbor)

Page 29: 第八章 图

用邻接表表示的图的类定义 template <class T, class E>struct Edge { // 边链表结点的定义 int dest; // 边的另一顶点位置

E cost; // 边上的权值 Edge<T, E> *link; // 下一条边链指针 Edge () {} // 构造函数 Edge (int num, E cost) // 构造函数 : dest (num), weight (cost), link (NULL) { } bool operator != (Edge<T, E>& R) const

{ return dest != R.dest; } // 判边等否};

Page 30: 第八章 图

template <class T, class E>struct Vertex { // 顶点的定义 T data; // 顶点的名字

Edge<T, E> *adj; // 边链表的头指针

};

Page 31: 第八章 图

template <class T, class E>class Graphlnk : public Graph<T, E> { // 图的类定义

friend istream& operator >> (istream& in, Graphlnk<T, E>& G); // 输入

friend ostream& operator << (ostream& out,Graphlnk<T, E>& G); // 输出

private: Vertex<T, E> *NodeTable;

// 顶点表 ( 各边链表的头结点 )// 顶点的个数存放在 numVertices ,从基类继

承 int getVertexPos (const T vertx) { // 给出顶点 vertex 在图中的位置

for (int i = 0; i < numVertices; i++) if (NodeTable[i].data == vertx) return i;

return -1; }

Page 32: 第八章 图

public:Graphlnk (int sz = DefaultVertices); // 构造函数~Graphlnk(); // 析构函数// 获取信息的接口T getValue (int i) ; // 取第 i 个顶点的值;E getWeight (int v1, int v2); // 取边 (v1,v2) 权值

// 结点和边的操作接口bool insertVertex (const T& vertex);bool removeVertex (int v);bool insertEdge (int v1, int v2, E cost);bool removeEdge (int v1, int v2);

// 遍历邻接结点的接口int getFirstNeighbor (int v);int getNextNeighbor (int v, int w);

};

Page 33: 第八章 图

template <class T, class E>Graphlnk<T, E>::Graphlnk (int sz) { // 建立一个空的邻接表

maxVertices = sz; numVertices = 0; numEdges = 0;

// 创建顶点表数组NodeTable = new Vertex<T, E>[maxVertices];if (NodeTable == NULL)

{ cerr << " 存储分配错! " << endl; exit(1); }

// 初始化边集合(空集) for (int i = 0; i < maxVertices; i++) NodeTable[i].adj = NULL;};

Page 34: 第八章 图

template <class T, class E>Graphlnk<T, E>:: ~ Graphlnk() {

// 析构函数:删除一个邻接表

// 删除所有顶点的边for (int i = 0; i < numVertices; i++ ) {

Edge<T, E> *p = NodeTable[i].adj;

// 删除第 i 个顶点的边链表中的所有结点(边的信息)

while (p != NULL) {NodeTable[i].adj = p->link;delete p; p = NodeTable[i].adj;

}}

// 删除顶点表数组delete [ ]NodeTable;

};

Page 35: 第八章 图

template <class T, class E>int Graphlnk<T, E>::getFirstNeighbor (int v) {// 给出顶点位置为 v 的第一个邻接顶点的位置 , // 如果找不到 , 则函数返回 -1

//if(v>=0 && v<numVertice) // 应该使用这个判断if (v != -1) { // 顶点 v 存在Edge<T, E> *p = NodeTable[v].adj; //对应边链表第一个边结点if (p != NULL) return p->dest;

// 存在 , 返回第一个邻接顶点 } return -1; // 第一个邻接顶点不存在};

Page 36: 第八章 图

template <class T, class E>int Graphlnk<T, E>::getNextNeighbor (int v, int w) {// 给出顶点 v 的邻接顶点 w 的下一个邻接顶点的位置 ,// 若没有下一个邻接顶点 , 则函数返回 -1 if (v != -1) { // 顶点 v 存在

Edge<T, E> *p = NodeTable[v].adj;

// 寻找当前的边;比较低效while (p != NULL && p->dest != w) p = p->link;

//p==NULL 表示 w 不是 v 的邻接顶点//p->link == NULL 表示 w 是最后一个邻接顶点if (p != NULL && p->link != NULL)

return p->link->dest; // 返回下一个邻接顶点

} return -1; // 下一邻接顶点不存在};

在遍历一个顶点的所有边时,可以保存指向当前链表结点的指针。这样寻找下一个邻接顶点时效率更高一些。

Page 37: 第八章 图

template <class T, class E>E graphlnk<T,E>::getWeight(int v1, int v2) {// 返回边 (v1,v2) 上的权值;无边则返回 0 ;

if(v1 != -1 && v2 != -1) {// 寻找从 v1 到 v2 的边;Edge<T,E> *p = NodeTable[v1].adj; // 第一条边while(p!=NULL && p->dest !=v2)

p = p->link; // 下一条边if(p!=NULL) return p->cost;

}return 0;

}

Page 38: 第八章 图

template <class T, class E>E graphlnk<T,E>::insertVertex ( const T& vertex) {

// 插入顶点 vertex 。成功返回 true ,失败返回 falseif (numVertice == maxVertices) return false;// 满

// 增加一个顶点:插入到最后,计数器加一。NodeTable[numVertices].data = vertex;numVertices ++;

return true;}

Page 39: 第八章 图

template <class T, class E>E graphlnk<T,E>::removeVertex(int v)

// 删除第 v 个顶点。成功则返回 true ,否则返回 false ;if(v<0 || v>= numVertices) return false;

// 不合法的顶点号;// 下面要做两件事情//1 、删除所有进入 v 的边;如果是无向图,那么// 是记录在其它边链表中的所有和 v 相连的边//2 、删除所有从 v 出发的边;//3 、消除结点的记录,并填补结点的空缺。// 并修改边中的信息。

Page 40: 第八章 图

// 删除和 v 相连的边Edge<T,E> *p, *s, *t; int i,k;while(NodeTable[v].adj != NULL) { // 删除第 v 个边链表中所有的边;

p = NodeTable[v].adj; k=p->dest;s = NodeTable[k].adj; t = NULL;

// 寻找和 p 所指结点对称的边; t 总是指向 s 的前一个结点while(s!=NULL && s->dest != v) { t=s; s=s->link;}if(s != NULL){ // 找到了对称边,删除

if(t==NULL)NodeTable[k].adj=s->link; // 对称边是首结点;else t->link = s->link;delete s;

}NodeTable[v].adj = p->link; // 清除顶点 v 的边链表结点;delete p; numEdges--;

}

对于有向图,应该分两步删除和 v 相连的边:

1 、删除从 v 出发的边; 2 、删除到达 v 的边;

第一步遍历顶点 v 的链表,第二步遍历所有链表

Page 41: 第八章 图

numVertices --; // 删除顶点,将最后的顶点移动到 v 处NodeTable[v].data = NodelTable[numVertices].data;

p = NodeTable[v].adj = NodeTable[numVertices].adj;while(p!=NULL){

s = NodeTable[p->dest].adj; // 原最后一个结点的边;while(s!=NULL)

// 原来指向最后一个结点的边现在应该指向 v ;if(s->dest == numVertices){s->dest = v; break;}else s=s->link;

}return true;

};//end of method;这里处理的同样是无向图。所以我们可以只修改 p 的邻接顶点上的边的信息。

处理有向图时需要遍历所有的边链表。

需要调整的原因是:在边里面用下标索引指示结点。现在因为结点的移动导致结点的下标出现变化

Page 42: 第八章 图

邻接多重表 (Adjacency Multilist)

• 邻接多重表可以方便地处理图的边• 每一条边只有一个边结点。结点的结构如下:

– Mark :标记域;– vertex1 , vertex2 存放顶点;– path1 , path2 :分别指向对应于 vertex1 和

vertex2 的边链表;– 还可以增加字段,存放其它和边相关的信息。

mark vertex1 vertex2 path1 path2 mark vertex1 vertex2 path1 path2

Page 43: 第八章 图

无向图的邻接多重表表示• 顶点的数据结构

• data 存放与该顶点相关的信息, Firstout 是指示第一条依附该顶点的边的指针。

• 顶点的数据可以存放在一个顺序表中。• 和每个顶点相连的边组成一个链表

– Firstout 指向第一条边– 顶点 v 的边 curEdge 的下一条边是:

(v==curEdge->vertext1)? path1 : path2;

data Firstout data Firstout

Page 44: 第八章 图

mark vtx1 vtx2 path1 path2

0 1

0 2

1 3

e1

e2

e3

data Fout

A

B

C

D

0

1

2

3

e1

e2

e3

A

B

C

D

邻接多重表的例子

Page 45: 第八章 图

• 邻接多重表 ( 十字链表 ) 可以同时表示邻接表和逆邻接表(即从一个顶点离开的边和到达一个顶点的边)。

• 边结点的结构

• mark :处理标记;• vertex1 和 vertex2 :始顶点和终顶点• path1 指向始顶点与该边相同的下一条边的指针;• path2 是指向终顶点与该边相同的下一条边的指针。

mark vertex1 vertex2 path1 path2 mark vertex1 vertex2 path1 path2

有向图的邻接多重表

Page 46: 第八章 图

• 顶点结点的结构

• 顶点的数据可以存放在一个顺序表中。• data :存放与该顶点相关的信息;• Firstin 指示以该顶点为始顶点的出边表的第一条边,• Firstout 指示以该顶点为终顶点的入边表的第一条边。

data Firstin Firstout data Firstin Firstout

Page 47: 第八章 图

mark vtx1 vtx2 path1 path2

0 1

0 3

1 2

2 3

3 4

4 0

e1

e2

e3

e4

e5

e6

data Fin Fout

A

B

C

D

E

0

1

2

3

4

e1

e2

e3

e4

e5

e6A

B C

D

E

邻接多重表的结构

Page 48: 第八章 图

图的遍历与连通性• 从给定连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 (Graph Traversal) 。

• 图中可能存在回路,因此可能沿着某些边多次到达一个顶点。

• 为避免重复访问,设置一个辅助数组 visited [ ]– visited[i]==0 表示 i 尚未被访问;– visited[i]==1 表示 i 已经被访问;– 遍历时,只访问 visited[i]==0 的顶点;访问顶点 i后立即将 visited[i] 设置为 1 。

Page 49: 第八章 图

• 深度优先搜索 DFS (Depth First Search)• 从起始结点开始,不断选择当前结点的相邻结点向前搜索。如果某个结点的所有相邻结点都被探索过,就回溯。

• 广度优先搜索 BFS (Breadth First Search)• 从起始结点开始,首先访问当前结点的所有相邻结点;然后访问这些相邻结点的相邻结点;…

• 在深度 /广度优先搜索的过程中访问到的顶点和边组成深度 /广度优先生成树。

图遍历的种类

Page 50: 第八章 图

深度优先搜索 DFS 示例

A

CD

E

G

B

F IH

A

CD

E

G

B

F IH

1 2 3

45

6

7

8 9

1 2 3

45

6

7

8 9前进 回退

深度优先搜索过程 深度优先生成树

Page 51: 第八章 图

• 前进部分– 访问图中某一起始顶点 v ;– 访问 v 的任一邻接顶点 w1;

– 再访问与 w1邻接但还没有访问过的顶点 w2;

– 然后再从 w2 出发 , 访问还没有访问过的顶点 , …

– 直至到达所有的邻接顶点都被访问过的顶点 u 为止。• 回溯部分

– 退到前一次刚访问过的顶点。如果有其它尚未访问的邻接顶点,则访问这个顶点并继续前进;否则回溯直到可以重新前进为止;

• 重复上述过程 ,直到连通图中所有顶点都被访问过。

深度优先搜索 DFS 的过程

Page 52: 第八章 图

图的深度优先搜索算法(主程序)

template<class T, class E> void DFS (Graph<T, E>& G, const T& v) {// 从顶点 v 出发对图 G 进行深度优先遍历的主过程 int i, loc, n = G.NumberOfVertices(); // 顶点个数 bool *visited = new bool[n]; // 创建辅助数组 for (i = 0; i < n; i++) visited [i] = false; //辅助数组 visited 初始化

loc = G.getVertexPos(v); DFS (G, loc, visited); // 从顶点 v开始深度优先搜索 delete [] visited; //释放 visited};

Page 53: 第八章 图

template<class T, class E>void DFS (Graph<T, E>& G, int v, bool visited[]) { cout << G.getValue(v) << ' '; //访问顶点 v visited[v] = true; // 作访问标记 int w = G.getFirstNeighbor (v); // 第一个邻接顶点 while (w != -1) { // 若邻接顶点 w 存在 if ( !visited[w] ) DFS(G, w, visited);

// 若 w未访问过 , 递归访问顶点 w w = G.getNextNeighbor (v, w); // 下一个邻接顶点 }};

深度优先搜索算法(递归版本)

Page 54: 第八章 图

广度优先搜索 BFS 示例

广度优先搜索过程 广度优先生成树

A

CD

E

G

B

F IH

A

CD

E

G

B

F H

1 2

34

5

6

7

8 9

1 2

34

5

6

7

8 9I

Page 55: 第八章 图

• BFS首先访问起始顶点 v ;由 v 出发 , 依次访问 v 的各个未被访问过的邻接顶点 w1, w2, …, wt,

• 然后再顺序访问 w1, w2, …, wt 的所有还未被访问过的邻接顶点。

• … 直到图中所有顶点都被访问到为止。• 广度优先搜索是一种分层的搜索过程 ,

– 每向前走一步可能访问一批顶点 , 不像深度优先搜索那样有往回退的情况。

– 广度优先搜索不是一个递归的过程。• 算法中使用一个队列来记录待处理的顶点;使用了

visited 数组来标记是否被访问过。

广度优先搜索过程

Page 56: 第八章 图

template <class T, class E> void BFS (Graph<T, E>& G, const T& v) {

int i, w, n = G.NumberOfVertices(); // 图中顶点个数bool *visited = new bool[n];for (i = 0; i < n; i++) visited[i] = false; //初始化 visited 数组;int loc = G.getVertexPos (v); // 取顶点号

out << G.getValue (loc) << ' '; //访问顶点 vvisited[loc] = true; // 做已访问标记 Queue<int> Q; Q.EnQueue (loc); // 顶点进队列 , 实现分层访问while (!Q.IsEmpty() ) { //循环 , 访问所有结点

// 不断从队列中取出结点,访问该结点的所有子结点,//并将这些子结点入栈。// 具体过程见下页

}//外层循环,判队列空否 delete [] visited;};

图的广度优先搜索算法

Page 57: 第八章 图

Q.DeQueue (loc); //取队列中的结点;w = G.getFirstNeighbor (loc); // 第一个邻接顶点while (w != -1) { // 遍历所有相邻结点

if (!visited[w]) { // 若未访问过cout << G.getValue (w) << ' ';//访问visited[w] = true; Q.EnQueue (w); // 顶点 w 进队列

}w = G.getNextNeighbor (loc, w);

// 下一个邻接顶点}

Page 58: 第八章 图

连通分量 (Connected component)

•对于非连通图,深度优先搜索算法或广度优先搜索算法只能访问到该顶点所在最大连通子图(连通分量)的所有顶点。

•从无向图每一连通分量中的一个顶点出发进行遍历 , 可求得无向图的所有连通分量。

•对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。

Page 59: 第八章 图

A

C D EB

F G O

I

H

J

NML

K

非连通无向图A H K

C D E IB

F OG J

NML

非连通图的连通分量的生成树

Page 60: 第八章 图

生成树并加入森林的代码if (F.IsEmpty()) //F 原为空生成森林 , 建根

subT = rt = F.BuildRoot(G.GetValue(i));

// 顶点 i 的值成为根 rt 的值 else

subT = F.InsertRightSibling(subT, G.GetValue(i));

// 顶点 i 的值成为 subT 右兄弟的值//subT 指向的是上一次得到的生成树// 注意:森林使用二叉树方式表示;

DFS_Tree (G, F, subT, i, visited);

// 从顶点 i 出发深度优先遍历 , 建子树

Page 61: 第八章 图

最小生成树 ( minimum cost spanning tree )

• 使用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。

• n 个顶点的连通网络的生成树有 n 个顶点、 n-1 条边。

• 构造最小生成树– 假设有一个网络,用以表示 n 个城市之间架设通信线路,边上的权值代表架设通信线路的成本。如何架设才能使线路架设的成本达到最小?

Page 62: 第八章 图

• 构造最小生成树的准则 必须使用且仅使用该网络中的 n-1 条边来联结网络中的 n 个顶点;

不能使用产生回路的边; 各边上的权值的总和达到最小。

北京 天津

南京

上海

广州

西安

成都

昆明

武汉

347

6

4158

3124

19

25

3822

2219

31

39

4450

Page 63: 第八章 图

北京 天津

南京

上海

广州

西安

成都

昆明

武汉

7

6

2419

22

2219

31

北京 天津

南京

上海

广州

西安

成都

昆明

武汉

347

6

4158

3124

19

25

3822

2219

31

39

4450

Page 64: 第八章 图

克鲁斯卡尔 (Kruskal) 算法

• 基本思想:– 一个 n 个顶点的连通网络 N = { V, E } ;– 最初先构造一个只有 n 个顶点 , 没有边的非连通图 T = { V, } ,然后逐步加入如下条件的边

• 尚未被选中• 具有最小权值• 且两个顶点落在不同的连通分量上

– 如此上一步,直到所有顶点在同一个连通分量上为止(共需要 n-1步)。

Page 65: 第八章 图

Kruskal 算法的伪代码描述 T = ; //T 是最小生成树的边集合

//E 是带权无向图的边集合while ( T 包含的边少于 n-1 && E 不空 ) {

从 E 中选一条具有最小代价的边 (v, w);

从 E 中删去 (v, w);

如果 (v, w) 加到 T 中后不会产生回路 , 则将

(v, w) 加入 T; 否则放弃 (v, w);

}

if ( T 中包含的边少于 n-1 条 )

cout << “ 不是连通图 " << endl;

Page 66: 第八章 图

为什么算法是正确的?( 1 )• 对于任意的连通网络 {V,E} ,其最小的边一定在其 MST 中

• 可用反证法证明:如果不在 MST T 中,则把最小边加入,就会形成具有一条回路的图 G 。删除这个回路中的任意一条边。得到的图仍然是连通的,且具有 n-1 条边。因此得到树 T’ 。 T’ 的权值要小于 T 。

Page 67: 第八章 图

为什么算法是正确的?( 2 )• 假设 {V,E} 中权值最小的边为 (v,v’)

–将 (v,v’) 看作一个顶点;– x 和新顶点之间的边的权值为 (x,v)以及 (x,v’)之间的最小者;

–假设新的图是 {V’,E’} ,那么新图的 MST ,加上 (v,v’)就是 {V,E} 的MST 。

Page 68: 第八章 图

• 算法的实现:利用最小堆 (MinHeap) 和并查集(DisjointSets)来实现克鲁斯卡尔算法。

• 首先 , 利用最小堆来存放 E 中的所有的边,并不断找出最小的边

• 利用并查集的运算检查依附一条边的两顶点tail 、 head 是否在同一连通分量 ( 即并查集的同一个子集合 ) 上。– 是:舍去这条边;– 否:将此边加入 T, 同时合并这两个顶点的连通分量上。

• 注意:连通性是一种等价关系。

边的两个顶点位置 边的权值 tail head cost tail head cost

Page 69: 第八章 图

10

55

00

44

66

11

33

22

28

10

25

14

24

22

16

1812

55

00

44

66

11

33

22 55

00

44

66

11

33

22

原图 (a) (b)

构造最小生成树的过程

Page 70: 第八章 图

10

12

55

00

44

66

11

33

22

28

10

25

14

24

22

16

1812

55

00

44

66

11

33

22 55

00

44

66

11

33

22

10 14

12

原图 (c) (d)

55

00

44

66

11

33

22

10 14 16

12

(e) (f) (g)

55

00

44

66

11

33

22

10 14

22

16

12

55

00

44

66

11

22

10

25

14

22

16

1233

Page 71: 第八章 图

最小生成树类定义const float maxValue = FLOAT_MAX // 机器可表示的、问题中不可能出现的大数template <class T, class E>struct MSTEdgeNode { // 树边结点的类定义 int tail, head; // 两顶点位置 E cost; // 边上的权值

MSTEdgeNode() : tail(-1), head(-1), cost(0) { }// 构造函数

};

Page 72: 第八章 图

template <class T, class E>class MinSpanTree { // 树的类定义protected: MSTEdgeNode<T, E> *edgevalue; // 边值数组 int maxSize, n; // 最大元素个数和当前个数

public: MinSpanTree (int sz = DefaultSize-1) : MaxSize (sz), n (0) { edgevalue = new MSTEdgeNode<T, E>[sz]; } int Insert (MSTEdgeNode& item); };

Page 73: 第八章 图

template <class T, class E> void Kruskal (Graph<T, E>& G, MinSpanTree<T, E>& MST) {

MSTEdgeNode<T, E> ed; // 边结点辅助单元int u, v, count;int n = G.NumberOfVertices(); // 顶点数int m = G.NumberOfEdges(); // 边数

// 将所有的边加入最小堆的代码,见下页;

// 不断从最小堆取边,使用并查集来记录连通性,适当的// 边加入最小堆。//代码见再下页。

} ;

Kruskal 算法的实现

Page 74: 第八章 图

建立最小堆MinHeap <MSTEdgeNode<T, E>> H(m);

// 最小堆的声明for (u = 0; u < n; u++) // 两个 for循环枚举所有边;

for (v = u+1; v < n; v++) // 其他数据结构的枚举方法不同 if (G.getWeight(u,v) != maxValue) {

//(u,v) 之间存在边则插入最小堆;ed.tail = u; ed.head = v; // 插入堆ed.cost = G.getWeight (u, v);H.Insert(ed);

}这个过程的时间复杂度比较高;实际上, Kruskal算法适合于边较少的情况;通常使用邻接表表示的数据结构

Page 75: 第八章 图

UFSets F(n); // 声明并初始化并查集count = 1; // 最小生成树边数计数

while (count < n) { //反复执行 , 取 n-1 条边 H.Remove(ed); //退出具最小权值的边 u = F.Find(ed.tail); v = F.Find(ed.head); // 取两顶点所在集合的根 u 与 v if (u != v) { // 不是同一集合 , 不连通时加入 F.Union(u, v); // 合并 , 连通它们 MST.Insert(ed); // 该边存入最小生成树 MST count++; } }

并查集的 Find 和 Union 所需时间基本可以认为是常数。

Page 76: 第八章 图

出堆顺序出堆顺序 (0,5,10) 选中 (2,3,12) 选中 (1,6,14) 选中 (1,2,16) 选中 (3,6,18) 舍弃 (3,4,22) 选中 (4,6,24) 舍弃 (4,5,25) 选中

并查集

原图

-2

-2

-2

-2

-1 -1 -1 -1 -1 -1 -1

-1 -1 -1 -1 -10

2-1 -1 -10

-2 2 0

0

0

0

0 1 2 3 4 5 6

-2 1-1

1-2 -1-4 21

-2 -5 1 2 11

-71 1 2 1 1

F

55

00

44

66

11

33

22

28

10

25

14

24

22

16

1812

(0,5,10)

(2,3,12)

(1,6,14)

(1,2,16)

(3,4,22)

(4,5,25)

Page 77: 第八章 图

普里姆 (Prim) 算法• 基本思想:

– 从连通网络 N = {V, E} 中任选顶点 u0 作为初始顶点,加入顶点集 U 中。边集 Emst的初始值为空。

– 选择权值最小的、满足条件如下的边一个顶点在 U 中 , 而另一个顶点不在 U

中。将另一顶点加入 U 中,这个边被加入 Emst中。

– 重复上一步骤,直到所有顶点都加入到 U 中。 Emst中的边就是最小生成树的边。

Page 78: 第八章 图

普里姆 (Prim) 的伪代码描述选定构造最小生成树的出发顶点 u0;

Vmst = {u0} , Emst = ;

while (Vmst包含的顶点少于 n && E 不空 ) {

从 E 中选一条边 (u, v), 满足 uVmst∩vV-Vmst, 且具有最小代价 (cost);

令 Vmst = Vmst {∪ v}, Emst = Emst {(∪ u, v)};

将新选出的边从 E 中剔除: E = E-{(u, v)};

}

if (Vmst包含的顶点少于 n)

cout << " 不是最小生成树 " << endl;

Page 79: 第八章 图

2525

10

55

00

44

66

11

33

22

28

10

25

14

24

22

16

1855

00

44

66

11

33

22 55

00

44

66

11

33

22

10

原图 (a) (b)

55

00

44

66

11

33

22

10

(c) (d) (e) (f)

55

00

44

66

11

33

22

10

22

12

55

00

44

66

11

22

10

25

14

22

16

1233

25

22

12

Page 80: 第八章 图

为什么 Prim 算法是正确的( 1 )

• 首先证明这样一个结论:– 对于任意一个连通网络 {V,E} ,设 T 是它的一棵MST 。那么对于任意顶点 v , T 的边集中必然包含了和 v 相连的边中最小的边。

–反证法:假设MST T’ 不包含 v 相连的最小边,我们可以如下构造出更小的生成树:首先在 T’ 加入和 v 相连的最小边,此时必然出现包含 v 的回路。这条回路中必然包含了这条最小边和 v 的另一条已经在 T’ 中的边。删除已经在 T’ 中的边就可以再得到一棵树T’’ 。新树的权值更小。

Page 81: 第八章 图

为什么 Prim 算法是正确的( 2 )

• 假设从 u0开始,选择一条边 (u0,v0) 后,将u0,v0 看作同一个顶点 v’’ 后得到 {V’,E’}

• {V,E} 的 MST就是 {V’,E’} 的 MST 中,将v’’替换为 u0,v0 ,加入边 (u0,v0) 。

• 因此,算法中每一步将 U 看作一个顶点。

Page 82: 第八章 图

Prim 算法的实现#include "heap.h"template <class T, class E>void Prim (Graph<T, E>& G, /* 图 */ const T u0, /* 起始顶点

*/ MinSpanTree<T, E>& MST /*得到的 MST*/)

{MSTEdgeNode<T, E> ed; // 边结点辅助单元int i, u, v, count;int n = G.NumberOfVertices(); // 顶点数int m = G.NumberOfEdges(); // 边数int u = G.getVertexPos(u0); //起始顶点号MinHeap <MSTEdgeNode<T, E>> H(m); // 最小堆

Page 83: 第八章 图

bool Vmst = new bool[n]; //bitSet , MST 的顶点集合for (i = 0; i < n; i++) Vmst[i] = false;Vmst[u] = true; //u 加入生成树 ,bitSet运算;count = 1;do { //迭代

v = G.getFirstNeighbor(u); while (v != -1) { //把所有和 u 相连的边加入堆中

if (!Vmst[v]) { //v 不在 mst 中ed.tail = u; ed.head = v;ed.cost = G.getWeight(u, v);H.Insert(ed); //(u,v) 加入堆

} v = G.getNextNeighbor(u, v);

}//堆中包含了所有一端在 MST 中,另一端不在

//MST 中的边。

Page 84: 第八章 图

while (!H.IsEmpty() && count < n) { H.Remove(ed); //选堆中具最小权的边if (!Vmst[ed.head]) { // 过滤掉两头在堆中的边。

MST.Insert(ed); // 加入最小生成树u = ed.head; Vmst[u] = true;//u 加入生成树顶点集合//跳出这个循环后,前面的 while循环// 将把这个 u 关联的边加入堆中。count++; break;

}}

} while (count < n); // 结点尚未加入 MST 中。};

Page 85: 第八章 图

• 例子

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(0,5,10), (0,1,28)}

ed = (0, 5, 10)

Vmst = {t, f, f, f, f, f, f}

Vmst = {t, f, f, f, f, t, f}

Page 86: 第八章 图

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(5,4,25), (0,1,28)}

ed = (5, 4, 25)

Vmst = {t, f, f, f, f, t, f}

Vmst = {t, f, f, f, t, t, f}

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(4,3,22), (4,6,24), (0,1,28)}

ed = (4, 3, 22)

Vmst = {t, f, f, f, t, t, f}

Vmst = {t, f, f, t, t, t, f}

Page 87: 第八章 图

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(3,2,12), (3,6,18), (4,6,24), (0,1,28)}ed = (3, 2, 12)Vmst = {t, f, f, t, t, t, f}

Vmst = {t, f, t, t, t, t, f}

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(2,1,16), (3,6,18), (4,6,24), (0,1,28)}ed = (2, 1, 16)Vmst = {t, f, t, t, t, t, f}

Vmst = {t, t, t, t, t, t, f}

Page 88: 第八章 图

55

00

44

66

11

33

22

28

10

25

14

24

22

16

18 12

H = {(1,6,14), (3,6,18), (4,6,24),

(0,1,28)}

ed = (1, 6, 14)

Vmst = {t, t, t, t, t, t, f}

Vmst = {t, t, t, t, t, t, t}• 最小生成树中边集合里存入的各条边为:

(0, 5, 10), (5, 4, 25), (4, 3, 22),

(3, 2, 12), (2, 1, 16), (1, 6, 14)

Page 89: 第八章 图

• prim算法适用于边稠密的网络。• Kruskal算法不仅适合于边稠密的情形,也适合于边稀疏的情形。

• 注意:当各边有相同权值时,由于选择的随意性,产生的生成树可能不唯一。

Page 90: 第八章 图

最短路径 (Shortest Path)

• 最短路径问题:如果从图中某一顶点(称为源点)另一顶点(称为终点)的路径可能不止一条,如何找到一条路径使得沿此路径上各边上的权值总和达到最小。

• 问题解法– Dijkstra 算法

• 边上权值非负情形的单源最短路径问题– Floyd 算法

• 所有顶点之间的最短路径:

Page 91: 第八章 图

边上权值非负情形的单源最短路径问题

• 问题– 给定一个带权有向图 D 与源点 v ,求从 v 到 D 中其他各顶点的最短路径。

– 各边的权值非负。• Dijkstra算法基本方法

– 按路径长度的递增次序 , 逐步产生最短路径。– 首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v 到其它各顶点的最短路径全部求出为止。

Page 92: 第八章 图

Dijkstra逐步求解的过程

源点 终点 最短路径 路径长度v0 v1 (v0,v1) 10

v2 (v0,v1,v2) (v0,v3,v2) ,60,50

v3 (v0,v3) 30

v4 (v0,v4) (v0,v3,v4) (v0,v3,v2 ,v4) 100,90,60

1100

44

3322

10 100

30

50

20

60

10

Page 93: 第八章 图

Dijkstra 算法① 初始化: S{v0};

dist[j]Edge[0][j], j = 1, 2, …, n-1; // n 为图中顶点个数② 求出最短路径的长度:

k :dist[k] 是所有 viV-S 使得 dist[i] 最小的 i ;S S {∪ k};

③ 修改: dist[i] min{dist[i], dist[k]+Edge[k][i]}, 对于每一个 iV-S ; ④ 判断:若 S = V, 则算法结束,否则转②。

// 如果图不连通,那么当 dist[k]=∞ 后,就可以判断 v0到//V-S 中的结点的最短路径长度都是∞,可以退出

Page 94: 第八章 图

• 可以证明如下的结论– 假设距离 v0最近的 k-1 个顶点的集合是 S ,那么从 v0到达第

k 个顶点 vx的最短路径除了最后 的 vx 其余都在 S 中。– 到达第 k 个顶点的最短路径必然是从 S 中的某个顶点经过一步到达。且所有这样的路径中,到达第 k 个顶点的路径长度最短

– 因此选择使得 miniS (Dist[i]+Edge[i,k]) 最小的 vk就得到 V-S

中离 v0最近的顶点。– 到达 vk的最短路径就是到达 vk-1的最短路径 +(vk-1,vk) 。

• 在开始执行第二步的时刻– 如果 iS , Dist[i] 就是从 v0 到 vi的距离;– 否则 Dist[i] = minjS(Dist[j]+Edge[j,i])

– 当程序运行结束时, Dist 存放了各个顶点的最短距离

Page 95: 第八章 图

计算从单个顶点到其他各顶点最短路径的算法

void ShortestPath (Graph<T, E>& G, int v/* 结点编号 */, E dist[], int path[]) {//Graph 是一个带权有向图。 dist[j], 0≤j<n, 是当前//求到的从顶点 v 到顶点 j 的最短路径长度 ,//path[j] 表示从 v0 到 j 的最短路径的倒数第二个节点;

int n = G.NumberOfVertices();bool *S = new bool[n]; // 最短路径顶点集, 0/1 表示int i, j, k; E w, min;for (i = 0; i < n; i++) // 初始化 S 为空集

S[i] = false;

Page 96: 第八章 图

S[v] = true; dist[v] = 0; // 顶点 v 加入顶点集合 Sfor (i = 0; i < n; i++) { //初始化 dist ;

dist[i] = G.getWeight(v, i); if (i != v && dist[i] < maxValue) path[i] = v;else path[i] = -1;

}

for (i = 0; i < n-1; i++) { //求解各顶点最短路径//选不在 S 中具有最短路径的顶点 u min = maxValue; int u = v; for (j = 0; j < n; j++)

if (!S[j] && dist[j] < min) { u = j; min = dist[j];}

S[u] = true; // 将顶点 u 加入集合 S

Page 97: 第八章 图

for (k = 0; k < n; k++) { // 修改 Distw = G.GetWeight(u, k);if (!S[k] && w < maxValue &&

dist[u]+w < dist[k]) {dist[k] = dist[u]+w; path[k] = u; // 修改到 k 的最短路径的前一个顶点;

}}

}//end of for (i = 0; i < n-1; i++) };

Page 98: 第八章 图

Dijkstra 算法中各辅助数组的最终结果

从表中读取源点 0 到终点 v 的最短路径的方法 : 举顶点 4为例 path[4] = 2 path[2] = 3 path[3] = 0 ,反过来排列,得到路径 0, 3, 2, 4 ,这就是源点 0 到终点 4 的最短路径。

00

4411

22 33

10

50 10

20

60

30

100

序号 顶点1 顶点2 顶点3 顶点4 Dist 10 50 30 60 path 0 3 0 2

Page 99: 第八章 图

活动网络 (Activity Network)

• 计划、施工过程、生产流程、程序流程等都是“工程”。除了很小的工程外,一般都把工程分为若干个叫做“活动”的子工程。

• 例如– 计算机专业学生的学习就是一个工程– 每一门课程的学习就是整个工程的一些活动。– 其中有些课程要求先修课程,有些则不要求。(活动之间存在先后关系)

用顶点表示活动的网络 (AOV 网络 )

Page 100: 第八章 图

C1 高等数学 C2 程序设计基础 C3 离散数学 C1, C2

C4 数据结构 C3, C2

C5 高级语言程序设计 C2

C6 编译方法 C5, C4

C7 操作系统 C4, C9

C8 普通物理 C1

C9 计算机原理 CC88

Page 101: 第八章 图

学生课程学习工程图

C8C8

C3C3

C5C5

C4C4

C9C9

C6C6

C7C7C1C1

C2C2

Page 102: 第八章 图

• AOV 网络 (Activity On Vertices) :用有向图表示一个工程的各个活动之间的关系。

– 用顶点表示活动,– 用有向边 <Vi, Vj> 表示活动 Vi 必须先于活

动 Vj 进行。• 在 AOV 网络中不能出现有向回路 , 即有向环。如果出现了有向环,则意味着某项活动应以自己作为先决条件。

Page 103: 第八章 图

• 有向环的检测– 构造 AOV 网络的拓扑排序序列。– 各个顶点 ( 代表各个活动 ) 在线形序列中的先后关系使得 AOV 网络中表示的所有前驱和后继关系都得到满足。

1

2

3

4

拓扑排序拓扑排序41

2

3

41 2 3

偏序关系 全序关系

Page 104: 第八章 图

• 拓扑排序– 构造 AOV 网络全部顶点的拓扑有序序列。

• AOV 网络中不存在有向环的充要条件是此网络存在一个拓扑排序序列。

Page 105: 第八章 图

• 例如 , 对学生选课工程图进行拓扑排序 , 得到的拓扑有序序列为

C1 , C2 , C3 , C4 , C5 , C6 , C8 , C9 , C7

或 C1 , C8 , C9 , C2 , C5 , C3 , C4 , C7 , C6

C8C8

C3C3

C5C5

C4C4

C9C9

C6C6

C7C7C1C1

C2C2

Page 106: 第八章 图

进行拓扑排序的方法① 输入 AOV 网络。令 n 为顶点个数。② 选择一个没有直接前驱的顶点输出 ; ③ 从图中删去该顶点以及所有它发出的

有向边 ;④ 重复以上 ②、③步 , 直到找不到没

有直接前驱的顶点为止。⑤ 如果所有顶点都已经输出,那么拓扑有序序列形成,拓扑排序完成;如果尚有未输出顶点,那么不存在拓扑有序序列。

Page 107: 第八章 图

C0C0 C1C1 C2C2

C3C3 C4C4 C5C5

拓扑排序的过程

(a) 有向无环图

C2C2

C5C5

C1C1C0C0

C3C3

(b) 输出顶点 C4

C1C1 C2C2

C5C5C3C3

(c) 输出顶点 C0

C4

C0 C2C2

C5C5

C1C1

C3

(d) 输出顶点 C3

Page 108: 第八章 图

C1C1 C2

C5C5

(e) 输出顶点 C2

C5C5

C1

(f) 输出顶点 C1

C5

(g) 输出顶点 C5

最后得到的拓扑有序序列为 C4 , C0 , C3 , C2 , C1 , C5 。它满足图中给出的所有前驱和后继关系,对于本来没有这种关系的顶点,如 C4 和C2 ,也排出了先后次序关系。

(h) 拓扑排序完成

Page 109: 第八章 图

AOVAOV 网络的其网络的其邻接表表示邻接表表示

C0C0 C1C1 C2C2

C3C3 C4C4 C5C5

C0

C1

C2

C3 0

C4

C5 0

C0

C1

C2

C3 0

C4

C5 0

0

1

2

3

4

5

count data adj

1

3

0

1

0

3

1 1

dest link

3 0 3 0

5 5

1 1 5 0 5 0

0 0 1 1 5 0 5 0

Page 110: 第八章 图

• 虽然在算法里面说“删除被输出的顶点以及从该顶点出发的边”,实际上,我们不希望在进行拓扑排序的时候真的修改 AOV 网络。

• 同时为了高效地找到没有前驱顶点的顶点,我们设立一个数组 count[] ,记录各顶点入度。入度为零的顶点即无前驱顶点。

• 在开始拓扑排序之前,入度数组 count[] 进行初始化– 在输入数据时初始化,可以在建立边 <j, k> 的边结点时增加 k 顶点的入度计数。

– 也可以在拓扑排序之前扫描所有顶点的边链表,也可以在拓扑排序之前扫描所有顶点的边链表,增加边的目标顶点的入度计数。增加边的目标顶点的入度计数。

• 每“删除”一个顶点,只需要将这个顶点的后继顶点的入度减一。

Page 111: 第八章 图

• 为了更加高效地“选择没有直接前驱的顶点”,– 使用一个链式栈存放入度为零的顶点。– 在将后继顶点的入度减一时,如果度数变成 0 ,就将这个顶

点加入栈中。• 精化后的算法:

1.建立入度为零的顶点栈,并将初始入度为 0 的顶点全部入栈;

2. 当入度为零的顶点栈不空时 , 重复执行 a)从顶点栈中退出一个顶点 , 并输出之 ; 同时对

输出顶点计数;b)对于所有离开这个顶点的边,将边的终顶点的

入度减一 ;c)如果终顶点入度减至 0, 则将该顶点入栈 ;

3. 如果输出顶点个数少于 AOV 网络的顶点个数 , 则报告网络中存在有向环。

Page 112: 第八章 图

• 算法空间效率的进一步优化– 性质:在栈内的顶点的入度一定是 0 ,而且我们不需要考虑栈内顶点的入度。

– 根据这个性质,我们可以考虑栈直接使用 count 数组的空间。– 栈中元素使用顶点下标表示,形成一个单链表。设立栈顶指

针 top, 指示当前栈顶位置。栈初始化时置 top = -1 。• 将顶点 i 进栈时执行以下指针的修改:

count[i] = top; top = i ;////top 指向新栈顶 i, , 原栈顶元素在 count[i] 中//i//i 已经进入栈中,因此已经进入栈中,因此 count[i]count[i] 是栈的链接指针是栈的链接指针

• 退栈操作可以写成:j = top; top = count[top];

//位于栈顶的顶点记于 j, top退到次栈顶• 栈空的判断是

top==-1 ;

Page 113: 第八章 图

拓扑排序时入度为零的顶点栈在count[] 中的变化

拓扑排序时入度为零的顶点栈在count[] 中的变化

C0C0 C1C1 C2C2

C3C3 C4C4 C5C5

130103

13-1123

22-1122

21-1222

0

1

2

3

4

5

0

1

2

3

4

5

0

1

2

3

4

5

0

1

2

3

4

5

top top

top

top

建栈

top

top

顶点 4出栈

top

top

顶点 0出栈

Page 114: 第八章 图

拓扑排序时入度为零的顶点栈在count[] 中的变化

拓扑排序时入度为零的顶点栈在count[] 中的变化

C0C0 C1C1 C2C2

C3C3 C4C4 C5C5

21-1222

2-1-1221

2-1-122-1

2-1-122-1

0

1

2

3

4

5

0

1

2

3

4

5

0

1

2

3

4

5

0

1

2

3

4

5

top

top

top

top

top

顶点 1出栈

top

顶点 5出栈

顶点 3出栈

顶点 2出栈

Page 115: 第八章 图

拓扑排序的算法template <class T, class E>void TopologicalSort (Graph<T, E>& G) {

int i, j, w, v;int top = -1; // 入度为零顶点的栈初始化int n = G.NumberOfVertices(); // 网络中顶点个数int *count = new int[n]; // 入度数组兼入度为零顶点栈

// AOV 网络的输入以及入度数组的初始化 for (i = 0; i < n; i++) count[i] = 0; cin >> i >> j; // 输入一条边 (i, j) while (i > -1 && i < n && j > -1 && j < n) {

G.insertEdge (i, j); count[j]++;cin >> i >> j;

}

Page 116: 第八章 图

//初始化入度数组 count ;for (i = 0; i < n; i++) //检查网络所有顶点

if (count[i] == 0) // 入度为零的顶点进栈{ count[i] = top; top = i; }

for (i = 0; i < n; i++) // 输出 n 个顶点{

if (top == -1) { // 中途栈空 , 转出cout << “ 网络中有回路! " << endl;return;

}

//选择一个入度为 0 的顶点,输出,并进行相应处理;//代码见下页。。。 。。。

}}

Page 117: 第八章 图

//选择一个入度为 0 的顶点,输出,并进行相应处理的代码;v = top; top = count[top]; //v= 一个入度为 0 的顶

点;

cout << G.getValue(v) << " " << endl; // 输出

// 遍历 v 的所有邻接顶点,将这些顶点的入度减一w = G.GetFirstNeighbor(v); while (w != -1) { //扫描顶点 v 的出边表

count[w]--; // 邻接顶点入度减一if (!count[w]) // 入度减至零则进栈

{ count[w] = top; top = w; }w = G.GetNextNeighbor (v, w);

} // 一个顶点输出后,调整其邻接顶点入度

Page 118: 第八章 图

• 效率分析– 如果 AOV 网络有 n 个顶点, e 条边– 在拓扑排序的过程中,初始化时搜索入度为零的顶点,建立链式栈所需要的时间是 O(n) 。

– 在正常的情况下,有向图有 n 个顶点,每个顶点进一次栈,出一次栈,输出一次。 这些操作的总和是 O(n) 的。

– 顶点入度减一的运算共执行了 e 次。– 所以总的时间复杂度为 O(n+e) 。

Page 119: 第八章 图

用边表示活动的网络 (AOE 网络 )

• AOE ( Activity On Edges ) 网络– 如果在无有向环的带权有向图中 , 用– 有向边表示一个工程中的活动 (Activity) ,– 用边上权值表示活动持续时间 (Duration),

– 用顶点表示事件 (Event) 。• 顶点实际上表示了一种同步行为,只有当进入顶点的边代表的活动都完成后,离开顶点的边所代表的活动才能开始。

Page 120: 第八章 图

• 用源顶点表示整个工程的开始,而汇顶点表示整个过程的结束。(从源顶点可以到达任意顶点,任意顶点都可到达汇顶点)

• AOE 网络可用于工程估算。– 完成整个工程至少需要多少时间 (假设网络中没有

环 )? 即汇点对应的事件最早在何时发生?– 为缩短完成工程所需的时间 , 应当加快哪些活动 ?

• 完成整个工程所需的时间取决于从源点到汇点的最长路径长度(关键路径, Critical Path )

Page 121: 第八章 图

事件的最早可能发生时间• 我们用 Ve(i) 表示顶点 Vi 最早可能发生的时间

其中 dur(<Vj,Vi>) 表示从 j 到 i 的距离,即完成相应活动需要的时间

• 显然, Ve(0)=0;

• 按照拓扑顺序,我们可以逐个顶点地推导出 Ve(i)的值。

• Ve(i) 等于从源点出发,到达 Vi 的最长路径的长度。

)}V,Vdur((j){VMax(i)V ijej

e

Page 122: 第八章 图

事件的最晚允许发生时间• 一个事件的最晚允许发生时间 Vl(i)

– 保证汇点发生时间不至于延后的前提下, Vi 最晚可在何时发生。

• 我们有如下规则

• 对于汇点,最晚时间就等于最早时间• 我们可以按照拓扑顺序的逆序计算出各个 Vl值。

)}V,Vdur((j){VMin(i)V jij

ll

Page 123: 第八章 图

活动开始的最早 / 晚时间• 对于活动 ak=<Vi,Vj> ,

– 最早可能开始时间 Ae(k) 等于 Ve(i)

– 最迟允许开始时间 Al(k) 等于 Vl(j)-dur(<Vi,Vj>)

– 时间余量(松弛时间)为 Al(k)- Ae(k) ,表示 ak

的开始时间变动范围的大小。– 当时间余量为 0 时,表示 ak 必须按时开始、按

时结束,否则会拖后工程进度。也就是说,它就是关键活动。

Page 124: 第八章 图

11

33

22

44

a1=8

a2=12

55

66

77 88a10=12

a9=6

a8=18

a5=28

a6=8

a7=6a3=14

a4=10

1 2 3 4 5 6 7 8

Ve 0 8 12 22 28 40 46 58

Vl 0 8 12 22 28 40 46 58

1 2 3 4 5 6 7 8 9 10

Ae 0 0 8 12 12 22 22 28 40 46

Al 0 0 8 12 12 32 22 28 40 46

Page 125: 第八章 图

求 AOE 网的关键活动//假设结点 0 , 1 , 2 , 3 ,…, n-1 是这个图的// 一个拓扑排序。template <class T, class E>void CriticalPath(graph<T, E>& G) { int i, j, k; E Ae, Al, dur; int n = G.NumberOfVertices();

//初始化,申请 Ve 和 Vl 的空间 E *Ve = new E[n]; E *Vl = new E[n]; for (i = 0; i < n; i++) Ve[i] = 0;

// 所有的 Ve初始化为 0 ,这样无法从//初始结点 0 到达的顶点的最终 Ve 值还是 0 ;

Page 126: 第八章 图

for (i = 0; i < n; i++) { //正向计算 Ve[]j = G.getFirstNeighbor(i);while (j != -1) {

dur = G.getWeight (i, j);if (Ve[i]+dur > Ve[j]) Ve[j] = Ve[i]+dur; j = G.getNextNeighbor(i, j);

}}

//实际上并不直接按照前面关于 Ve 的规则//来计算。因为结点已经拓扑排序,因此在 for语句// 的语句体开始处,顶点 i 的各个直接前驱都已经被// 处理过。而处理这些前驱时,总是计算从这个前//驱到达 i 的路径长度。所以在语句体的开始处,//Ve[i] 已经计算得到。// 这么做是因为取一个顶点的前驱比较麻烦

Page 127: 第八章 图

// 增加了初始化 Vl 的语句!for (i = 0; i < n; i++) Vl[i] = MaxNumber;

Vl[n-1] = Ve[n-1]; //汇点的最晚时间即最早时间for (j = n-2; j > 0; j--) { //逆向计算 Vl[]

k = G.getFirstNeighbor (j); while (k != -1) { // 遍历所有后继顶点

dur = G.getWeight (j, k);if (Vl[k]-dur < Vl[j]) Vl[j] = Vl[k]-dur;k = G.getNextNeighbor (j, k);

}}

Page 128: 第八章 图

// 求关键路径for (i = 0; i < n; i++) { //求各活动的 e, l

j = G.getFirstNeighbor (i);while (j != -1) {// 遍历 i 的所有后继

// 处理从 i 到 j 的边(活动)Ae = Ve[i]; Al = Vl[k]-G.getWeight(i, j);if (Al == Ae)

cout << "<" << G.getValue(i) << ","<< G.getValue(j) << “>” << " 是关键活动 " << endl;

j = G.getNextNeighbor (i, j); } }

delete [] Ve; delete [] Vl;};

Page 129: 第八章 图

注意• 假设所有顶点都按拓扑有序的次序编号。• 仅计算 Ve[i] 和 Vl[i] 是不够的,还须计算

e[k] 和 l[k] 。• 不是任一关键活动加速一定能使整个工程提前。想使整个工程提前,要考虑各个关键路径上所有关键活动。

• 如果任一关键活动延迟,整个工程就要延迟。