有向图

Introduction

就是边是有方向的,像单行道那样,也有很多典型的应用。

点的出度指从这个点发出的边的数目,入度是指向点的边数。当存在一条从点 v 到点 w 的路径时,称点 v 能够到达点 w ,但要注意这并不意味着点 w 可以到达点 v 。

Digraph API

先给出表示有向图的 API 以及简单的测试用例,booksite-4.2 上可以找到完整的。

API

Sample Client

运行示例

仍然使用邻接表来实现有向图,比无向图还简单其实。

Adjacency-list

Java Implementation

public class Digraph {
private final int V;
private final Bag<Integer>[] adj; // adjacency lists public Digraph(int V) {
this.V = V;
// create empty graph with V vertices
adj = (Bag<Integer>[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<Integer>();
}
} // add edge v->w
public void addEdge(int v, int w) {
adj[v].add(w);
} // iterator for vertices pointing from v
public Iterable<Integer> adj(int v) {
return adj[v];
}
}

用邻接表来表示有向图,类似的内存使用正比于 E+V ,常数时间就能加入新边,判断点 v 到 w 是否有条边需要正比于点 v 出度的时间,遍历从点 v 发出的边也是。

Digraph Search

同样的,可以直接用 Undirected Graphs 提到的 DFS 和 BFS 这两种搜索策略。

DFS

public class DirectedDFS {
private boolean[] marked; // true if path from s // constructor marks vertices reachable from s
public DirectedDFS(Digraph G, int s) {
marked = new boolean[G.V()];
dfs(G, s);
} // recursive DFS does the work
private void dfs(Digraph G, int v) {
marked[v] = true;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G, w);
}
}
} // client can ask whether any vertex is reachable from s
public boolean visited(int v) {
return marked[v];
}
}

示例

BFS

随便来张图感受一下。

Topological Sort

拓扑排序。就是把有向图整理成箭头都朝同一个方向,像把下面的中间变成右边那种。应用也很广泛啦,比如说大学里某些课要上先修课才行,拓扑排序就可以帮我们安排课程顺序。

另外,拓扑排序是针对有向无环图(DAG, directed acyclic graph)来说的,设想 a 的完成依赖于 b,b 的完成又依赖于 a ,显然没有解。

Solution

DFS 稍作修改,就可以帮我们完成拓扑排序。因为 DFS 正好只会访问每个顶点一次,如果将 dfs() 参数之一的点保存在一个数据结构中,遍历这个数据结构实际上就能访问图中的所有顶点,遍历的顺序取决于这个数据结构的性质以及是在递归调用之前还是之后进行保存。在典型的应用中,人们感兴趣的是点的以下 3 种排列顺序。

  • 前序(Preorder):在递归调用之前将点加入队列。
  • 后序(Postorder):在递归调用之后将点加入队列。
  • 逆后序(Reverse postorder):在递归调用之后将点压入栈。

前序就是 dfs() 的调用顺序,后序就是点遍历完成的顺序,而逆后序就是拓扑排序。

样图

模拟

前序和后序看上图感受一下,对于逆后序就是拓扑排序可以这么想:对于任意边 v->w,在调用 dfs(v) 之时,可能的情况有三种。

  • dfs(w) 已经被调用过且返回了(w 已经被标记)。
  • dfs(w) 还没有被调用(w 还未被标记),因此 v->w 会直接或间接调用并返回 dfs(w),且 dfs(w) 会在 dfs(v) 返回前返回。
  • dfs(w) 已经被调用但还未返回。证明的关键在于,在 DAG 中这种情况是不可能出现的,这是由于递归调用链意味着存在从 w 到 v 的路径,再加上现在的边 v->w 则刚好补成一个环。

所以在 DAG 中只可能有前面两种情况,其中 dfs(w) 都会在 dfs(v) 之前完成,也就是说后序排序中 v 指向的点都会在其前面,那么逆后序就是把 v->w 中的 w 排在 v 后面啦。具体实现把 DFS 改一下就好,加些数据结构存点,完整示例可以参见:DepthFirstOrder.java

Directed Cycle Detetion

上面提到过,当且仅当有向图没有有向环时,它才有拓扑排序,DFS 也能用于检测图是否含有环。因为系统维护的递归调用的栈表示的正是“当前”正在遍历的有向路径,如果我们遇到了一条边 v->w ,而 w 已经在栈里,就找到了一个环 v->w->v。

