原文链接
一幅有方向性的图(有向图),由一组顶点和有方向的边组成,每条有向边都连接着一组有序对。
一条至少含有一条边且起点和终点相同的有向路径。简单有向环是一条(除了起点终点相同)不含有重复顶点和边的环。
路径或环的长度即为边的数量
package cn.ywrby.Graph;
//有向图
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
public class Digraph {
private final int V; //图中顶点数目
private int E; //边的个数
private Bag<Integer>[] adj; //邻接表
//构造函数
public Digraph(int V){
this.V=V;
this.E=0;
adj=(Bag<Integer>[]) new Bag[V];
for(int v=0;v<V;v++){
adj[v]=new Bag<Integer>();
}
}
public int V(){return V;}
public int E(){return E;}
//与无向图最大区别,只向v中写入w,而在w的邻接表中不包含v
public void addEdge(int v,int w){
adj[v].add(w); //向v的链表中写入顶点w
E++; //增加边的数目
}
public Digraph(In in){
this(in.readInt());
int E=in.readInt();
for(int i=0;i<E;i++){
int v=in.readInt();
int w=in.readInt();
addEdge(v,w);
}
}
public Iterable<Integer>adj(int v){return adj[v];}
//有向图特有方法,倒置所有有向路径,将图反向
//利用正向图可以获得某个节点都指向了哪些顶点
//利用反向图,可以获得有多少点指向该节点
public Digraph reverse(){
Digraph R=new Digraph(V);
for(int v=0;v<V;v++){
for (int w:adj(v)){
R.addEdge(w,v);
}
}
return R;
}
}
给定一幅有向图与一个起点S,是否存在一条从S到达给定顶点V的有向路径
给定一幅有向图和一组顶点集合,是否存在一条从集合中任意顶点到达给定顶点v的有向路径
利用无向图中谈到过的深度优先搜索方法可以处理以上两种问题,并且比处理无向图中的连通性更流程简介,因为深度优先搜索与广度优先搜索本就更适应于有向图
package cn.ywrby.Graph;
//有向图的可达性
//利用有向图与深度优先搜索实现
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;
public class DirectedDFS {
private boolean[] marked;
//处理单个节点可行性问题
public DirectedDFS(Digraph G,int s){
marked=new boolean[G.V()];
dfs(G,s);
}
//利用数据结构处理多节点可行性问题
public DirectedDFS(Digraph G,Iterable<Integer>sources){
marked=new boolean[G.V()];
for(int s:sources){
dfs(G,s);
}
}
private void dfs(Digraph G,int s){
marked[s]=true;
for(int w:G.adj(s)){
if(!marked[w]) dfs(G,w);
}
}
public boolean marked(int v) {return marked[v];}
public static void main(String[] args) {
Digraph G=new Digraph(new In(args[0]));
Bag<Integer> sources=new Bag<Integer>();
for(int i=1;i<args.length;i++){
sources.add(Integer.parseInt(args[i]));
}
DirectedDFS reachable=new DirectedDFS(G,sources);
for(int i=0;i<G.V();i++){
if(reachable.marked(i)) StdOut.print(i+" ");
}
StdOut.println();
}
}
多点可达性的一个重要应用就是类似Java中的垃圾清理机制(对运行内存使用情况的控制),在程序执行的过程中,有些对象可以被直接访问,而不能被直接访问的对象就应当被及时清楚。通过类似于深度优先搜索的机制,Java每隔一段时间就会检测程序运行过程中的不可达顶点,并对它进行清理
只需要修改前面介绍过的广度优先搜索和深度优先搜索的相应方法的类名,就可以实现类似无向图的有向图寻路功能,单点最短有向路径等
给定一组任务并安排他们的执行顺序(上课顺序,执行顺序等等),限制条件是这些任务的执行方法和起始时间。限制条件还可能包括任务的耗时与资源利用情况,其中最重要的一种限制条件就是优先级限制
执行任务很容易碰到优先级问题,例如在安排大学课程时,要先学习计算机理论基础,才能开始学习人工智能,先学习算法才能学习与之相关的数据库知识。基于此的调度问题就可以利用有向图来解决
但在解决过程中如果遇到有向环,例如学习A后才可以学习B,学习B后才可以学习C,学习C后才可以学习A。这种有向环明显是不符合逻辑的,也会导致问题无解
所以,怎么辨别有向图中是否包含有向环(有向图是否是有向无环图)就非常重要了
package cn.ywrby.Graph;
//利用深度优先寻找有向环
import cn.ywrby.dataStructure.Stack;
public class DirectedCycle {
private boolean[] marked;
private int[] edgeTo;
private Stack<Integer> cycle; //有向环中所有顶点
private boolean[] onStack; //递归调用的栈上所有的顶点
public DirectedCycle(Digraph G){
marked=new boolean[G.V()];
edgeTo=new int[G.V()];
onStack=new boolean[G.V()];
for(int v=0;v<G.V();v++){
if(!marked[v]) dfs(G,v);
}
}
/*
* 在进行深度优先搜索的过程中不断把所有顶点放入栈中
* 如果发现曾经已经存在栈中的顶点再次被别的顶点指向
* 说明在这条有向路径中出现了一个环
* (例如1->5,5->4,4->3,3->5 在再次放入5的过程中就发现了这个环5,4,3)
* 我们可以通过edgeTo将这个环的路径找到
* */
private void dfs(Digraph G,int v){
marked[v]=true;
onStack[v]=true;
for(int w:G.adj(v)){
if(this.hasCircle()) return; //发现一个环就结束循环
else if(!marked[w]) {edgeTo[w]=v;dfs(G,w);}
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 hasCircle(){return cycle!=null;}
public Iterable<Integer> cycle(){return cycle;}
}
给定一幅有向图,将所有顶点排序,使得所有有向边均从排在前面的元素指向排在后面的元素(或者说明无法做到这一点)
优先级限制下的调度问题,等价于计算有向无环图中的拓扑排序
在实现拓扑排序前,首先要理解以下三种有向图的遍历方式:
package cn.ywrby.Graph.DepthFirstPaths;
import cn.ywrby.Graph.Digraph;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.Stack;
//有向图中基于深度优先搜索的顶点排序
//能够实现前序,后序,逆后序
public class DepthFirstOrder {
private boolean[] marked;
private Queue<Integer> pre; //前序遍历
private Queue<Integer> post; //后序遍历
private Stack<Integer> reversePost; //逆后序
public DepthFirstOrder(Digraph G){
pre=new Queue<Integer>();
post=new Queue<Integer>();
reversePost=new Stack<Integer>();
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 s){
pre.enqueue(s); //在递归之前加入队列,前序遍历
marked[s]=true;
for(int w:G.adj(s)){
if(!marked[w]) dfs(G,w);
}
post.enqueue(s); //递归完成之后加入队列,后序遍历
reversePost.push(s); //递归完成之后加入栈,实现反向,逆后序
}
public Iterable<Integer> pre(){return pre;}
public Iterable<Integer> post(){return post;}
public Iterable<Integer> reversePost(){return reversePost;}
}
package cn.ywrby.Graph;
import cn.ywrby.Graph.DepthFirstPaths.DepthFirstOrder;
import edu.princeton.cs.algs4.StdOut;
//拓扑排序 利用逆后序实现
public class Topological {
private Iterable<Integer> order;
public Topological(Digraph G){
DirectedCycle cyclefinder=new DirectedCycle(G);
if(!cyclefinder.hasCircle()){ //判断是否符合有向无环图的条件
DepthFirstOrder dfs=new DepthFirstOrder(G); //进行深度优先搜索
order=dfs.reversePost(); //取出逆后序排列
}
}
public Iterable<Integer> order(){return order; }
public boolean isDAG() {return order!=null;}
public static void main(String[] args) {
String filename=args[0];
String separator =args[1];
SymbolDigraph sg=new SymbolDigraph(filename,separator);
Topological top=new Topological(sg.G());
for(int v:top.order()){
StdOut.println(sg.name(v));
}
}
}
对于任意边v->w,在调用dfs(v)时,都必然涉及下面三种情况中的一种:
综上所述,只有钱两种情况发生的前提下,v会按照拓扑排序出现在w的后方且对于所有顶点都适用,所以逆后序排列后的结果就是拓扑排序的结果
代码一共进行两次遍历,第一次深度优先搜索保证不含有环结构,第二次深度优先搜索进行逆后序排列,两次都遍历了数组,所以与V+E成正比
如果v可达w,且w可达v(存在一条路径从v到w,也存在一条路径从w到v),则称v顶点和w顶点是强连通的
强连通性与无向图中的连通性一样,都是一种等价关系,它们均满足三条性质:
由于强连通性是一种等价关系,它将所有顶点分成了不同的等价类,这些等价类称作强连通分量
package cn.ywrby.Graph;
import cn.ywrby.Graph.DepthFirstPaths.CC;
import cn.ywrby.Graph.DepthFirstPaths.DepthFirstOrder;
import cn.ywrby.Graph.Graph;
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;
/*
* Kosaraju算法计算有向图中强连通分量个数
* 算法流程很简单,但要真正理解需要仔细思考
* 首先运用深度优先搜索,遍历有向图,利用逆倒序排列将所有顶点储存在栈中
* 然后按照出栈顺序再次利用深度优先搜索遍历整个有向图
* 在同一次递归调用中的顶点,都是同一个强连通分量下的顶点
* */
public class KosarajuSCC {
private boolean[] marked;
private int[] id; //id数组用来存储每个顶点所在的强连通分量
private int count; //计算图中强连通分量个数
public KosarajuSCC(Digraph G){
marked=new boolean[G.V()];
id=new int[G.V()];
//利用深度优先搜索获取逆倒序排列
DepthFirstOrder order=new DepthFirstOrder(G.reverse());
//按照逆倒序排列遍历整个有向图
for(int s:order.reversePost()){
if(!marked[s]){
dfs(G,s);
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];}
//返回当前节点id
public int id(int v){return id[v];}
public int count(){return count;}
public static void main(String[] args) {
Digraph G=new Digraph(new In(args[0]));
//进行深度优先搜索处理强连通分量
KosarajuSCC cc=new KosarajuSCC(G);
int M=cc.count();
StdOut.println(M+" components");
//创建一个背包,存储所有强连通分量
Bag<Integer>[] components;
components=(Bag<Integer>[])new Bag[M];
for(int i=0;i<M;i++){
components[i]=new Bag<Integer>();
}
//将每个顶点,按照所在强连通分量加到背包中
for(int v=0;v<G.V();v++){
components[cc.id(v)].add(v);
}
for(int i=0;i<M;i++){
for(int v:components[i]){
StdOut.print(v+" ");
}
StdOut.println();
}
}
}
算法流程很简单,但要真正理解需要仔细思考
首先运用深度优先搜索,遍历有向图,利用逆倒序排列将所有顶点储存在栈中
然后按照出栈顺序再次利用深度优先搜索遍历整个有向图
在同一次递归调用中的顶点,都是同一个强连通分量下的顶点
利用反证法,假设存在v和s强连通且在s调用dfs方法时未被访问到,因为存在s到v的路径,所以说明v已经被标记了。但在标记v时,必然调用dfs(G,v),因为s与v强连通,所以存在v到s的路径,所以s之前必然已经被标记过了,那就不会再被访问了,与论题相悖,证明1成立
v是dfs(G,s)到达的某一个顶点,证明G中必然含有s到v的路径,说明G的反向图中必然含有v到s的路径,所以此时要证明s与v具有强连通性,只要证明G中含有v到s的路径,即G的反向图中含有s到v的路径即可。根据第一条证明可知,先出现的是s顶点,所以v顶点一定先于s顶点结束调用,进入栈中。所以在反向图中有两种调用情况
两种情况如下图,易知第一种情况不可能实现,第二种情况证明了我们想得到的结论
算法运行过程中处理有向图和与之对应的反向图,一共进行两次深度优先搜索,三步所需时间都和V+E成正比,所需空间主要用于构造反向图,也与V+E成正比