在很多计算机应用中,由相连的节点所表示的模型起到了关键作用。为了描述这些问题,我们要使用一种抽象的数学对象,叫做图。
图论作为数学领域中的一个重要分支已经有数百年的历史了,关于图的算法研究相对来说才开始不久。尽管有些基础的算法在几个世纪前就已经发现了,但大多数有趣的结论都是近几十年才被发现。
图论有着广泛的应用,比如地图、计算机网络、电路、任务调度、社交网络等等。
术语表
在无向图中,边仅仅是两个顶点之间的连接。
图是由一组顶点和一组能够将两个顶点相连的边组成的
我们使用0至V-1来表示一张含有V个顶点的图中各个顶点,这样是为了方便使用数组索引来编写能够高效访问各个顶点中信息的代码。
特殊的图
自环:即一条连接一个顶点和其自身的边
平行边:连接同一对顶点的多条边
如果从任意一个顶点都存在一条路径到达另一个任意顶点,我们称这幅图是连通图。一幅非连通的图由若干连通的部分组成,它们都是极大连通子图。一般来说,要处理一张图就要一个个地处理它的连通分量。树是一幅无环连通图。
树的定义非常有用,稍作改动就可以变成用来描述程序行为的(函数调用层次)模型和数据结构(二叉查找树)。
当且仅当一幅含有V个顶点的图G满足下列5个条件之一是,它就是一棵树:
- G有V-1条边且不含环
- G有V-1条边且是连通的
- G是连通的,但删除任意一条边都会使它不再连通
- G是无环的,但添加任意一条边都会产生一条环
- G中任意一对顶点之间仅存在一条简单路径
图的密度是指已经连接的顶点对占所有可能被连接的顶点对的比例。在稀疏图中,被连接的顶点对很少,而在稠密图中,只有少部分定点对之间没有边连接。
二分图是一种能够将所有节点分为两部分的图,其中图的每条边所连接的两个顶点都分别属于不同的部分。
表示无向图的数据类型
数据结构需要有以下两种要求:
- 它必须为可能在应用中碰到的各种类型的图预留出足够的空间
- Graph的实例方法的实现一定要快
邻接矩阵:邻接矩阵对内存的要求太高,不满足第一个条件。
事实上,我们可以使用邻接表数组,我们可以使用一个以顶点为索引的列表数组,其中每个元素都是和该顶点相邻的顶点列表。
数据结构使用的空间和V+E成正比
添加一条边所需的时间为常数
遍历顶点V的所有相邻顶点所需的时间和V的度数成正比
多个不同的邻接表可能表示同一幅图
class Graph
{
private:
const int V; // vertex num
int E = 0; // edge num
std::vector<std::vector<int>> adj;
public:
Graph(int V) : V(V) { adj.resize(V); }
int getV() { return V; }
int getE() { return E; }
void addEdge(int v, int w) { adj[v].push_back(w); adj[w].push_back(v); E++;}
std::vector<int> getAdj(int v) { return adj[v]; }
auto getAdjs() { return adj; }
};
深度优先搜索
要搜索一幅图,只需用一个递归方法来遍历所有顶点,在访问其中一个顶点时:
- 将它标记为已访问
- 递归的访问它的所有没有被标记过的邻居顶点
在图中,我们会路过每条边两次(在它的两个端点各一次),这意味着深度优先搜索的轨迹可能会比预想的长一倍。标记数组防止了死循环。
算法遍历边和访问顶点的顺序与图的表示是有关的,而不只是与图的结构或是算法有关。
class DepthFirstSearch
{
private:
std::vector<bool> marked = { false };
int count = 0;
void dfs(Graph G, int v) {
marked[v] = true;
count++;
for (auto w : G.getAdj(v))
if (!marked[w])
dfs(G, w);
}
public:
DepthFirstSearch(Graph G, int s) { marked.resize(G.getV()); dfs(G, s); }
bool getMarked(int w) { return marked[w]; }
int getCount() { return count; }
};
深度优先搜索能有效处理许多和图有关的任务,例如“判断两个顶点的连通性”和“计算图中连通子图的个数”。
我们可以使用深度优先搜索来寻找路径。
class DepthFirstPaths
{
private:
std::vector<bool> marked = { false };
std::vector<int> edgeTo;
int s;
void dfs(Graph G, int v) {
marked[v] = true;
for (auto w : G.getAdj(v))
if (!marked[w])
{
edgeTo[w] = v;
dfs(G, w);
}
}
public:
DepthFirstPaths(Graph G, int s) : s(s) { marked.resize(G.getV()); edgeTo.resize(G.getV()); dfs(G, s); }
bool hasPathTo(int v) { return marked[v]; }
std::stack<int> pathTo(int v) {
if (!hasPathTo(v))
return {};
std::stack<int> path;
for (int x = v; x != s; x = edgeTo[x])
path.push(x);
path.push(s);
return path;
}
};
深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比
使用深度优先搜索得到从给定起点到任意标记顶点的路径所需的时间与路径的长度成正比
广度优先搜索
深度优先搜索无法解决单点最短路径,但是广度优先搜索可以。
在搜索一幅图时遇到有多条边需要遍历的情况时,我们会选择其中一条并将其他通道留到以后再继续搜索。在深度优先搜索中,我们使用一个可以下压的栈(这是由系统管理的,以支持递归搜索方法),在广度优先搜索中,使用队列来代替栈即可。
首先,先将起点加入队列,然后重复一下步骤直至队列为空:
- 取队列的下一个顶点v并标记它
- 将与v相邻的所有未被标记过的顶点加入队列
class BreadthFirstPaths
{
private:
std::vector<bool> marked = { false };
std::vector<int> edgeTo;
int s;
void bfs(Graph G, int s) {
std::queue<int> q;
marked[s] = true;
q.push(s);
while (!q.empty())
{
int v = q.front();
q.pop();
for (auto w : G.getAdj(v))
if(!marked[w])
{
edgeTo[w] = v;
marked[w] = true;
q.push(w);
}
}
}
public:
BreadthFirstPaths(Graph G, int s) : s(s) { marked.resize(G.getV()); edgeTo.resize(G.getV()); bfs(G, s); }
bool hasPathTo(int v) { return marked[v]; }
std::stack<int> pathTo(int v) {
if (!hasPathTo(v))
return {};
std::stack<int> path;
for (int x = v; x != s; x = edgeTo[x])
path.push(x);
path.push(s);
return path;
}
};
对于从s可达的任意顶点v,广度优先搜索都能找到一条从s到v的最短路径
广度优先搜索所需的时间在最坏情况下和V+E成正比
在搜索中,我们都会先将起点存入数据结构中,然后重复以下步骤直到数据结构被清空
- 取其中的下一个顶点并标记它
- 将v的所有相邻而又未被标记的顶点加入数据结构
这两种算法的不同之处仅在于从数据结构中获取下一个顶点的规则。
连通分量
深度优先搜索和广度优先搜索还可以用来找出一幅图的连通分量。
class CC
{
private:
std::vector<bool> marked = { false };
std::vector<int> id;
int count = 0;
void dfs(Graph G, int v) {
marked[v] = true;
id[v] = count;
for (auto w : G.getAdj(v))
if(!marked[w])
dfs(G, w);
}
public:
CC(Graph G) {
marked.resize(G.getV());
id.resize(G.getV());
for (int s = 0; s < G.getV(); s++)
if (!marked[s])
{
dfs(G, s);
count++;
}
}
bool connected(int v, int w) { return id[v] == id[w]; }
int getId(int v) { return id[v]; }
int getCount() { return count; }
};
深度优先搜索的预处理使用的时间和空间与V+E成正比,且可以在常数时间内处理关于图的连通性查询。
下一篇介绍有向图。