Implementation

public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; // 有向环中的所有顶点(如果存在)
private boolean[] onStack; // 递归调用的栈上的所有顶点 public DirectedCycle (Digraph G) {
onStack = new boolean[G.V()];
edgeTo = new int[G.V()];
marked = new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
if (!marked[v]) {
dfs(G, v);
}
}
} private void dfs(Digraph G, int v) {
onStack[v] = true; // 递归调用开始时标记为在栈中
marked[v] = true;
for (int w : G.adj(v)) {
if (this.hasCycle()) {
return;
}
else if (!marked[w]) {
edgeTo[w] = v;
dfs(G, w);
}
// v->w 中 w 已经在栈中,保存环 v->w->...->v到 cycle 里
else if (onStack[w]) {
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x]) {
cycle.push(x);
}
cycle.push(w);
cycle.push(v);
}
onStack[v] = false; // 递归调用结束时标记为不在栈中
}
} public boolean hasCycle() {
return cycle != null;
} public Iterable<Integer> cycle() {
return cycle;
}
}

Strong Components

强连通。在有向图中,若同时存在路径 v->w 和 w->v,则称点 v 和 w 是强连通的。类似的,这显然也是一个等价关系,满足:

  1. symmetric: 自反性, v 和 v 自身是强连通的。
  2. reflexive: 对称性, v 和 w 强连通,则 w 和 v 强连通。
  3. transitive: 传递性,如果 v 和 w 强连通,又有 w 和 x 强连通,那么 v 和 x 强连通。

强连通分量也很好理解,就是区域里面的点之间都是强连通的。

Demo

强连通分量可以帮助生物学家理解食物链中能量的流动,帮助程序员组织程序模块等。

Undirected Graphs 中提到的连通分量问题,可以用 DFS 预处理图,然后就可以在常数时间回应查询。同样的强连通问题也可以用 DFS 解决,用 kosaraju-sharir 算法,分成两步用两次 DFS。算法思想是计算核心 DAG (把强连通分量当成一个点)的拓扑排序,再按逆拓扑序列对点跑 DFS。我也不知道在说什么,看下面证明。

Phase 1

用 DFS 计算图 G 的反向图 \(G^{R}\) (边的方向相反)的逆后序序列。

Phase 2

按第一步中的逆后序序列来对图 G 进行 DFS 。

证明

分两点证明该算法的正确性。

  • 第二步构造函数中调用的 dfs(G, s) 会访问每个和 s 强连通的点

    反证法。假设某个和点 s 强连通的点 v 没有在 dfs(G, s) 中被访问,那就意味着 marked[v] 为 true,即点 v 在 s 之前就已经被访问过了。又因为两点强连通,故存在着从 v 到 s 的路径,所以访问 v 的时候的 dfs(G, v) 就会调用 dfs(G, s),而不会轮到构造函数来调用。矛盾,得证。

  • 构造函数调用的 dfs(G, s) 所到达的任意点 v 都必然和 s 强连通

    v 能被 dfs(G, s) 访问到,说明存在路径 s->v ,那么只要再证明存在路径 v->s 就能说明两点是强连通的。即等价于在 \(G^{R}\) 中找路径 s->v,且是在已知存在路径 v->s 的前提下。因为在 \(G^{R}\) 的逆后序中 v 排在 s 后面,所以 dfs(G, v) 结束得比 dfs(G, s) 早,那就只有两种情况:

    1. 调用 dfs(G, v) 结束在调用 dfs(G, s) 开始之前。
    2. 调用 dfs(G, v) 开始在 dfs(G, s) 调用开始之后且结束在 dfs(G, s) 结束之前。

    又因为已知存在路径 v->s ,所以第一种情况是不可能的,而第二种则意味着存在路径 s->v,证毕,再来张没什么大用的图。

实现

实现只要对 Undirected Graphs 中的 “Implementation With DFS” 代码稍作修改就好其实。

public class KosarajuSharirSCC {
private boolean[] marked;
private int[] id;
private int count; public KosarajuSharirSCC(Digraph G) {
marked = new boolean[G.V()];
id = new int[G.V()];
DepthFirstOrder dfs = new DepthFirstOrder(G.reverse());
// 按逆后序进行 DFS
for (int v : dfs.reversePost()) {
if (!marked[v]) {
dfs(G, v);
count++;
}
}
} private void dfs(Digraph G, int v) {
marked[v] = true;
id[v] = count;
for (int w : G.adj(v)) {
if (!marked[w]) {
dfs(G, w);
}
}
} public boolean stronglyConnected(int v, int w) {
return id[v] == id[w];
}
}

