在有向图中,边是单向的:每条边所连接的两个顶点都是一个有序对,它们的邻接性是单向的。
术语
一幅有方向的图(或有向图)是由一组顶点和一组有方向的边组成的,每条有方向的边都连接着有序的一对顶点
处理有向图就如同在一座只有单行道的城市中穿梭,而且这些单行道的方向是杂乱无章的。
我们使用邻接表来表示有向图。
有向图取反的操作很有用,它返回该有向图的一个副本,但将其中所有的边反转。这样用例就可以找出“指向”每个顶点的所有边。
class Digraph
{
private:
int V;
int E = 0;
std::vector<std::vector<int>> adj;
public:
Digraph(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); E++; }
auto getAdj(int v) { return adj[v]; }
auto getAdjs() { return adj; }
Digraph reverse() {
Digraph R(V);
for (int v = 0; v < V; v++)
for (int w : getAdj(v))
R.addEdge(w, v);
return R;
}
};
有向图的可达性
单点可达性:给定一幅有向图和一个起点 s,回答“是否存在一条从 s 到达给定顶点 v 的有向路径?”
在有向图中,深度优先搜索标记由一个集合的顶点可达的所有顶点所需的时间与被标记的所有顶点的出度之和成正比。
class DirectedDFS
{
private:
std::vector<bool> marked = { false };
void dfs(Digraph G, int v) {
marked[v] = true;
for (auto w : G.getAdj(v))
if (!marked[w])
dfs(G, w);
}
public:
DirectedDFS(Digraph G, int s) { marked.resize(G.getV()); dfs(G, s); }
bool getMarked(int v) { return marked[v]; }
};
单点最短有向路径
给定一幅有向图和一个起点 s,类似于无向图,我们可以使用广度优先搜索算法来回答”从 s 到给定目的顶点 v 是否存在一条有向路径?如果有找出其中最短的那条“
class DirectedBFS
{
private:
std::vector<bool> marked = { false };
std::vector<int> edgeTo;
int s;
void bfs(Digraph G, int s) {
std::queue<int> q;
q.push(s);
marked[s] = true;
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:
DirectedBFS(Digraph 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;
}
};
环和有向无环图
在和有向图相关的实际应用中,有向环特别的重要。从原则上来说,一幅有向图可能含有大量的环,在实际应用中,我们只想知道它们是否存在。
有向无环图(DAG)就是一幅不含有向环的有向图
因此,解决有向环检测的问题就可以回答下面这个问题:一幅有向图是有向无环图吗?基于深度优先搜索来解决这个问题并不困难,因为由系统维护的递归调用的栈表示的正是当前正在遍历的有向路径。
class DirectedCycle
{
private:
std::vector<bool> marked = { false };
std::vector<int> edgeTo;
std::stack<int> cycle;
std::vector<bool> onStack = { false };
void dfs(Digraph G, int v) {
onStack[v] = true;
marked[v] = true;
for (auto w : G.getAdj(v))
if (hasCycle())
return;
else if (!marked[w])
{
edgeTo[w] = v;
dfs(G, w);
}
else if (onStack[w])
{
for (int x = v; x != w; x = edgeTo[x])
cycle.push(x);
cycle.push(w);
cycle.push(v);
}
onStack[v] = false;
}
public:
DirectedCycle(Digraph G) {
onStack.resize(G.getV());
edgeTo.resize(G.getV());
marked.resize(G.getV());
for (int v = 0; v < G.getV(); v++)
if (!marked[v])
dfs(G, v);
}
bool hasCycle() { return !cycle.empty(); }
auto getCycle() { return cycle; }
};
当且仅当一幅有向图是无环图时它才能进行拓扑排序
事实上,我们我们已经见过一种拓扑排序的算法:只要添加一行代码,标准深度优先搜索程序就能完成这项任务!它的基本思想是深度优先搜索正好只会访问每个顶点一次。如果将 dfs() 的参数顶点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。在典型的应用中,人们感兴趣的是顶点的以下3中排列顺序。
- 前序:在递归调用之前将顶点加入队列
- 后序:在递归调用之后将顶点加入队列
- 逆后序:在递归调用之后将顶点压入栈
class DepthFirstOrder
{
private:
std::vector<bool> marked = { false };
std::queue<int> pre;
std::queue<int> post;
std::stack<int> reversePost;
void dfs(Digraph G, int v) {
pre.push(v);
marked[v] = true;
for (int w : G.getAdj(v))
if (!marked[w])
dfs(G, w);
post.push(v);
reversePost.push(v);
}
public:
DepthFirstOrder(Digraph G) {
marked.resize(G.getV());
for (int v = 0; v < G.getV(); v++)
if (!marked[v])
dfs(G, v);
}
auto getPre() { return pre; }
auto getPost() { return post; }
auto getReversePost() { return reversePost; }
};
一幅有向无环图的拓扑排序即为所有顶点的逆后序排列
class Topological
{
private:
std::stack<int> order;
public:
Topological(Digraph G) {
DirectedCycle cy(G);
if (!cy.hasCycle())
{
DepthFirstOrder dfo(G);
order = dfo.getReversePost();
}
}
auto getOrder() { return order; }
bool isDAG() { return !order.empty(); }
};
使用深度优先搜索对有向无环图进行拓扑排序所需的时间和 V+E 成正比
在实际应用中,拓扑排序和有向环的检测总会一起出现,因为有向环的检测是排序的前提。
有向图中的强连通性
如果两个顶点 v 和 w 是互相可达的,则称它们为强连通的
如果一幅有向图中的任意两个顶点都是强连通的,则称这副有向图也是强连通的
两个顶点是强连通的当且仅当它们都在一个普通的有向环中
和无向图中的连通性一样,有向图的强连通性也是一种顶点之间的等价关系(自反性、对称性、传递性),强连通性将所有顶点分为了一些等价类,每个等价类都是由相互均为强连通的顶点的最大子集组成的。我们将这些子集称为强连通分量。
一个强连通图只含有一个强连通分量
一个有向无环图中则含有 V 个强连通分量
使用 Kosaraju 算法来计算强连通分量,该算法能回答“给定的两个顶点是强连通的吗?”和“这幅有向图中含有多少个强连通分量”等类似问题。
class KosarajuSCC
{
private:
std::vector<bool> marked = { false };
std::vector<int> id;
int count = 0;
void dfs(Digraph G, int v) {
marked[v] = true;
id[v] = count;
for (auto w : G.getAdj(v))
if (!marked[w])
dfs(G, w);
}
public:
KosarajuSCC(Digraph G) {
marked.resize(G.getV());
id.resize(G.getV());
DepthFirstOrder dfo(G.reverse());
auto order = dfo.getReversePost();
while (!order.empty())
{
auto t = order.top();
if (!marked[t])
{
dfs(G, t);
count++;
}
order.pop();
}
}
bool isStronglyConnected(int v, int w) { return id[v] == id[w]; }
int getId(int v) { return id[v]; }
int getCount() { return count; }
};
Kosaraju 算法的预处理所需的时间和空间与 V+E 成正比,且支持常数时间的有向图强连通性的查询