算法——图之有向图

来源:互联网 发布:js里面遍历树形json 编辑:程序博客网 时间:2024/06/07 09:38

我们主要讨论一下方面:

1.有向图的表示

有向图的可达性
有向图的路径

2.
判断有向图中是否有环
拓扑排序,优先级限制下的调度问题

3.
有向图的强连通性


有向图的表示

和无向图中的一样,我们也采用邻接表矩阵的方式来表示有向图。只需要修改addEdge方法,只增加一条边,而不是增加双向边就可以了。

public class DiGraph {private int V;  // 节点数private int E;  // 边的数目private List<Integer>[] adj; // 邻接表矩阵public DiGraph(int V) { // 创建节点个数为V的没有边的有向图this.V = V;this.E = 0;adj = (List<Integer>[])new List[V];for (int i = 0; i < V; i++) {adj[i] = new ArrayList<Integer>();}}public void addEdge(int v, int w) { // 在有向图中增加边v->wadj[v].add(w);E++;}public List<Integer> adj(int v) { // 返回v节点的相邻节点return adj[v];}public int V() { // 返回节点数return V;}public int E() { // 返回边的数目return E;}public String toString() { // 打印图String s = V + " 个顶点, " + E + " 条边\n";for (int i = 0; i < V; i++) {s += i + ": ";for (Integer node : adj(i)) {s += node + " ";}s += "\n";}return s;}public DiGraph reverse() {DiGraph g = new DiGraph(V);for (int i = 0; i < V; i++) {for (Integer node : adj[i]) {g.addEdge(node, i);}}return g;}}
我们在其中增加了reverse方法,获得当前表的反向表,即将所有边的方向反向,例如v->w变成w-v。


有向图的路径和可达性

这个也和无向图中的一样,只需要将图的类改成DiGraph就可以直接使用了。


有向图在许多应用中发挥重要的作用,一个典型的应用就是任务调度。

有向图的一个节点就代表着一个任务,有向图的边代表着优先级,例如v->w,说明v的优先级比w高。


给我们一张图,我们就需要给出这个图对应的任务调度方法。这个问题等价于拓扑排序。

拓扑排序

给定一副有向图,将所有顶点排序,使得所有的有向图均从排在前面的元素指向排在后面的元素。


但是并不是所有图都能进行拓扑排序的。如果图中存在一个有向环的话,那么就拓扑排序就不能成功。

假设x必须在y之前完成,y在z之前完成,z又在x之前完成,那么就不可能进行拓扑排序了。


所以要解决拓扑排序的问题,我们首先要解决图是否存在环的问题。


怎么样会存在环呢?

要判断图中是否存在环,我们肯定要对图进行遍历,遍历的方法我们有深搜和广搜。

要判断图中是否有环,我们就要记录当前走的这条路,如果又走到了当前这条路中的一个节点,那么就说明有环。

使用深搜遍历的时候,递归调用就是我们当前走的这条路。所以我们只需要在使用深搜的时候,判断当前这个节点是否在这条路中,就可以判断是否存在环了。


实现:

public class DirectedCycle { // 判断图是否有环private boolean[] inStack;private Stack<Integer> cycle;private Integer[] edgeTo;private boolean[] isMarked;public DirectedCycle(DiGraph g) {inStack = new boolean[g.V()];edgeTo = new Integer[g.V()];isMarked = new boolean[g.V()];for (int i = 0; i < g.V(); i++) {if (!isMarked[i]) {dfs(g, i);}}}private void dfs(DiGraph g, int begin) {isMarked[begin] = true;inStack[begin] = true;for (Integer node : g.adj(begin)) {if (hasCycle()) return;if (!isMarked[node]) {edgeTo[node] = begin;dfs(g, node);} else if (inStack[node]) { // 如果当前路径Stack中含有node,又再次访问的话,说明有环// 将环保存下来cycle = new Stack<>();for (int i = begin; i != node; i = edgeTo[i]) {cycle.push(i);}cycle.push(node);cycle.push(begin);}}inStack[begin] = false;}public boolean hasCycle() {return cycle != null;}public Stack<Integer> cycle() {return cycle;}}

如果是使用迭代的方式,当前走过的路径就会存放在stack当中,stack就是当前的路径。但是使用递归的方式,我们无法知道路径,所以我们使用一个boolean数组来表示。

所以我们的数组需要和stack中的行为保持一致,在进入的时候设置为true,相当于stack的push,退出的时候设置为false,相当于pop。

我们在递归中进行判断,如果当前这个节点之前走过,那么就出现环了。


完成了时候存在环的操作之后,我们就可以实现拓扑排序了。

这是时候拓扑排序的实现是非常简单的,我们只需要增加一点点代码,就可以实现拓扑排序。

public class DirectedDFS { // 深搜解决图的可达性和路径,保存拓扑排序private boolean[] isMarked; // 是否可达private Integer[] edgeTo; // 记录路径private List<Integer> begin; // 开始节点们private List<Integer> reversePost; // 拓扑排序顺序public DirectedDFS(DiGraph g) { // 所有节点遍历reversePost = new ArrayList<>();isMarked = new boolean[g.V()];edgeTo = new Integer[g.V()];List<Integer> begins = new ArrayList<>();for (int i = 0; i < g.V(); i++) {begins.add(i);}this.begin = begins;for (int i = 0; i < g.V(); i++) {if (!isMarked[i]) {dfs(g, i);}}}public DirectedDFS(DiGraph g, int begin) { // 从begin节点开始,进行深搜reversePost = new ArrayList<>();isMarked = new boolean[g.V()];edgeTo = new Integer[g.V()];this.begin = new ArrayList<>();this.begin.add(begin);dfs(g, begin);}public DirectedDFS(DiGraph g, List<Integer> begins) { // 找出一堆begin中所有可达的地方reversePost = new ArrayList<>();isMarked = new boolean[g.V()];edgeTo = new Integer[g.V()];this.begin = begins;for (int i = 0; i < begin.size(); i++) {if (!isMarked[i]) {dfs(g, begin.get(i));}}}public void dfs(DiGraph g, int begin) { // 深搜将所有节点遍历,标记被访问过的节点isMarked[begin] = true;for (Integer node : g.adj(begin)) {if (!isMarked[node]) {edgeTo[node] = begin;dfs(g, node);}}reversePost.add(0, begin);}public boolean hasPath(int v) {return isMarked[v];}public String pathTo(int v) {if (!hasPath(v)) {return "";}Stack<Integer> stack = new Stack<>();stack.push(v);for (int i = v; !begin.contains(i); i = edgeTo[i]) {stack.push(edgeTo[i]);}return stack.toString();}public Iterable<Integer> reversePost() {return reversePost;}}
我们仅仅需要在递归调用结束的时候,将节点增加进入。或许这里用Stack会更加容易理解,不用Stack的主要原因是,使用for遍历Stack的时候,会按照push的顺序遍历,而我们希望从栈顶向下遍历。

为什么这样就实现了拓扑排序呢?我们仔细思考一下,对于递归调用开始反弹的时候,当前的节点是被其他节点调用的,说明当前节点的优先级在其他节点之前。所以对于每一个节点,优先级都遵循这样的规则,而对于那些没有优先级关系的,他们的先后关系对于拓扑排序来说其实并不关系。

这样,根据拓扑排序,我们就可以得到一个任务的调度策略。



有向图和无向图的区别之一在于连通性问题,对于无向图来说,连通性是双向的。然而对于有向图来说,是单向的。

而有向图的强连通性也是十分重要的,它可以简化节点的数目,对于复杂的图来说,可以简化图。并且简化出来的图是一个DAG(有向无环图)。
属于同一个强连通分量的节点,我们会认为是类似的,可以进行划分等等。

那么如何实现呢?

kosaraju算法

思路:

1. 获取反向图
2. 获取反向图的拓扑排序
3. 根据拓扑排序顺序对原图进行深搜, 记录count为强连通分量

我们先来看实现:

/* * 1. 获取反向图 * 2. 获取反向图的拓扑排序 * 3. 根据拓扑排序顺序对原图进行深搜, 记录count为连通分量 * */public class StrongConnection {private boolean[] isMark; // 是否被访问过private int[] id;  // 连通分量idprivate int count; // 当前连通分量public StrongConnection(DiGraph g) {isMark = new boolean[g.V()];id = new int[g.V()];count = 0;// 1.获取反向图DiGraph reverseG = g.reverse();// 2. 获取反向图的拓扑排序DirectedDFS dfs = new DirectedDFS(reverseG);// 3. 根据拓扑排序顺序对原图进行深搜, 记录count为连通分量for (Integer node : dfs.reversePost()) {if (!isMark[node]) {System.out.println("node: " + node);dfs(g, node);count++;}}}private void dfs(DiGraph g, int begin) {isMark[begin] = true;id[begin] = count;for (Integer node : g.adj(begin)) {if (!isMark[node]) {dfs(g, node);}}}public boolean isStrongConnect(int v, int w) { // v和w是否强连通return id[v] == id[w];}public int count() { // 返回连通分量数量return count;}}
实现可以说是非常的简单,思路却不容易让人理解。

对于实现强连通,我们首先会想到使用无向图中的方法,但是这却是有向图,是不同的。如果我们在判断是否连通的时候,可以不考虑这个问题的话,那么我们就成功了。

例如,上图中,节点0,1属于同一个连通分量,2属于一个连通分量。

我们在使用无向图的方法的时候,就会希望,如果我们遍历的顺序,2会在1前面就好了。因为先遍历2的话,2就会被标记,再遍历1的时候,因为发现2已经被标记了,就不会再访问了。这样我们就避开了方向的问题。

我们使用反向图的拓扑排序就可以得出这么一个遍历的顺序。

1.对于同一个强连通分量,因为在这个连通分量中,是强连通的,所以反向图和原图的顺序无关紧要。

2.对于不同连通分量,反向图的拓扑排序,实现了反向图中优先级高的比优先级低的先访问,这就保证了在原图中优先级低的节点会在优先级高的先访问,因为他们是反向的。

对于上面的例子来说,就实现了节点2会在节点1一直被遍历,这就避开了有向图的问题。


有向图在是处理很多问题的基础,这篇用的基本都是深搜,对于实现有向图的最短路径,我们可以使用广搜。实现的方式和无向图中的基本一致。

原创粉丝点击