Directed Graphs的更多相关文章

  1. Java数据结构——带权图

    带权图的最小生成树--Prim算法和Kruskal算法 带权图的最短路径算法--Dijkstra算法 package graph; // path.java // demonstrates short ...

  2. 3-HOP: A High-Compression Indexing Scheme for Reachability Query

    title: 3-HOP: A High-Compression Indexing Scheme for Reachability Query venue: SIGMOD'09 author: Ruo ...

  3. JSON-LD

    RDF RDF用于信息需要被应用程序处理而不是仅仅显示给人观看的场合.RDF提供了一种用于表达这一信息.并使其能在应用程序间交换而不丧失语义的通用框架.既然是通用框架,应用程序设计者可以利用现成的通用 ...

  4. Markov Random Fields

    We have seen that directed graphical models specify a factorization of the joint distribution over a ...

  5. 【Python排序搜索基本算法】之深度优先搜索、广度优先搜索、拓扑排序、强联通&Kosaraju算法

    Graph Search and Connectivity Generic Graph Search Goals 1. find everything findable 2. don't explor ...

  6. zz A list of open source C++ libraries

    A list of open source C++ libraries < cpp‎ | links http://en.cppreference.com/w/cpp/links/libs Th ...

  7. Leetcode: Graph Valid Tree && Summary: Detect cycle in undirected graph

    Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), ...

  8. [zt]Which are the 10 algorithms every computer science student must implement at least once in life?

    More important than algorithms(just problems #$!%), the techniques/concepts residing at the base of ...

  9. Ural1387 Vasya's Dad

    Description Vasya's dad is good in maths. Lately his favorite objects have been "beautiful" ...

随机推荐

  1. 使用 DL4J 训练中文词向量

    目录 使用 DL4J 训练中文词向量 1 预处理 2 训练 3 调用 附录 - maven 依赖 使用 DL4J 训练中文词向量 1 预处理 对中文语料的预处理,主要包括:分词.去停用词以及一些根据实 ...

  2. 如何封装一个Cookie库

    由Cookie详解我们已经了解到了Cookie是为了实现有状态的基于HTTP协议的应用而存在的.一个Cookie的由以下几个部分组成: //设置cookie的格式和Set-Cookie头中使用的格式一 ...

  3. Golang Socket编程

    Socket编程 在很多底层网络应用开发者的眼里一切编程都是Socket,话虽然有点夸张,但却也几乎如此了,现在的网络编程几乎都是用Socket来编程.你想过这些情景么?我们每天打开浏览器浏览网页时, ...

  4. IOS仿微信朋友圈好友展示

    前几天小伙伴要帮他做一个群聊功能,里面有好友列表,要求和微信的差不多(见下图),让小伙伴自己实现了下,他将CollectionView放在tableView的tableHead中,可是当添加好友或删除 ...

  5. 可变参数的lambda表达式

    delegate int mydelegate(params int[] a); class Program { static void Main(string[] args) { //接收可变参数的 ...

  6. win10下设置IIS、安装php7.2

    开启IIS及相关功能: 控制面板——程序和功能——启用或关闭Windows功能——勾选Internet Information Service——万维网服务——性能和功能——勾选CGI 开启成功后在 ...

  7. MVC官方教程索引

    1.MVC教程首页http://www.asp.net/learn/mvc/?lang=cs 2.MVC概况2.1创建一个基于数据库的"电影"web应用http://www.asp ...

  8. java读取项目或包下面的属性文件方法

    1.使用java.util.Properties类的load()方法 //文件在项目下.不是在包下!! InputStream in = new BufferedInputStream(newFile ...

  9. HDU P3341 Lost's revenge 题解+数据生成器

    Lost and AekdyCoin are friends. They always play "number game"(A boring game based on numb ...

  10. PyCharm导入包的问题

    在此之前,我们说一下虚拟环境这个概念: 在django项目中,直接就安装各种package,可能会造成系统混乱,因为package之间会有依赖的.比方说,你现在直接装django,他会依赖其他的包(开 ...