有向图

有向图同无向图的区别为每条边带有方向,表明从一个顶点至另一个顶点可达。有向图的算法多依赖深度搜索算法。

本文主要介绍有向图的基本算法,涉及图的表示、可达性、检测环、图的遍历、拓扑排序以及强连通检测等算法。

1 定义有向图

采用邻接表结构存储边信息,同时提供reverse接口生成反向图,倒置每个边的方向,该接口在后续其他算法中会用到。

/**
* 采用邻接表表示的有向图
*/
public class DiGraph {
private final int V;
private int E;
private ArrayList<Integer>[] adj;
public DiGraph(int V)
{
this.V = V;
E = 0;
adj = new ArrayList[V];
for (int i = 0; i < V; i++) {
adj[i] = new ArrayList<>();
}
}
public DiGraph(Scanner scanner)
{
this(scanner.nextInt());
int E = scanner.nextInt();
for (int i = 0; i < E; i++) {
int v = scanner.nextInt();
int w = scanner.nextInt();
addEdge(v, w);
}
}
public void addEdge(int v, int w)
{
// 添加一条v指向w的边
adj[v].add(w);
E++;
}
/**
* 返回有向图的反向图, 将每条边的方向反转
*/
public DiGraph reverse()
{
DiGraph diGraph = new DiGraph(V);
for (int v = 0; v < V; v++) {
for (int w : adj[v]) {
diGraph.addEdge(w, v);
}
}
return diGraph;
}
public void show() {
System.out.println("V: " + V);
System.out.println("E: " + E);
for (int i = 0; i < V; i++) {
System.out.print(i + ": ");
for (Integer integer : adj[i]) {
System.out.print(integer + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输入用例参加附录1
DiGraph diGraph = new DiGraph(scanner);
// 输入结果见附录2
diGraph.show();
}
}

2 有向图的可达性

有向图的可达性是指给定一个或一组顶点,判断是否可以到达图中其他顶点。垃圾清除常见算法“标记-清除”算法中,采用有向图的可达性算法

标记所有可以被访问的对象,然后在回收阶段,仅仅回收那些未被标记的对象。

/**
* 基于深度优先的有向图可达性算法
* 求出给定顶点或一组顶点,有向图中能到达的点
*/
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 v : sources) {
if(!marked[v]){
dfs(G, v);
}
}
}
private void dfs(DiGraph G, int v)
{
marked[v] = true;
for (int w : G.adj(v)) {
if(!marked[w])
dfs(G, w);
}
}
public boolean marked(int v) { return marked[v]; }
public static void main(String[] args) {
// 输入用例参加附录1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 输出结果参加附录3
// 测试顶点2到达的点
System.out.println("顶点2到达的点");
DirectedDFS reachable = new DirectedDFS(diGraph, 2);
for (int i = 0; i < diGraph.V(); i++)
if(reachable.marked(i)) System.out.print(i + " ");
System.out.println();
// 测试一组点:1,2,6能够到达的点
System.out.println("1,2,6能够到达的点");
DirectedDFS reachable2 = new DirectedDFS(diGraph, Arrays.asList(1, 2, 6));
for (int i = 0; i < diGraph.V(); i++)
if(reachable2.marked(i)) System.out.print(i + " ");
System.out.println();
}
}

3 单点有向路径和单点最短有向路径

分别采用深度优先搜索和广度优先搜索实现

有向图的路径

