算法导论学习笔记14_基本图算法


1. 图的表示

对于一个图 G = ( V , E ) G=(V, E) G=(V,E),通常由两种表示方法:邻接链表邻接矩阵。对于稀疏图(边的条数 ∣ E ∣ |E| E远远小于 ∣ V ∣ 2 |V|^2 V2的图),通常用邻接链表表示,而对于稠密图 ∣ E ∣ |E| E接近 ∣ V ∣ 2 |V|^2 V2),通常采用邻接矩阵的形式表示。

1.1 邻接链表

对于图 G = ( V , E ) G=(V,E) G=(V,E)来说,邻接链表表示由一个包含 ∣ V ∣ |V| V条链表的数组 A d j Adj Adj所构成,每个结点由一条链表。

对于每个结点 u ∈ V u\in V uV,邻接链表 A d j [ u ] Adj[u] Adj[u]包含所有与结点 u u u之间有边相连的结点。

上图左侧为一个无向图,由五个顶点和7条边构成。其邻接链表的表示方式如右图所示,该邻接链表由5个链表组成,每个链表代表一个顶点,链表中的结点表示与该顶点相连接的各个顶点。以顶点2为例,与之相连的顶点有1、5、4、3,因此链表 A d j [ 2 ] Adj[2] Adj[2]中有四个结点,它们以链表的形式连接。

注意:当用邻接链表存储无向图时,所有邻接链表的长度之和为为 2 ∣ E ∣ 2|E| 2E;用邻接链表存储有向图时,所有邻接链表的长度之和为 ∣ E ∣ |E| E。因此,邻接链表表示法的存储空间需求为 Θ ( V + E ) \Theta(V+E) Θ(V+E)

对邻接链表稍加修改,即可用来表示权重图。设 G = ( V , E ) G=(V, E) G=(V,E)为一个权重图,其权重函数为 ω \omega ω,可以直接将边 ( u , v ) ∈ E (u, v)\in E (u,v)E的权重值 ω ( u , v ) \omega(u, v) ω(u,v)存放在结点 u u u的邻接链表里。

邻接链表的一个潜在缺陷是无法快速判断一条边 ( u , v ) (u, v) (u,v)是否为图中的一条边,唯一的方法是在邻接链表 A d j [ u ] Adj[u] Adj[u]里面搜索结点 v v v。邻接矩阵表示则克服了上述缺陷。

1.2 邻接矩阵

