算法——图之有向图
来源:互联网 发布: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一直被遍历,这就避开了有向图的问题。
有向图在是处理很多问题的基础,这篇用的基本都是深搜,对于实现有向图的最短路径,我们可以使用广搜。实现的方式和无向图中的基本一致。
- 算法——图之有向图
- 算法——图之加权有向图
- 十六、图算法之有向图
- 数据结构——有向图(拓扑排序算法)
- 【最大流模板——Dinic算法】【有向图】
- 有向图—拓扑排序,Kosaraju算法
- 有向图—朱刘算法模板
- Floyd算法 有向图。
- 算法4.2 有向图
- 每日一省之————有向图(有向非赋权图)
- 有向图强连通分量之tarjan算法
- 有向图的强连通分量之Tarjan算法
- 数据结构与算法之有向图的拓扑排序
- 有向图基本算法 -- 遍历算法
- 有向图基本算法 -- 遍历算法
- 数据结构之有向图
- 有向图的Dijkstra算法
- 有向图强连通算法
- 音频单元组件服务参考(Audio Unit Component Services Reference)
- EA&UML日拱一卒--序列图(Sequence Diagram)::门
- Doves and bombs UVA
- 内核资料:ALSA资料
- android用视频当做背景
- 算法——图之有向图
- 本地的vs项目设置成带域名的项目
- Homebrew 换源
- CentOS6.8安装MySQL
- Java——String练习1:将一个字符串"abcd_itcast"按照长度由长到短打印出来
- Unity Shader 属性及标签
- linux alsa 音频路径切换
- 生成网表
- [29] Window PowerShell DSC 学习系列----如何备份数据库和解决一个MS Server 2008上的大坑?