/**
* 单点有向路径,给定顶点v,确定对于图中任一点w;
* 是否存在v到w的路径,并输出路径;
* 注意,深度优先搜索的路径无法保证是最短路径
*/
public class DigraghDepthFirstPaths {
// 标记点是否可达
private boolean[] marked;
// 记录到达点的那条边
private int[] edge;
private final int s;
public DigraghDepthFirstPaths(DiGraph G, int s)
{
this.s = s;
marked = new boolean[G.V()];
edge = new int[G.V()];
edge[s] = s;
dfs(G, s);
}
private void dfs(DiGraph G, int v)
{
marked[v] = true;
for (int w : G.adj(v)) {
if(!marked[w]){
edge[w] = v;
dfs(G, w);
}
}
}
public boolean hasPathTo(int v){ return marked[v]; }
public Stack<Integer> pathTo(int v)
{
Stack<Integer> paths = new Stack<>();
for (int x=v; x!=s; x=edge[x]){
paths.add(x);
}
paths.add(s);
return paths;
}
public static void main(String[] args) {
// 输入用例参加附录1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 输出结果参加附录4
// 构建顶点0到其他顶点的有向路径
DigraghDepthFirstPaths depthFirstPaths = new DigraghDepthFirstPaths(diGraph, 0);
System.out.print("顶点0可达的点: ");
for (int i = 0; i < diGraph.V(); i++) {
if (depthFirstPaths.hasPathTo(i)) System.out.print(i + " ");
}
System.out.println();
// 是否存在有向路径
if(depthFirstPaths.hasPathTo(12))
System.out.println("0至12存在有向路径");
else
System.out.println("0至12不存在有向路径");
// 顶点0到顶点3的一条有向路径
System.out.print("0至3的一条有向路径: ");
Stack<Integer> pathTo = depthFirstPaths.pathTo(3);
while (!pathTo.isEmpty()){
if (pathTo.size() == 1)
System.out.print(pathTo.pop());
else
System.out.print(pathTo.pop() + " -> ");
}
System.out.println();
}
}

有向图的最短路径,基于广度优先算法

/**
* 基于广度优先搜索的单向路径算法;
* 在此方法下,求得的路径为最短路径(忽略边权重)
*/
public class DigraphBreadthFirstPaths {
private boolean[] marked;
// 采用队列保持带访问的顶点
private ArrayDeque<Integer> enqueue;
private int[] edge;
private final int s;
public DigraphBreadthFirstPaths(DiGraph G, int s)
{
this.s = s;
marked = new boolean[G.V()];
edge = new int[G.V()];
enqueue = new ArrayDeque<>();
enqueue.add(s);
bfs(G);
}
private void bfs(DiGraph G)
{
while (!enqueue.isEmpty())
{
int v = enqueue.poll();
for (int w : G.adj(v)) {
if(!marked[w]){
edge[w] = v;
marked[w] = true;
enqueue.add(w);
}
}
}
}
public boolean hasPathTo(int v){ return marked[v]; }
public Stack<Integer> pathTo(int v)
{
Stack<Integer> paths = new Stack<>();
for (int x=v; x!=s; x=edge[x]){
paths.add(x);
}
paths.add(s);
return paths;
}
public static void main(String[] args) {
// 输入用例参加附录1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 输出结果参加附录5
// 构建顶点0到其他顶点的有向路径
DigraphBreadthFirstPaths breadthFirstPaths = new DigraphBreadthFirstPaths(diGraph, 0);
System.out.print("顶点0可达的点: ");
for (int i = 0; i < diGraph.V(); i++) {
if (breadthFirstPaths.hasPathTo(i)) System.out.print(i + " ");
}
System.out.println();
// 是否存在有向路径
if(breadthFirstPaths.hasPathTo(12))
System.out.println("0至12存在有向路径");
else
System.out.println("0至12不存在有向路径");
// 顶点0到顶点3的最短路径
System.out.print("0至3的一条有向路径: ");
Stack<Integer> pathTo = breadthFirstPaths.pathTo(3);
while (!pathTo.isEmpty()){
if (pathTo.size() == 1)
System.out.print(pathTo.pop());
else
System.out.print(pathTo.pop() + " -> ");
}
System.out.println();
}
}

4 检测有向图的环

检测有向图是否包含环,检测图没有环是拓扑排序的前提条件。

多数情况下,需要知道有向图是否包含环,并且输出够成环的边。