对于邻接矩阵表示来说,通常将图 G G G中的结点编为 1 , 2 , . . . , ∣ V ∣ 1, 2, ..., |V| 1,2,...,V,这种编号可以是任意的。在进行上述编号之后,图 G G G的邻接矩阵表示由一个 ∣ V ∣ × ∣ V ∣ |V|×|V| V×V的矩阵 A = ( a i j ) A=(a_{ij}) A=(aij)实现,该矩阵满足 a i j = { 1 ( i , j ) ∈ E 0 o t h e r s a_{ij} = \begin{cases}1&(i,j)\in E\\0 &others\end{cases} aij={10(i,j)Eothers 下图给出了一个邻接矩阵表示无向图的例子:

从上图中可以看出,无向图的邻接矩阵就是一个对称矩阵,即 A T = A A^T=A AT=A

同样的,邻接矩阵也可以用来表示权重图。例如,如果 G = ( V , E ) G=(V, E) G=(V,E)为一个权重图,其权重函数为 ω \omega ω,则我们直接将边 ( u , v ) ∈ E (u, v)\in E (u,v)E的权重 ω ( u , v ) \omega(u, v) ω(u,v)存放在邻接矩阵中的第 u u u行第 v v v列记录上。对于不存在的边,通常用 0 0 0 ∞ \infty 来表示。

2. 广度优先搜索(BFS)

广度优先搜索是最简单的图搜索算法之一,也是许多图算法的原型。

给定图 G = ( V , E ) G=(V, E) G=(V,E)和一个可以识别的源结点 s s s,广度优先搜索对图 G G G中的边进行系统性的搜索来发现可以从源结点 s s s到达的所有结点。该算法能够计算从源结点 s s s到每个可到达的结点的距离,同时生成一棵“广度优先生成树”。

对于每个从源结点 s s s可以到达的结点 v v v,在广度优先搜索树里从结点 s s s到结点 v v v的简单路径所对应的就是图 G G G中从结点 s s s到结点 v v v的“最短路径”。

下面是广度优先搜索的伪代码:

BFS(G, s)
	for each vertex u ∈ G.V - {s}
		u.color = WHITE
		u.d = ∞
		u.π = NIL
	s.color = GRAY
	s.d = 0
	s.π = NIL
	Q =ENQUEUE(Q, s)
	while Q ≠ ∅
		u = DEQUEUE(Q)
		for each v ∈ G.Adj[u]
			if  v.color == WHITE
				v.color = GRAY
				v.d = u.d + 1
				v.π = u
				ENQUEUE(Q, v)
		u.color = BLACK;

上述代码中,为了显示BFS的进度,用黑、白、灰三种颜色对结点进行标记,其中黑色表示已经被搜索过的结点,灰色表示被发现但未被搜索的结点,白色表示未被搜索或发现的结点。

此外,伪代码中的d属性表示在结点在广度优先搜索树中的深度;π属性是结点 v v v的前驱 u u u

下面是一个具有8个顶点的无向图,其邻接链表表示如右图所示:

BFS的过程如下图所示:

BSF初始化时先将所有结点标记为白色,将初始结点 s s s标记为灰色并加入队列中。算法第11-19行的while循环一直执行到不再有灰色结点时结束。灰色结点指的是已被发现的结点,但其邻接链表尚未被完全检查。

从上述过程可以看出,从结点 s s s对该无向图做广度优先搜索的顺序为: s − w − r − t − x − v − u − y s-w-r-t-x-v-u-y swrtxvuy

前驱子图:对于图 G = ( V , E ) G=(V, E) G=(V,E)和源结点 s s s,定义图 G G G的前驱子图为 G π = ( V π , E π ) G_\pi=(V_\pi, E_\pi) Gπ=(Vπ,Eπ),其中 V π = { v ∈ V : v . π ≠ N I L } ∪ { s } , E π = { ( v . π , v ) : v ∈ V π − { s } } V_\pi=\{v\in V:v.\pi\neq NIL\}\cup\{s\},E_\pi=\{(v.\pi, v):v\in V_\pi-\{s\}\} Vπ={vV:v.π̸=NIL}{s},Eπ={(v.π,v):vVπ{s}}

广度优先树:如果 V π V_\pi Vπ由从源结点 s s s可以到达的结点组成,并且对于所有的 v ∈ V π v\in V_\pi vVπ,子图 G π G_\pi Gπ含一条从源结点 s s s到结点 v v v的唯一简单路径,且该路径也是图 G G G里面从源结点 s s s到结点 v v v之间的一条最短路径,则前驱子图 G π G_\pi Gπ是一棵广度优先树。

3. 深度优先搜索(DFS)

深度优先搜索总是对最近才发现的结点 v v v的出发边进行探索,直到该结点的所有出发边都被发现为止。一旦结点 v v v的所有出发边都被发现,搜索则“回溯”到 v v v的前驱结点,来搜索该前驱结点的出发边。该过程一直持续到从源结点可以达到的所有结点都被发现为止。如果还存在尚未发现的结点,则深度优先搜索将从这些未被发现的结点中任选一个作为新的源结点,并重复同样的过程。

与广度优先搜索不同的是,广度优先搜索的前驱子图形成一棵树,而深度优先搜索的前驱子图可能由多棵树组成,因为搜索可能从多个源结点重复进行。

深度优先搜索的前驱子图:设图 G π = ( V , E π ) G_\pi = (V, E_\pi) Gπ=(V,Eπ),其中 E π = ( v . π , v ) : v ∈ V 且 v . π ≠ N I L E_\pi={(v.\pi, v): v\in V且v.\pi \neq NIL} Eπ=(v.π,v):vVv.π̸=NIL

深度优先搜索的前驱子图形成一个由多棵深度优先树构成的深度优先森林

像广度优先搜索算法一样,深度优先搜索算法在搜索过程中也是对结点进行涂色来指明结点的状态。每个结点的初始颜色都是白色,在结点被发现后变为灰色,在其邻接链表被扫描完成后变为黑色。此策略可以使得每个结点仅在一棵深度优先树中出现,因此所有的深度优先搜索树是不想交的。

除了创建一个深度优先搜索树外,深度优先搜索算法还在每个结点盖上一个时间戳,每个结点 v v v有两个时间戳:第一个时间戳 v . d v.d v.d记录结点 v v v第一次被发现的时间;第二个时间戳 v . f v.f v.f记录搜索完成对 v v v的邻接链表扫描的时间。

下面的伪代码是基本的深度优先搜索算法,变量 t i m e time time是一个全局变量,用来计算时间戳。

DFS(G)
	for each vertex u ∈ G.V
		u.color = WHITE
		u.π = NIL
	time = 0
	for each vertex u ∈ G.V
		if u.color == WHITE
			DFS_VISIT(G, u)
DFS_VISIT(G, u)
	time = time + 1
	u.d = time
	u.color = GRAY
	for each v ∈ G:Adj[u]
		if v.color == WHITE
			v.π = u
			DFS_VISIT(G, v)
	u.color = BLACK
	time = time + 1
	u.f = time

注:深度优先搜索算法的运行时间为 Θ ( V + E ) \Theta(V+E) Θ(V+E)

括号化定理:在对有向或无向图 G = ( V , E ) G=(V, E) G=(V,E)进行的任意深度优先搜索中,对于任意两个结点 u u u v v v来说,下面三种情况只有一种成立:

  • 区间 [ u . d ,   u . f ] [u.d,\space u.f] [u.d, u.f]和区间 [ v . d ,   v . f ] [v.d,\space v.f] [v.d, v.f]完全分离,在深度优先森林中,结点 u u u不是结点 v v v的后代,结点 v v v也不是结点 u u u的后代。
  • 区间 [ u . d ,   u . f ] [u.d,\space u.f] [u.d, u.f]完全包含在 [ v . d ,   v . f ] [v.d,\space v.f] [v.d, v.f]内,在深度优先树中,结点 u u u是结点 v v v的后代。
  • 区间 [ v . d ,   v . f ] [v.d,\space v.f] [v.d, v.f]完全包含在 [ u . d ,   u . f ] [u.d, \space u.f] [u.d, u.f]内,在深度优先树中,结点 v v v是结点 u u u的后代。

后代区间的嵌套:在有向或无向图 G G G的深度优先森林中,结点 v v v是结点 u u u的真后代当且仅当 u . d &lt; v . d &lt; v . f &lt; u . f u.d &lt; v.d &lt; v.f &lt; u.f u.d<v.d<v.f<u.f成立。

白色路径定理:在有向或无向图 G = ( V , E ) G = (V, E) G=(V,E)的深度优先森林中,结点 v v v是结点 u u u的后代当且仅当在发现结点 u u u的时间 u . d u.d u.d,存在一条从结点 u u u到结点 v v v的全部由白色结点所构成的路径。

对于在图 G G G上运行深度优先搜索算法所生成的深度优先森林 G π G_\pi Gπ,我们可以定义4中边的类型:

  1. 树边:为深度优先森林 G π G_\pi Gπ中的边。如果结点 v v v是因算法对边 ( u , v ) (u, v) (u,v)的探索而首先被发现,则 ( u , v ) (u, v) (u,v)是一条树边。
  2. 后向边:后向边 ( u , v ) (u, v) (u,v)是将结点 u u u连接到其在深度优先树中(一个)祖先结点 v v v的边。由于有向图中可以有自循环,自循环也被认为是后向边。
  3. 前向边:是将结点 u u u连接到其在深度优先树中一个后代结点 v v v的边 ( u , v ) (u, v) (u,v)
  4. 横向边:指其他的边。这些边可以连接同一棵深度优先树中的结点,只要其中一个结点不是另外一个结点的祖先,也可以连接不同深度优先树中的两个结点。

在遇到某些边时,DFS有足够的信息来对这些边进行分类。当第一次探索边 ( u , v ) (u, v) (u,v)时,结点 v v v的颜色能够告诉我们关于该条边的一些信息。

  1. 结点 v v v为白色表明该条边 ( u , v ) (u, v) (u,v)是一条树边。
  2. 结点 v v v为灰色表明该条边 ( u , v ) (u, v) (u,v)是一条后向边。
  3. 结点 v v v为黑色表明该条边 ( u , v ) (u, v) (u,v)是一条前向边或横向边。

4. 拓扑排序

对于一个有向无环图 G = ( V , E ) G=(V, E) G=(V,E)来说,其拓扑排序 G G G中所有结点的一种线性次序,该次序满足条件:如果图 G G G包含边 ( u , v ) (u, v) (u,v),则结点 u u u在拓扑排序中处于结点 v v v的前面(如果图 G G G包含环路,则不可能排出一个线性次序)。

图(a)中所示的有向无环图中,有向边 ( u , v ) (u, v) (u,v)表明服装 u u u必须在服装 v v v之前穿上。对该有向无环图进行拓扑排序所获得的就是一种合理穿衣的次序。

图(b)将拓扑排序后的有向无环图在一条水平线上展示出来,在该水平线上,所有的有向边都从左指向右。

下面是对一个有向无环图进行拓扑排序的伪代码:

TOPOLOGICAL_SORT(G)
	call DFS(G) to compute finishing time v.f for each vertex v
	as each vertex is finished, insert it onto the front of a linked list
	return the linked list of vertices

上述代码可以在 Θ ( V + E ) \Theta(V+E) Θ(V+E)的时间内完成拓扑排序,因为深度优先搜索算法的运行时间为 Θ ( V + E ) \Theta(V+E) Θ(V+E),将结点插入链表最前端所需的时间为 O ( 1 ) O(1) O(1),一共有 ∣ v ∣ |v| v个结点需要插入。

5. 强连通分量

有向图 G = ( V , E ) G=(V, E) G=(V,E)强连通分量是一个最大结点集合 C ⊆ V C\subseteq V CV,对于该集合中的任意一对结点 u u u v v v来说,路径 u → v u\rightarrow v uv和路径 v → u v\rightarrow u vu同时存在;也就是说,结点 u u u和结点 v v v可以相互到达。

从上图可以看出,有向图 G = ( V , E ) G=(V, E) G=(V,E)有四个强连通分量,分别图中四个阴影部分区域。

对图 G = ( V , E ) G=(V, E) G=(V,E)的强连通分量进行收缩可以得到无环分量图 G S C C G^{SCC} GSCC,这种收缩将每个强连通分量收缩为一个结点,即由一个结点来替换整个连通分量。

为了求得图 G = ( V , E ) G=(V, E) G=(V,E)的强连通分量,需要对其进行转置。定义转置为 G T = ( V , E T ) G^T = (V, E^T) GT=(V,ET),这里的 E T = { ( u , v ) : ( v , u ) ∈ E } E^T=\{(u, v):(v, u)\in E\} ET={(u,v):(v,u)E},也就是说, E T E^T ET由对图 G G G中的边进行反向而获得。下图是对图 G G G求转置的结果:

值得注意的是,图 G = ( V , E ) G=(V, E) G=(V,E) G T = ( V , E T ) G^T = (V, E^T) GT=(V,ET)具有相同的强连通分量。

下面是线性时间(即 Θ ( V + E ) \Theta(V+E) Θ(V+E)时间)算法使用两次深度优先搜索来计算有向图 G = ( V , E ) G=(V, E) G=(V,E)的强连通分量,其中第一次深度优先搜索运行在图 G G G上,另一次运行在转置图 G T G^T GT上。

STRONGLY-CONNECTED_COMPONENTS(G)
	call DFS(G) to compute finishing times u.f for each vertex u
	compute G.T
	call DFS(G.T), but in the main loop of DFS, consider the vertices in order of decreasing u.f (as computed in line 1)
	output the vertices of each tree in the depth-first forest formed in line 3 as a separate strongly connected component

上述性质依赖于如下关键性质:假定图 G G G有强连通分量 C 1 , C 2 , . . . , C k C_1, C_2, ..., C_k C1,C2,...,Ck。结点集 V S C C V^{SCC} VSCC { v 1 , v 2 , . . . , v k } \{v_1, v_2, ..., v_k\} {v1,v2,...,vk},对于图 G G G的每个强连通分量 C i C_i Ci来说,该集合包含代表该分量的结点 v i v_i vi。如果对于某个 x ∈ C i x\in C_i xCi y ∈ C j y\in C_j yCj,图 G G G包含一条有向边 ( x , y ) (x, y) (x,y),则边 ( v i , v j ) ∈ E S C C (v_i, v_j)\in E^{SCC} (vi,vj)ESCC。从另一个角度来看,通过收缩所有相邻结点都在同一个强连通分量中的边,剩下的图就是 G S C C G^{SCC} GSCC

6. 附录(代码)

6.1 广度优先搜索

#include <iostream>
#include <vector>
#include <list>
#include <utility>
#include <unordered_map>
#include <queue>
using namespace std;
enum COLOR {
	WHITE, GRAY, BLACK
};
struct vertex {
	char id;
	int n;
	int d;
	int f;
	vertex* p;
	COLOR color = WHITE;
	vertex(char ID, int N) :id(ID), n(N) { };
};

class DFS {
private:
	int time;
	vector<vertex*> V;
	vector<list<vertex*>> Adj;
	vertex* find(char ch);
	void dfs_visit(vertex* u);
public:
	void graph_init(vector<pair<char, vector<char>>> data);
	void dfs();
};
vertex* DFS::find(char ch) {
	for (vertex* v : V) {
		if (v->id == ch) return v;
	}
}
void  DFS::graph_init(vector<pair<char, vector<char>>> data) {
	int n = data.size();
	for (int i = 0; i < n; i++) {
		vertex* p = new vertex(data[i].first, i);
		V.push_back(p);
	}
	for (int i = 0; i < n; i++) {
		list<vertex*> list1;
		for (char ch : data[i].second) {
			list1.push_back(find(ch));
		}
		Adj.push_back(list1);
	}
}
void DFS::dfs_visit(vertex* u) {
	time = time + 1;
	u->d = time;
	u->color = GRAY;
	for (vertex* v : Adj[u->n]) {
		if (v->color == WHITE) {
			v->p = u;
			dfs_visit(v);
		}
	}
	u->color = BLACK;
	cout << u->id << " ";
	time = time + 1;
	u->f = time;
}
void DFS::dfs() {
	for (vertex* u : V) {
		u->color = WHITE;
		u->p = nullptr;
	}
	time = 0;
	for (vertex* u : V) {
		if (u->color == WHITE)
			dfs_visit(u);
	}
}
int main(int argc, char* argv[]) {
	vector<pair<char, vector<char>>> data = { {'r', {'s', 'v'}},
											  {'s', {'w', 'r'}},
											  {'v', {'r'}},
											  {'w', {'s', 't', 'x'}},
											  {'t', {'w', 'x', 'u'}},
											  {'u', {'t', 'x', 'y'}},
											  {'x', {'w', 't', 'u', 'y'}},
											  {'y', {'x', 'u'}} };
	DFS D;
	D.graph_init(data);
	D.dfs();
	return 0;
}

6.2 深度优先搜索

#include <iostream>
#include <vector>
#include <list>
#include <utility>
#include <unordered_map>
#include <queue>
using namespace std;
enum COLOR {
	WHITE, GRAY, BLACK
};
struct vertex {
	char id;
	int n;
	COLOR color = WHITE;
	int d;
	vertex* p;
	vertex(char ID, int N) :id(ID),n(N) { };
};

class BFS {
private:
	vector<vertex*> V;
	vector<list<vertex*>> Adj;
	vertex* find(char ch);
public:
	void graph_init(vector<pair<char, vector<char>>> data);
	void bfs(char s);
};
vertex* BFS::find(char ch) {
	for (vertex* v : V) {
		if (v->id == ch) return v;
	}
}
void  BFS::graph_init(vector<pair<char, vector<char>>> data) {
	int n = data.size();
	for (int i = 0; i < n; i++) {
		vertex* p = new vertex(data[i].first, i);
		V.push_back(p);
	}
	for (int i = 0; i < n; i++) {
		list<vertex*> list1;
		for (char ch : data[i].second) {
			list1.push_back(find(ch));
		}
		Adj.push_back(list1);
	}
}
void BFS::bfs(char ch) {
	for (auto u : V) {
		u->color = WHITE;
		u->d = 0x7fffffff;
		u->p = nullptr;
	}
	vertex* s = find(ch);
	s->color = GRAY;
	s->d = 0;
	s->p = nullptr;
	queue<vertex*> Q;
	Q.push(s);
	while (!Q.empty()) {
		vertex* u = Q.front();Q.pop();
		for (vertex* v : Adj[u->n]) {
			if (v->color == WHITE) {
				v->color = GRAY;
				v->d = u->d + 1;
				v->p = u;
				Q.push(v);
			}
		}
		u->color = BLACK;
		cout << u->id << " ";
	}

}
int main(int argc, char* argv[]) {
	vector<pair<char, vector<char>>> data = { {'r', {'s', 'v'}},
											  {'s', {'w', 'r'}},
											  {'v', {'r'}},
											  {'w', {'s', 't', 'x'}},
											  {'t', {'w', 'x', 'u'}},
	 										  {'u', {'t', 'x', 'y'}},
											  {'x', {'w', 't', 'u', 'y'}},
											  {'y', {'x', 'u'}} };
	BFS B;
	B.graph_init(data);
	B.bfs('s');
	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值