/**
* 基于深度优先搜索检测图中是否包含环
*/
public class DirectedCycle {
private boolean[] onStack;
private Stack<Integer> cycle;
private int[] edge;
private boolean[] marked;
public DirectedCycle(DiGraph G)
{
onStack = new boolean[G.V()];
edge = new int[G.V()];
marked = new boolean[G.V()];
for (int i = 0; i < G.V(); i++) {
if(!marked[i])
dfs(G, i);
}
}
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]){
edge[w] = v; dfs(G, w); }
// onStack[w]为true表明,当前v节点是一条经过w的抵达,表明w -> v有路径
// 由于v -> w有边,因此必为环
else if(onStack[w]){
cycle = new Stack<>();
for (int x = v; x != w; x=edge[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; } public static void main(String[] args) {
// 输入用例参加附录1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 输出结果参加附录6
DirectedCycle directedCycle = new DirectedCycle(diGraph);
System.out.println("有向图是否包含环: " + (directedCycle.hasCycle() ? "是" : "否"));
if (directedCycle.hasCycle()){
System.out.print("其中一条环为:");
for (int i : directedCycle.cycle()) {
System.out.print(i + " ");
}
}
System.out.println();
}
}

5 顶点的深度优先次序

顶点的深度优先次序分为前序、后序和逆后续,区别是记录点的时机发生在递归调用的前还是后。该算法产生的pre、post和reversePost

顺序在图的高级算法中十分有用。

public class DepthFirstOrder {
private boolean[] marked;
private ArrayDeque<Integer> pre; // 保存前序遍历的结果
private ArrayDeque<Integer> post; // 保存后序的遍历结果
private ArrayDeque<Integer> reversePost; //保存逆后序的遍历结果
public DepthFirstOrder(DiGraph G)
{
marked = new boolean[G.V()];
pre = new ArrayDeque<>();
post = new ArrayDeque<>();
reversePost = new ArrayDeque<>();
for (int v=0; v<G.V(); v++)
if (!marked[v]) dfs(G, v);
}
private void dfs(DiGraph G, int v)
{
marked[v] = true;
pre.add(v);
for (int w : G.adj(v))
if(!marked[w])
dfs(G, w);
post.add(v);
// 按post的倒序保存
reversePost.addFirst(v);
}
public Iterable<Integer> pre(){ return pre; }
public Iterable<Integer> post(){ return post; }
public Iterable<Integer> reversePost(){ return reversePost; }
public static void main(String[] args) {
// 构造无环图的输入参见附录7
DiGraph diGraph = new DiGraph(new Scanner(System.in));
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(diGraph);
// 输出结果参加附录8
// 注意:对于同一幅图,构造图的输入顺序不一致
// 会导致输出不相同
System.out.print("前序节点顺序: ");
for (int v : depthFirstOrder.pre())
System.out.print(v + " ");
System.out.println();
System.out.print("后续节点顺序:");
for (int v : depthFirstOrder.post())
System.out.print(v + " ");
System.out.println();
System.out.print("逆后序节点顺序:");
for (int v : depthFirstOrder.reversePost())
System.out.print(v + " ");
}
}

6 拓扑排序

给定一幅有向图,给出一组顶点排序,在有向图中,所有的边均是前面的点指向后面的点。

拓扑排序依赖图的环检测和逆后序遍历算法。

/**
* 计算有向无环图中的所有顶点的拓扑排序,
* 通常用于解决优先级限制下的调度问题
*/
public class Topological {
private Iterable<Integer> order;
public Topological(DiGraph G)
{
DirectedCycle directedCycle = new DirectedCycle(G);
if(!directedCycle.hasCycle())
order = new DepthFirstOrder(G).reversePost();
}
public boolean isDAG(){ return order == null; }
public Iterable<Integer> order(){ return order; }
public static void main(String[] args) {
// 输入用例参考附录7
DiGraph diGraph = new DiGraph(new Scanner(System.in));
Topological topological = new Topological(diGraph);
// 输出结果参见附录9
if (topological.isDAG())
System.out.println("有向图带有环,无法进行拓扑排序");
else{
System.out.print("拓扑排序结果:");
for (int v : topological.order()) {
System.out.print(v + " ");
}
}
}
}

7 强联通检测

如果存在从v至w的路径,同时还存在从w至v的路径,则称v和w之间是强连通;如果一幅有向图中任意两点间都

是强连通,则这幅有向图也是强连通的。检测强连通算法依赖图的反转和逆后序遍历算法。算法比较简洁,但是

理解起来比较难,需要仔细分析理解。

/**
* 有向图的强连通性,该算法依赖逆后序排序、图的反转、无向图的联通性算法
*/
public class SCC {
private int[] id;
private int count;
private boolean[] marked;
public SCC(DiGraph G)
{
id = new int[G.V()];
marked = new boolean[G.V()];
DepthFirstOrder depthFirstOrder = new DepthFirstOrder(G.reverse());
for (int v : depthFirstOrder.reversePost())
if(!marked[v]) {
dfs(G, v);
count++;
}
}
private void dfs(DiGraph G, int v)
{
id[v] = count;
marked[v] = true;
for (int w : G.adj(v))
if(!marked[w])
dfs(G, w);
}
// 两点是否是强连通
public boolean stronglyConnected(int v, int w){ return id[v] == id[w]; }
// 强联通分量数
public int count(){ return count; }
// 节点所在的联通分量标识符
public int id(int v){ return id[v]; }
public static void main(String[] args) {
// 带环的图,输入用例参见附录1
DiGraph diGraph = new DiGraph(new Scanner(System.in));
// 输出结果参见附录10
SCC scc = new SCC(diGraph);
System.out.println("有向图中强连通分量数:" + scc.count());
System.out.println("节点6与12是否是强连通:" + (scc.stronglyConnected(6, 12) ? "是" : "否"));
System.out.println("节点9与12是否是强连通:" + (scc.stronglyConnected(9, 12) ? "是" : "否"));
System.out.println("输出联通分量");
for (int i = 0; i < scc.count(); i++) {
for (int v = 0; v < diGraph.V(); v++) {
if(scc.id[v] == i)
System.out.print(v + " ");
}
System.out.println();
}
}
}

附录1,有向图构造数据

13
22
4 2
2 3
3 2
6 0
0 1
2 0
11 12
12 9
9 10
9 11
8 9
10 12
11 4
4 3
3 5
7 8
8 7
5 4
0 5
6 4
6 9
7 6

附录2,有向图输出

V: 13
E: 22
0: 1 5
1:
2: 3 0
3: 2 5
4: 2 3
5: 4
6: 0 4 9
7: 8 6
8: 9 7
9: 10 11
10: 12
11: 12 4
12: 9

附录3:有向图的可达性测试

顶点2到达的点
0 1 2 3 4 5
1,2,6能够到达的点
0 1 2 3 4 5 6 9 10 11 12

附录4:基于深度优先搜索的单向路径测试结果

顶点0可达的点: 0 1 2 3 4 5
0至12不存在有向路径
0至3的一条有向路径: 0 -> 5 -> 4 -> 2 -> 3

附录5:基于广度优先搜索的最短路径测试结果

顶点0可达的点: 0 1 2 3 4 5
0至12不存在有向路径
0至3的一条有向路径: 0 -> 5 -> 4 -> 3

附录6:检测环算法的测试输出

有向图是否包含环: 是
其中一条环为:3 2 4 5 3

附录7:构造无环图的输入用例

13
15
0 1
0 5
0 6
2 0
2 3
3 5
5 4
6 4
6 9
7 6
8 7
9 10
9 11
9 12
11 12

附录8:深度优先遍历图的输出结果

前序节点顺序: 0 1 5 4 6 9 10 11 12 2 3 7 8
后续节点顺序:1 4 5 10 12 11 9 6 0 3 2 7 8
逆后序节点顺序:8 7 2 3 0 6 9 11 12 10 5 4 1

附录9:拓扑排序测试输出结果

拓扑排序结果:8 7 2 3 0 6 9 11 12 10 5 4 1

附录10:带环有向图的强连通性测试输出结果

有向图中强连通分量数:5
节点6与12是否是强连通:否
节点9与12是否是强连通:是
输出联通分量
1
0 2 3 4 5
9 10 11 12
6
7 8

有向图的基本算法-Java实现的更多相关文章

  1. 无向图的最短路径算法JAVA实现

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  2. 无向图的最短路径算法JAVA实现(转)

    一,问题描述 给出一个无向图,指定无向图中某个顶点作为源点.求出图中所有顶点到源点的最短路径. 无向图的最短路径其实是源点到该顶点的最少边的数目. 本文假设图的信息保存在文件中,通过读取文件来构造图. ...

  3. 归并排序算法 java 实现

    归并排序算法 java 实现 可视化对比十多种排序算法(C#版) [直观学习排序算法] 视觉直观感受若干常用排序算法 算法概念 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Di ...

  4. 快速排序算法 java 实现

    快速排序算法 java 实现 快速排序算法Java实现 白话经典算法系列之六 快速排序 快速搞定 各种排序算法的分析及java实现 算法概念 快速排序是C.R.A.Hoare于1962年提出的一种划分 ...

  5. 堆排序算法 java 实现

    堆排序算法 java 实现 白话经典算法系列之七 堆与堆排序 Java排序算法(三):堆排序 算法概念 堆排序(HeapSort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,可以利用数组的特 ...

  6. Atitit 电子商务订单号码算法(java c# php js 微信

    Atitit 电子商务订单号码算法(java c# php js  微信 1.1. Js版本的居然钱三爷里面没有..只好自己实现了. 1.2. 订单号标准化...长度16位 1.3. 订单号的结构 前 ...

  7. 基于FP-Tree的关联规则FP-Growth推荐算法Java实现

    基于FP-Tree的关联规则FP-Growth推荐算法Java实现 package edu.test.ch8; import java.util.ArrayList; import java.util ...

  8. 双色球机选算法java实现

    双色球机选算法java实现 一.代码 package com.hdwang; import java.util.Random; /** * Created by admin on 2017/1/10. ...

  9. Floyd算法java实现demo

    Floyd算法java实现,如下: https://www.cnblogs.com/Halburt/p/10756572.html package a; /** * ┏┓ ┏┓+ + * ┏┛┻━━━ ...

随机推荐

  1. Mybatis入门(三)------日志系统

    Mybatis日志系统 简介 Mybatis内置的日志工厂提供日志功能,具体的日志实现有以下几种方式: •SLF4J •Apache Commons Logging •Log4j 2 •Log4j • ...

  2. js实现将时分秒转化成毫秒,将秒转化成时分秒

    // 时间转为毫秒 timeToSec(time) { var hour = time.split('[0] var min = time.split('[1] var sec = time.spli ...

  3. 自己搭建的集群,启动hadoop时slave节点的datanode没有启起来怎么办?

    自己搭建的集群,启动hadoop 集群是,发现slave节点的datanode没有启动,查了资料发现是因为我在启动集群前,执行了这个命令: hadoop namenode -format 这个指令会重 ...

  4. 滴滴AI Labs斩获国际机器翻译大赛中译英方向世界第三

    桔妹导读:深耕人工智能领域,致力于探索AI让出行更美好的滴滴AI Labs再次斩获国际大奖,这次获奖的项目是什么呢?一起来看看详细报道吧! 近日,由国际计算语言学协会ACL(The Associati ...

  5. Vue基础(五)---- 前端路由

    基本结构: ◆ 1.路由的基本概念与原理 ◆ 2.vue-router的基本使用 ◆ 3.vue-router嵌套路由 ◆ 4.vue-router动态路由匹配 ◆ 5.vue-router命名路由 ...

  6. 洛谷 P3177 [HAOI2015]树上染色 树形DP

    洛谷 P3177 [HAOI2015]树上染色 树形DP 题目描述 有一棵点数为 \(n\) 的树,树边有边权.给你一个在 \(0 \sim n\)之内的正整数 \(k\) ,你要在这棵树中选择 \( ...

  7. Google Kick Start 2020 Round B T4 Wandering Robot

    题意 一个\(n \times m\)的矩形空间,起点是\((1,1)\),终点是\((n,m)\). 假设当前位于\((x,y)\): 如果当前位于最后一行,那么下一步只能走向\((x,y+1)\) ...

  8. KUDU 学习笔记

    Kudu 现存系统针对结构化数据存储与查询的一些痛点问题,结构化数据的存储,通常包含如下两种方式: 静态数据通常以Parquet/Carbon/Avro形式直接存放在HDFS中,吞吐能力大,适合离线分 ...

  9. 跟着兄弟连系统学习Linux-【day05】

    day05-20200602 p19.其他文件搜索命令 (百度搜索everything,安装,可以实现Windows秒级搜索文件)Linux中同样可以实现此功能. [locate 文件名]locate ...

  10. JVM学习第三天(JVM的执行子系统)之类加载机制补充

    昨晚没看完,今天继续 系统的类加载器 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间.这句话可以表达得更通俗一些: ...