Java数据结构--图

0、图

某班级的学生信息表如下图所示,每个学生都来自不同的城市,如果老师从重庆出差到南京,顺路要选择部分学生做一次家访,怎么样选择才最合理呢?
在这里插入图片描述

下图是一个简单的模拟城市路线的地图,从重庆出差到南京,如何选择用时最短的路线出差,同时做家访呢?
在这里插入图片描述

注:数字代表往返两城市间所需要的时间(单位:小时)。
要解决该问题,最好的方法是使用数据结构中图的知识。

一、图的概念及基本术语

  • 图:图G是由两个集合V和E所限定的一种数据结构,记作G=(V,E),其中V是定点有限非空集合,E是表示顶点之间关系的边的集合。在图的实际应用中,可能会同时接触若干个图,为了区别不同图的顶点集和边集,常常将图记作G=( V(G),E(V) )。
  • 无向图:在图G中,如果代表边的顶点偶对是无序的,则称G为无向图,如图1.1.1中G1、G2所示。
    有向图:在图G中,如果代表边的顶点偶对是有序的,则称G为有向图,如图1.1.1中的G3、G4所示。
    在这里插入图片描述
  • 完全图:若图中的每两个顶点之间都存在着一条边,称该图为完全图。完全有向图有n(n-1)条边;完全无向图有n(n-1)/2条边。
  • 端点和邻接点:在一个无向图中,若存在一条边(vi,vj),则称vi(顶点i的简称)和vj为此边的两个端点,并称他们互为邻接点。在一个有向图中,若存在一条边<vi,vj>,则称vi和vj为此边的两个端点,也称它们互为邻接点,这里,vi为起点,vj为终点。
    顶点的度、入度和出度:在无向图中,顶点所具有的边的数目称为该顶点的度。在有向图中,顶点v的度又分为入度和出度,以顶点v为终点的入边的数目,称为该顶点的入度。以顶点v为起点的出边的数目,称为该顶点的出度。一个顶点的入度和出度的和为该顶点的度。
  • 子图:设有两个图G =(V(G),E(G))和 G’ = (V(G’ ),E(G’ )),若V(G’ )是V(G)的子集,即V(G’ ) ≤ V(G) 且 E(G’ )是E(G)的子集,即E(G’)包含E(G),称G’是G的子图。
  • 路径和路径长度:在一图G =(V,E)中,从vi,到vj的一条路径是一个顶点序列(vi,vi1,vi2,…,vim,vj),若此图G是无向图,则边(vi,vi1),(vi1,vi2),…,(vim-1,vimv),(vim,vj)属于E(G);若此图是有向图,则(vi,vi1),(vi1,vi2),…,(vim-1,vim),(vim,vj)属于E(G);若此图是有向图,则(vi,vi1),(vi1,vi2),(vim-1,vim),(vim,vj)属于E(G)。路径长度是指一条路径上经过的边的数目。若一条路径上除开始点和结束点可以向同外。其余顶点均不相同,则称此路径为简单路径。
  • 回路或环:若在一条路径上的开始点和结束点为同一个顶点,则此路径被称为回路或环。开始点与结束点相同的简单路径被称为简单环。
  • 连通、连通图和连通分量:在无向图中,若从vi到vj有路径,则称vi和vj是连通的。若图G中任意两个顶点连通,则称G为连通图,否则称为非连通图。无向图G中的极大连通子图称为连通分量。显然,任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。
  • 稠密图、稀疏图:当一个图接近完全图时,则称为稠密图。相反,当一个图含有较少边数时,称为稀疏图。
  • 生成树:连通图G的生成树是指G的包含其全部顶点的一个极小连通子图。极小连通子图时值包含所有顶点并且保证连通的前提下包含原图中最下的边,一个具有n个顶点的连通图G的生成树有且仅有n-1条边,如果少一条边就不是连通图,如果是多一条边就一定有环。但有n-1条边的图就不一定是生成树。
  • 权和网:图中没一条边都可以附有一个对应的树,这种与边相关的树为权。权可以表示从一个顶点到另一个顶点的距离或花费的代价。边上带有权的图称为带权图,也为为网,如图1.1.2中的G8所示。
    在这里插入图片描述

二、图的存储结构

图的结构较为复杂,任意两个顶点间都可能存在联系,因而图的存储方法有很多,如邻接矩阵表示法,邻接表表示法,邻接多重链表和十字链表等。下面讲解其中最常用的邻接矩阵表示法和邻接表表示法。

2.1 邻接矩阵表示法

邻接矩阵是顶点之间相邻的矩阵。其中一个一维数组存储图中各个顶点的信息(顶点值),而顶点的编号隐含地用数组元素的下标表示;而用一个二维数组存储图中边的信息(即顶点之间邻接关系的信息),该二维数组称为图的邻接矩阵。
对于具有n(n ≥ 1)个顶点的图G = (V,E),其顶点编号按顺序1,2,…,n顺序编号,则存储表示图中边的信息的邻接矩阵arcs是一个n阶方阵,其元素定义为:
在这里插入图片描述

图2.1中(a),(b),(c),(d)分别给出了图1.1.2中G5,G6,G7和G8的邻接矩阵存储结构图。
在这里插入图片描述

根据各种图的邻接矩阵,可以得出如下结论:
1)无向图的邻接矩阵具有如下特点:

  • 它是对称矩阵(因为(i,j)=(j,i));
  • 第i行(或第i列)上1元素的个数等于顶点i的度数;
  • 整个矩阵中1元素的个数等于边数的2倍。

2)有向图邻接矩阵有如下特点:

  • 一般情况下,它不是对称矩阵(因为<i, j> ≠ <j, i>);
  • 第i行上1元素的个数等于顶点i的出度;
  • 第i行上1元素的个数等于顶点i的入度;
  • 整个矩阵中1元素的个数等于弧数;
  • 无向图的邻接矩阵与无向图大致相同,只是将1元素改为非零或无穷大元素数(即权值数)即可。
  • 有向图的邻接矩阵的特点与有向图大致相同,只是将1元素改为非零或无穷大元素数即可。
2.2 邻接表表示法

邻接表时图的一种链式存储结构,在邻接表中,为图中每个顶点建立一个单链表。第i个单链表中的结点表示关联于顶点i的边(对有向图则是一顶点i为始点的弧)。表结点的结构根据无权图和有全图而有所不同,无全图的表结点由三个域组成,其中邻接顶点域存储于顶点i邻接的顶点编号(即该顶点在图中的位置),链域指向顶点i的单链表中的表示关联于顶点i的下一个表结点。而对于有全图,为了表示边上有权,则表结点中增设了存储该边上权值得域。每个单链表附设一个头结点,头结点中除了设有指向单链表的第一个表结点的链域外,还设有存储第i个顶点信息的数据域(Data),其结构如图2.2所示。
在这里插入图片描述

一个具有n个顶点的图,就应该有n个这样的单链表。为了能随机访问任一顶点的单链表,通常将n个单链表的头结点构成一个顺序结构(以头结点为元素的一维数组)。上述定义中,左边是无权图的邻接表类型定义,右边是有权图的邻接表类型定义。图2.2中(a)、(b)、(c)分别给出了图1.1.2中G5和G8的邻接表存储结构图和逆邻接表存储结构图。
根据各种图的邻接表存储结构,可以得出结论:
1)对于无权图和无向网的邻接表:

  • 第i个单链表的长度等于顶点i的度数;
  • 总表结点数等于边树的2陪。
    2)对于有向图和有向网的邻接表:
  • 第i个单链表的长度等于顶点i的出度;
  • 总表结点树等于弧数。
    一个具有n个顶点和e条边的无向图(包括无向网),它的邻接表存储结构需要n个头结点和2e个表结点。显然在稀疏(e << n (n-1)/2 )情况下,用邻接表表示图比邻接矩阵节省存储空间。而有向图(包括有向网)只需要n个头结点和e个表结点,比无向图所需存储更少。
    有向图的邻接表表示对求顶点i的出度很方便,只需在遍历第i个单链表过程中对表结点进行记数就可以求得。但要求顶点i的入度却很麻烦,必须遍历邻接表的全部表结点才行。因此,有时为了便于求得顶点i的入度或以顶点i为弧头(终点)的弧,可以建立一个有向图的逆邻接表。所谓逆邻接表,就是为图中每个顶点i建立一个以i为终点的单链表。图2.2(c)所示是图1.1.2中G8的逆邻接表。
    在这里插入图片描述

三、、图的遍历

图的遍历是树的遍历的推广,从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历,亦是将网络结构按某种规则线性化的过程。图的遍历方法有两种:深度优先搜索法和广度优先搜索法。

3.1 深度优先搜索法

深度优先遍历类似于树的先序遍历,它的基本思想是:首先访问指定的起始顶点v,然后选取与v邻接的未被访问的任意一个顶点w,访问之,再选取与w邻接的未被任一顶点,访问之。重复进行如上的访问,当一个顶点所有邻接顶点都被访问过时,则依次退回到最近被访问过的顶点,若它还有邻接顶点未被访问过,则从这些未被访问过的顶点中取其中的一个顶点开始重复上述访间同过程,直到所有的顶点都被访问过为止

示例:对图3.1(a)中无向图进行深度优先搜索遍历。。

对图3.1(a)中无向图进行深度优先搜索遍历的过程如图3.1©所示,其中黑色的实心箭头代表访问方向,空心箭头代表回溯方向,箭头旁的数字代表搜索顺序,顶点a是起点。遍历过程如下:首先访问顶点a,然后
(1)顶点a的未曾访问的邻接点有b、d、e,选择邻接点b进行访问
(2)顶点b的未曾访问的邻接点有c、e,选择邻接点c进行访问
(3)顶点c的未曾访问的邻接点有e、f,选择邻接点e进行访问;
(4)顶点e的未曾访问的邻接点只有f,访问f;
(5)顶点f无未曾访问的邻接点,回溯至e;
(6)顶点e无未曾访问的邻接点,回溯至c;
(7)顶点c无未曾访问的邻接点,回溯至b;
(8)顶点b无未曾访问的邻接点,回溯至a;
(9)顶点a还有未曾访问的邻接点d,访问d;
(10)顶点d无未曾访问的邻接点,回溯至a。

到此,a再没有未曾访问的邻接点,也不能向前回溯,从a出发能够访问的顶点均已访问,并且此时图中再没有未曾访问的顶点,遍历结束。由以上过程得到的遍历序列为:a, b. c, e, f, d对于有向图而言,深度优先搜索的执行过程是一样的,例如图3.1(b)中有向图的深度优先搜索过程如图3.1(d)所示。在这里需要注意的是从顶点a出发深度优先搜索只能访问到a,b,c,e,f,而无法访问到图中所有顶点,所以搜索需要从图中另一个未曾访问的顶点d开始进行新的搜索,即图3.1(d)中的第9步。
在这里插入图片描述

显然从某个顶点v出发的深度优先搜索过程是一个递归的搜索过程,因此可以简单地使用递归算法实现从顶点ν开始的深度优先搜索。然而从ν出发深度优先搜索未必能访问到图中所有顶点,因此还需找到图中下一个未曾访问的顶点,从该顶点开始重新进行搜索。深度优先搜索算法的具体实现如下:

public class GraphDFS {
	static int[][] graph = new int[100][100];
	static int[] visited = new int[100];
	static int n = 6;
	static int predfn,postdfn;
	static List<Integer> queue = new ArrayList<>();
	
	public static void DFS() {
		int flag = 0;
		for(int i =0; i < n; i++) {
			if (visited[i] == 0) {
				flag++;
				dfs(i, flag);
			}
		}
	}
	
	private static void dfs(int i, int flag) {
		visited[i] = 1;
		System.out.println(" " + (char)(i+1+96));
		for (int j = 0; j < n; j++) {
			if (graph[i][j] == 1 && visited[j] == 0) {
				dfs(i, flag);
			}
		}
	}
	
	public static void main(String[] args) {
		graph[0][1] = graph[1][0] = 1; 
		graph[0][3] = graph[3][0] = 1; 
		graph[0][4] = graph[4][0] = 1; 
		graph[1][2] = graph[2][1] = 1; 
		graph[1][4] = graph[4][1] = 1;
		graph[2][4] = graph[4][2] = 1;
		graph[2][5] = graph[5][2] = 1;
		graph[4][5] = graph[5][4] = 1;
		DFS();
	}
	
}
3.2 广度优先搜索法

广度优先遍历(也称为宽度优先搜索遍历)类似于树的层次遍历,它的基本思想是:首先访问指定的起始顶点v,然后选取与y邻接的全部顶点w1,w2,…wt,再依次访与w1,w2,…wt邻接的全部顶点(已被访问的顶点除外),再从这些被访问的顶点出发,逐次访问与它们邻接的全部顶点(已被访问的顶点除外)。依次类推,直到所有顶点都被访问为止。
示例:对图3.1(a)中无向图进行广度优先搜索遍历。
对3.1(a)中无向图进行广度优先遍历的搜索过程如图3.2(a)所示,根据广度优先搜索遍历的算法,假定a为初始点,首先访问结点a,接下来访问a的邻接结点,因为a的邻接结点b,d,e均未被访问过,访问结点b,d,e。访问结点a的邻接结点之后,再找a的第一个邻接结点b的未被访问过的邻接结点,结点b的邻接结点有d,e,结点d未被访问过,访问d结点,结点e被访问过了,跳过。接下来找a的第二个邻接结点d的未被访问过的邻接结点,没有。再下来找a的第三个邻接结点e的未被访问过的邻接结点,结点e的邻接结点有f,未被访问过,访问f结点。所以对图3.2(a)中有向图的深度优先搜索遍历序列为:a,b,d,e,c,f。同样,在这里从顶点a出发广度优先搜索只能访问到a,b,e,c,f,所以搜索需要从图的另一个未曾访问的顶点d开始进行新的搜索,即图3.2(b)中的第5步。
在这里插入图片描述

在广度优先遍历中,若对x的访问先于y,则对x的邻接结点的访问先于y的邻接结点的访问。也就是说广度优先遍历邻接结点具有“先进先出”的特征。因此,为了保证结点这种先后关系,可采用队列暂存那些访问过的结点。广度优先搜索算法的具体实现如下:

public class GraphBFS {
	static int[][] graph = new int[100][100];
	static int[] visited = new int[100];
	static int n = 6;
	static int predfn,postdfn;
	static List<Integer> queue = new ArrayList<>();
	
	public static void BDF() {
		int flag = 0;
		for (int i = 0; i < n; i++) {
			if (visited[i] == 0) {
				flag++;
				bfs(i, flag);
			}
		}
	}
	
	public static void bfs(int i, int flag) {
		visited[i] = 1;
		queue.add(i);
		System.out.println(" " + (char)(i+1+96));
		while(!queue.isEmpty()) {
			int v = queue.remove(queue.size() - 1);
			for (int j = 0; j < n; j++) {
				if (graph[v][j] == 1 && visited[j] == 0) {
					visited[j] = 1;
					queue.add(j);
					System.out.println(" " + (char)(j+1+96));
				}
			}
		}
	}
	
	public static void main(String[] args) {
		graph[0][1] = graph[1][0] = 1; 
		graph[0][3] = graph[3][0] = 1; 
		graph[0][4] = graph[4][0] = 1; 
		graph[1][2] = graph[2][1] = 1; 
		graph[1][4] = graph[4][1] = 1;
		graph[2][4] = graph[4][2] = 1;
		graph[2][5] = graph[5][2] = 1;
		graph[4][5] = graph[5][4] = 1;
		BDF();
	}
}

四、图的应用

4.1 最小生成树
4.1.1 生成树

无回路的图称为树或自由树或无根数。若连通图G有n个顶点,取G中n个顶点, 取连接n个顶点的n-1条边且无回路的子图称为G的生成树。满足这个定义的生成树可能有多棵,即生成树不唯在对具有n个顶点的连通图进行遍历时,要访问图中的所有顶点,在访问n个顶点过程中一定经过n-1条边,由深度优先遍历和广度优先遍历所经过的n-1条边是不同的,通常把由深度优先遍历所经过的n-1条边和n个顶点组成的图形称为深度优先生成树。而由广度优先遍历所经过的n-1条边和n个顶点组成的图形称为广度优先生成树。

4.1.2 最小生成树

图的生成树不是唯一的,即一个图可以产生若干棵生成树。对于带边权的图来说同样可以有许多生成树,通常把树中边权之和定义为树的权,则在所有生成树中树权最小的那棵生成树就是最小生成树。
求最小生成树的基本算法有普里姆(Prim)算法和克鲁斯卡尔( Kruskal)算法两种如图4.1所示为无向图及其邻接矩阵。

在这里插入图片描述

示例:根据图4.1给出的无向图,使用善里姆算法生成最小生成树
假设N=(V,E)是具有n个顶点的连通图,T=(U,TE)为N的最小生成树,U是T的顶点集,TE是T的边集。Prim算法的基本思想是:

  • 1)初始化:U={U0},TE={}, ( 即u0为决策的出发顶点为空集 )
  • 2)在所有的u∈U,v∈V-U的边(u,v)中选取一条权值最小的边(u’,v’),并将v’并入U,(u’, v’)并入TE;
    重复步骤(2),直至U=V为止
    当算法结束时TE中正好有n-1条边,此时T=(U,TE)就是N的一棵最小生成树。图4.2给出了根据Prim算法构造图4.1无向网络N的最小生成树的过程(出发顶点为v1)。
    在这里插入图片描述

下面是Java实现其功能,其中图的信息是用邻接矩阵存储的,0表示结点自己和自己的边,10000表示结点间 无边,具体代码如下:

public class MST {
		public int[][] graph;
		public int v;
		public int[][] tree;
		public boolean[] s;

		public void input (int[][] graph, int v){
			this.graph=graph;
			this.v=v;
			tree=new int[graph.length-1][];
			s=new boolean[graph.length];
			for (boolean i: s) i = false;
			s[v] = true;
			calculate();
		}

		public void calculate() {
			for (int i = 0; i < graph.length - 1; i++) {
				int[][] edge = { {0, 0, 10000, }, };
				for (int j = 0; j < graph.length; j++) {
					for (int k = 0; s[j] == true && k < graph.length; k++) {
						if (s[k] == false && graph[j][k] < edge[0][2]){
							edge[0][0] = j;
							edge[0][1] = k;
							edge[0][2] = graph[j][k];
						}
					}
				}
				tree[i] = edge[0];
				s[tree[i][1]] = true;
			}
		}

		public int[][] getTree() {
			return this.tree;
		}

	public static void main(String[] args) {
		int[][] graph = { 
				{5, 0, 3, 6, 10000, 1, 5, 10000},
				{10000, 3, 0, 4, 10000, 8},
				{1, 6, 4, 0, 6, 5},
				{5, 10000, 10000, 6, 0, 2},
				{10000, 10000, 6, 5, 2, 0}
		};
		
		int v = 0;
		MST miniSpanTree = new MST();
		miniSpanTree.input(graph, v);
		int[][] tree = miniSpanTree.getTree();
		for (int i = 0; i < graph.length - 1;i++) {
			System.out.println("边:" + tree[i][0] + "-" + tree[i][1] + "权值:" + tree[i][2]);
		}
	}

}

示例:根据图4.1给出的无向图,使用Kruscal算法生成最小生成树。

若N=(V, E)是具有n个顶点的连通网,T=(U,TE)为N的最小生成树,其求解的和步骤是:
(1)初始化:U=V,TE={}
(2)在网络N中选择一条权值最小的边(u,v)。若将其加入TE不产生回路,则将其并入TE;否则就舍弃掉该边。对这条被选择的边(u,v)经过判断处理后,则从网络N的边集E中删除之;
重复步骤(2),直至TE正好含有n-1条边为止。
图4.3给出了根据 Kruscal算法构造图4.1所示无向网络N的最小生成树的过程

在这里插入图片描述

为了便于对图边按权值比较排序,设计边类class Bian是实现了comparable接口的比较方法,具体代码如下:

public class Bian implements Comparable{
    private int first, second;  // 表示一条边的两个节点
    private int value;  // 权值

    public Bian(int first, int second, int value){
        this.first = first;
        this.second = second;
        this.value = value;
    }

    public int getFirst() {
        return first;
    }

    public int getSecond() {
        return  second;
    }

    public int getValue(){
        return value;
    }


    @Override
    public int compareTo(Object o) {
        return value > ((Bian)o).value ? 1 : (value == ((Bian)o).value ? 0 : -1);
    }

    @Override
    public String toString() {
        return "Bian{" +
                "first=" + first +
                ", second=" + second +
                ", value=" + value +
                '}';
    }
}
public class KMST {
    // 存放每一个数组中的节点的数组
    static ArrayList<ArrayList> list = new ArrayList<>();
    // 对应存放数组中的边的数组
    static ArrayList<ArrayList> bianList = new ArrayList<>();

    public static void check(Bian b) { // 检查在哪个数组中
        if (list.size() == 0) {
            ArrayList<Integer> sub = new ArrayList<>();
            sub.add(b.getFirst());
            sub.add(b.getSecond());

            list.add(sub);
            ArrayList<Bian> bian = new ArrayList<>();
            bian.add(b);
            bianList.add(bian);
            return;
        }

        int first = b.getFirst();
        int shuyu1 = -1;
        int second = b.getSecond();
        int shuyu2 = -1;
        for (int i = 0; i < list.size(); i++) { // 检查两个节点分别属于哪个数组
            for (int m = 0; m < list.get(i).size(); m++) {
                if (first == (Integer)list.get(i).get(m)) {
                    shuyu1 = i;
                }
                if (second == (Integer)list.get(i).get(m)){
                    shuyu2 = i;
                }
            }
        }


        if (shuyu1 == -1 && shuyu2 == -1) { // 表示这两个节点都没有需要新加入
            ArrayList<Integer> sub = new ArrayList<>();
            sub.add(b.getFirst());
            sub.add(b.getSecond());
            list.add(sub);
            ArrayList<Bian> bian = new ArrayList<>();
            bian.add(b);
            bianList.add(bian);
        }

        if (shuyu1 == -1 && shuyu2 != -1) {
            list.get(shuyu2).add(first);
            bianList.get(shuyu2).add(b);
        }

        if (shuyu2 == -1 && shuyu1 != -1) {
            list.get(shuyu1).add(first);
            bianList.get(shuyu1).add(b);
        }

        if (shuyu1 == shuyu2 && shuyu1 != -1) { // 表述两个在同一个组中形成环
            // 这里什么也不做,表示直接丢弃边b
        }

        if (shuyu1 != shuyu2 && shuyu1 != -1 && shuyu2 != -1) {
            for (int i = 0; i < list.get(shuyu2).size(); i++) {
                list.get(shuyu1).add(list.get(shuyu2).get(i));
            }
            list.remove(shuyu2);
            for (int i = 0; i < bianList.get(shuyu2).size(); i++) {
                bianList.get(shuyu1).add(bianList.get(shuyu2).get(i));
            }
            bianList.get(shuyu1).add(b);
            bianList.remove(shuyu2);
        }
    }

    public static void show() {
        for (int i = 0; i < bianList.get(0).size(); i++) {
            System.out.println(bianList.get(0).get(i));
        }
    }


    public static void main(String[] args) {
        ArrayList<Bian> l = new ArrayList<>();
        l.add(new Bian(1, 4, 1));
        l.add(new Bian(1, 2, 5));
        l.add(new Bian(1, 5, 5));
        l.add(new Bian(2, 3, 3));
        l.add(new Bian(2, 6, 6));
        l.add(new Bian(3, 4, 4));
        l.add(new Bian(3, 6, 6));
        l.add(new Bian(4, 5, 6));
        l.add(new Bian(4, 6, 5));
        l.add(new Bian(5, 6, 2));

        Collections.sort(l);
        for (int i = 0; i < l.size(); i++) {
            KMST.check(l.get(i));
        }
        KMST.show();
    }
}
4.2 最短路径

如果用顶点表示城市,边表示城市之间的道路,边上的权表示道路的里程(或所需时,或交通费用等),考虑到交通的有向性(如航运时的顺水、逆水等情况),则可以用一个有向网络表示一个交通网络。如果从顶点vi沿着有向边可以到达顶点vj,就称从vi到vj有一条路径,且称路径上的第一个顶点vi为源点( Sourse),称路径上最后一个顶点vj为终点( Destination),称这条路径上所有有向边的权值之和为这条路径的长度。
对于这样一个交通网络可以研究两类最短路径问题。

  • 第一类最短路径问题是:固定图中一个顶点作为源点,而将图中其余的顶点分别作为终点,则我们关心从源点到其余任一个终点间是否存在最短路径?如果存在,则最短路径是什么?最短路径长是多少?这就是单源最短路径问题。
  • 第二类最短路径问题是:如果站在运输管理的全局来看,我们关心每对顶点之间的最短路径是什么?它们的路径长是多少?这就是每对顶点间的最短路径问题。

通常采用狄克斯特拉(Di jkstra)算法求一个顶点到其余各顶点的最短路径。针对单目标最短路径问题:找出图中每个节点v到某指定节点u的最短路径。只需将图中每条边反向,就可将这一问题转变为单源最短路径问题;而对所有节点之间的最短路径问题:对图中每对节点u和v,找到节点u到v的最短路径问题。这一问题可用每个节点作为源点调用一次单源最短路径问题算法予以解决。 对于一个有向带权图,利用Di jkstra算法求最短路径。根据 Dijkstra算法求解最短路径过程如下 + (1)初始时,集合S只包含源点,即S={v},v的距离是0。集合U包含除v以外的其他节点,集合U中节点u的距离为边上的权或者为∞ + (2)从集合U中选取节点k,使得v到k的最短路径长度最小,将k加入集合S中。 + (3)以k为新的中间点,修改集合U中各节点的距离:如果从源点v到节点u的距离比原来的距离还短,则修改节点u的距离值,修改后的距离值是节点k的距离加上

在这里插入图片描述

针对图4.2.1,求解v1结点为始点的所有最短路径的过程如下

第一次:

v1到v~2~ v1v3v1到v4v1到v5v1到v6
1030100

所以,当前最短路径为10。然后检查能否从v3结点绕到其余未访问的结点且距离更近。
由于v1到v3再到v4的距离为 10+50=60,小于原有的∞,所以v1到v4的距离更新为60。

v1到v~2~ v1v3v1到v4v1到v5v1到v6
106030100

第二次:
除v2结点外,当前最短路径为v1到v5的30.然后检查能否从v5结点绕到其余未访问的结点且距离更近。
由于v1到v5再到v4的距离为30+20=50,小于原有的60,所以v1到v4的距离更新为50,v1到v5再到v6的距离为30+60=90,小于原有的100,所以v1到v6的距离更新为90。

v1到v~2~ v1v3v1到v4v1到v5v1到v6
10503090

第三次:
除v3,v5结点外,当前最短路径为v1到v4的50。然后检查能否从v4结点绕到其余未访问的结点且距离更近。
由于v1到v4再到v6的距离为50+10=60,小于原有的90,所以v1到v6的距离更新为60。

v1到v~2~ v1v3v1到v4v1到v5v1到v6
10503060

第四次:
除v3,v5,v4结点外,当前最短路径为v1到v6的60。然后检查能否从v6结点绕到其余未访问的结点且距离更近。
由于不能从v6结点绕到其余未访问的结点。所以v1到v6的最短距离为60。

v1到v~2~ v1v3v1到v4v1到v5v1到v6
10503060

第五次:
除v3,v5,v4,v6结点外,当前最短路径为v1到v2的∞。已经没有未访问的结点,算法结束,最后的结果为:

通过上述步骤。可以得到v1作为起始点到其他所有结点的最短路径依次为:

10路径为<v1,v3>
30路径为<v1,v5>
50路径为<v1,v5>,<v5,v4>
60路径为<v1,v5> ,<v5,v4> <v4,v6>
表示无路径>

下面是用Java实现带权有向图4.2.1的v1结点为始点的所有最短路径的代码。

public class ShortestPath {
    public int[][] graph;
    public int v;
    public int[][] path;

    public void input(int[][] graph, int v) {
        this.graph = graph;
        this.v = v;
        calculate();
    }

    public void calculate() {
        this.path = new int[this.graph.length - 1][];
        int[] s = new int[this.graph.length];
        for (int i : s) i = 0; s[v] = 2;
        for (int i = 0; i < this.graph.length - 1; i++) {
            int[][] pointToSet = {{1, 1000, -1}, {1, 1000, -1}};
            for (int j = 0; j < this.graph.length; j++) {
                if (s[j] == 0 && this.graph[v][j] < pointToSet[0][1]) {
                    pointToSet[0][1] = graph[v][j];
                    pointToSet[0][0] = j;
                }
            }
            int[][] setToSet = {{1, 1000, -1}};
            for (int j = 0; j < i; j++) {
                pointToSet[1][1] = 1000;
                pointToSet[1][2] = j;
                for (int k = 0; k < this.graph.length; k++) {
                    if (s[k] == 0 && this.graph[path[j][0]][k] < pointToSet[1][1]) {
                        pointToSet[1][1] = this.graph[path[j][0]][k];
                        pointToSet[1][0] = k;
                    }
                }
                pointToSet[1][1] = pointToSet[1][1] + path[j][1];
                if (pointToSet[1][1] <setToSet[0][1]) {
                    setToSet[0] = pointToSet[1];
                }
            }
            if (pointToSet[0][1] < setToSet[0][1]) {
                path[i] = pointToSet[0];
            } else {
                path[i] = setToSet[0];
            }
            s[path[i][0]] = 1;
        }
    }

    public int[][] getPath() {
        return path;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {10000, 10000, 10, 10000, 30, 100},
                {10000, 10000, 1000050, 10000, 10000, 10000},
                {10000, 10000, 10000, 20, 10000, 60},
                {10000, 10000, 10000, 10000, 10000, 10000}
        };

        int v = 0;
        ShortestPath shortestPath = new ShortestPath();
        shortestPath.input(graph, v);
        int[][] path = shortestPath.getPath();
        for (int i = 0; path[i][1] != 1000; i++) {
            System.out.println("起点:" + v + ";终点:" + path[i][0] + "; 长度:" + path[i][1] + "; 终点前驱节点:" + path[i][2]);
        }
    }
}
4.3 AOV网与拓扑图

有一类特殊的有向图在实际中有一些重要的应用其特殊性表现在它不存在由有向边构成的有向环。这种无环的有向图称作有向无环图,简称为DAG图。DAG图又分为AOV网和AOE网。对于AOV网我们将研究拓扑排序问题,对于AOE网我将研究关键路径问题。

4.3.1 AOV网

设 G = {V,E} 是一个具有n个顶点的有向无环图,V中顶点序列v1,v2,…vn称为一个拓扑序列, 当且仅当该顶点序列满足下列条件:若<vi, vj>,则在序列中顶点 vi必须排在顶点vj之前。AOV网中的弧表示孤尾活动与弧头活动之间存在的制约关系。
在解决拓扑排序的实际问题时,有向无环图通常用来表示活动之间的先后关系。顶点表示活动,有向边表示活动的先后关系。若活动u的完成是活动v可以开始的条件,则在顶点u和v之间有一条边<u,v>。若从顶点u到顶点v有一条有向路径,则u是v的前驱,v是u的后继。这样的有向图称为AOV网。
在AOV网中, 不应该出现有向环, 因为存在环意味着某项活动以自身为先决条件。

4.3.2 拓扑排序

在一个有向无环图中找到一个拓扑序列的过程称为拓扑排序,拓扑排序过程如下所示:

  • (1) 在AOV网中选取一个没有前趋(即入度为0) 的顶点并输出之;
  • (2) 从AOV网中删除该顶点以及从该顶点发出的所有有向边(可以用有向边射入的顶点人度减1实现);

重复步骤(1)、(2),直至全部顶点输出完毕(称为拓扑排序成功),或者再也找
不到没有前趋的顶点(对应于拓扑排序失败,即图中存在有向环)为止。
对于AOV网, 采用栈存储结构实现拓扑排序。根据拓扑排序算法和邻接矩阵的特点,
将AOV网表示为graph[n] [] , 其中graph[i] [0] 为节点i的人度, 其余为其后继节点,
生成一个拓扑排序序列list。

例:根据有向图4.3(a),实现并生成其拓扑排序序列。
AOV网产生拓扑排序序列的过程如图4.3所示, 得到拓扑排序序列为:b, a, e, c, d, f。

在这里插入图片描述

要实现拓扑排序,先实现结点信息存放类,具体代码如下:

class Vertex(//图中的节点
	private Object value;
	Vertex(Object value)(
		this.value=value;
	}
	Object value() {
		return value;
	}
	@Override 
	public String toString(){
		return "" +value ;
	}
}

接下来实现拓扑排序, AOV图的结点信息存放于Vertex数组中, 结点的插人通过add方法实现, 图的有向边的联通标志存放邻接矩阵Object[][] adjMat中,结点的连接通过connect方法填充邻接矩阵,具体代码如下:

public class Topology { // 用邻接矩阵法表示的图
    private Vertex[] vertexs;
    private Object[][] adjMat;  // 记载是否联通
    private int length = 0;
    private static Object CONN = new Object();// 标志是否联通

    public Topology(int size) {
        this.vertexs = new Vertex[size];
        this.adjMat = new Object[size][size];
    }

    void add(Object value) {
        // assert length  <= vertexs.length;
        this.vertexs[length++] = new Vertex(value);
    }

    void connect(int from, int to) {
        // assert from  < length;
        // assert to  < length;
        adjMat[from][to] = CONN; // 标志联通
    }

    void remove(int index) {    //   移除指定的顶点
        remove(vertexs, index); //   在顶点数组中删除指定位置的下标
        for (Object[] bs : adjMat) remove(bs, index);       // 邻接矩阵中删除指定的列
        remove(adjMat, index);  //  在邻接矩阵中删除指定的行
        length--;
    }

    private void remove (Object[] a, int index) {
        for (int i = index; i < length - 1; i++) a[i] = a[i+1];
    }

    int noNext(){
        int result = -1;

        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                if (adjMat[i][j] == CONN) continue;
            }
            return i;
        }
        return -1;
    }

    Object[] topo() {
        Object[] result = new Object[length];   // 准备结果数组
        int index;
        int pos = length;
        while (length > 0) {
            index = noNext();   // 找到第一个没有后续的节点
            assert index != -1 : "图中存在环";
            result[--pos] = vertexs[index]; // 放入结果中
            remove(index);
        }
        return result;
    }

    public static void main(String[] args) {
        Topology g = new Topology(20);
        g.add('a'); // 加入结点
        g.add('b');
        g.add('c');
        g.add('d');
        g.add('e');
        g.add('f');
        
        g.connect(0, 2);    //连接结点
        g.connect(0, 4);
        g.connect(0, 5);
        g.connect(1, 4);
        g.connect(2, 3);
        g.connect(3, 5);
        g.connect(4, 3);
        g.connect(4, 5);
        
        for (Object o : g.topo()) {
            System.out.println(o + " ");
        }

        System.out.println();
    }
    
}
class Vertex{   //图中的节点
    private Object value;

    Vertex(Object value){
        this.value=value;
    }
    Object value() {
        return value;
    }

    @Override
    public String toString(){
        return "" +value ;
    }
}
4.4 AOE网与关键路径
4.4.1 AOE网

如果在带权的有向图中,用顶点表示事件,用有向边表示活动,边上的权表示活动持续的时间,这种带权的有向图称为AOE网( Activity On Edge Network)。AOE网中顶点所表示的事件实际上就是它的入边所表示的活动都已完成,它的出边所表示的活动均可以开始这样一种状态。
  AOE网可以用来估算一件工程所需的完成时间。通常在AOE网中列出完成预定工程所需进行的活动(子工程或工序)及它所需的完成时间;要发生哪些事件及这些事件与活动之间的关系。例如图4.4所示的AOE网含有11项活动,9个事件。事件v1表示整个工程的开始;事件v9表示整个工程的结束;事件v5表示活动a4和a5已经完成,活动a7、a8可以开始。有向边上的权表示执行对应于该边的活动所需的时间(如天数等),如活动a3需要5天,活动a7需7天等。

在这里插入图片描述

表示实际工程的A0E网应该是无回路的,并且只有一个表示整个工程开始的顶点(称作源点,其入度为0)和一个表示整个工程结束的顶点(称为汇点,其出度为0)

4.4.2 关键路径

在AOE网中,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径。
  完成整个工程的最短时间就是网中关键路径的长度,也就是网中关键路径上各活动持续时间的综合,把关键路径上的活动称为关键活动。
在图G={V,E}中,假设V={v0,v1,…,vn-1),其中n = |V|,v0是源点,vn-1是源点。为求关键活动,我们定义以下变量:

  • 事件vi的最早可能开始时间ve[i]:是从源点v0到顶点vi的最长路径长度
  • 活动ak的最早可能开始时间e[k]:设活动ak在边<vi, vj>上,则e[k]是从源点v0到顶点vi的最长路径长度。因此,e[k] = ve[i]。
  • 事件vi的最迟允许开始时间v1[i]:是在保证汇点vn-1在ve[n-1]时刻完成的前提下,事件v允许的最迟开始时间。
  • 活动ak的最迟允许开始时间1[k]:设活动ak在<vi, vj>边上,l[k]是在不会引起时间延误的前提下,该活动允许的最迟开始时间。l[k] = Vl[j] - dur(<i, j>)。其中,dur(<i, j>) = weight(<vi, vj>)是完成ak所需的时间。

时间余量l[k] - e[k]:表示活动ak的最早可能开始时间和最迟允许开始时间的时间余量。1[k] = e[k] 表示活动ak是没有时间余量的关键活动。
 为找出关键活动,需要求各个活动的e[k]与1[k],以判别是否是1[k] = e[k]。为求得e[k]与[1k],需要先求得从源点v0到各个顶点vi的ve[i]和vl[i]。为求ve[i]和v1[i]需分两步进行:

  • (1)从ve[0] = 0 开始向汇点方向推进ve[j] = Max{ ve[i] + dur(<i, j>) | vi是vj的所有直接前趋顶点 };
  • (2)从v1[n-1] = ve[n-1]开始向源点方向推进vl[i] = Min { vl[j] -dur(<i, j>) | vj是vi的所有直接后续顶点 }。
     这两个递推公式的计算必须分别在拓扑有序和逆拓扑有序的前提下进行。也就是说,ve[i] 必须在其所有直接前驱顶点的最早开始时间求得之后才能进行;vl[i]必须在其所有直接后续顶点的最迟开始时间求得之后才能进行。因此,可以在拓扑序列的基础上求解关键活动。

例:在图4.4(a)所示的AE网络上,实现其关键路径的求解。
在图4.4(a)所示的AOE网络上找关键路径的思想如下:

  • 求解AOE网中所有事件的最早发生时间ve()。
  • 求解AOE网中所有事件的最迟发生时间v1()。
  • 求解AOE网中所有活动的最早开始时间e()。
  • 解AOE网中所有活动的最迟开始时间l()。
  • 求解AOE网中所有活动的d()。
  • 找出所有d()为0的活动构成关键路径。
  • 求解AOE网中的关键路径。根据AOE网的特性和求解关键路径的方法,将所有可能的关键路径存储于二维数组path[][]中, pathic[i][0]和 path[i][1]表示边的节点path[i][2]表示权值。

下面用Java实现其功能,其中AOE网存放于邻接链表中

public class KeyPath {
    public int[][] graph;
    public int[][] path;
    public int len;
    public  void  input(int[][] graph) {
        this.graph = graph;
        this.path = new int[graph.length - 1][];
        this.len = 0;
        calculate();
    }

    public void  calculate() {
        int[] ve = new int[this.graph.length];
        Stack<Integer> stack1 = new Stack<>();
        Stack<Integer> stack2 = new Stack<>();

        int i, j, v;
        for (int t : ve) t = 0;
        stack1.push(0);

        while (stack1.empty() != true) {
            v = stack1.pop();
            for (i = 1; i < this.graph[v].length; i = i+2) {
                j = graph[v][i];
                if (--graph[i][0] == 0) {
                    stack1.push(j);
                }
                if (ve[v] + graph[v][i+1] > ve[j]) {
                    ve[j] = ve[v] + graph[v][i+1];
                }
            }
            stack2.push(v);
        }

        int[] v1 = new int[graph.length];
        for (i = 0; i <graph.length; i++) {
            v1[i] = 1000;
        }
        v1[graph.length - 1] = ve[graph.length - 1];
        while (stack2.empty() != true) {
            v = stack2.pop();
            for (i = 1; i < graph[v].length; i = i + 2) {
                j = graph[v][i];
                if (v1[j] - graph[v][i+1] < v1[v]) {
                    v1[v] = v1[j] - graph[v][i+1];
                }
            }
        }

        for (v = 0; v < graph.length - 1; v++) {
            for (i=1;i<graph[v].length;i = i + 2) {
                j = graph[v][i];
                if (ve[v] == (v1[j] - graph[v][i+1])) {
                    int[][] p = { {v, j, graph[v][i+1], },  };
                    path[len++] =p[0];
                }
            }
        }
    }

    public int[][] getPath() {
        return path;
    }

    public int getLen() {
        return len;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {0, 1, 6, 2, 4, 3,5},
                {1, 4, 1},
                {1, 4, 1},
                {1, 5, 2},
                {2, 6, 9, 7, 7},
                {1, 7, 4},
                {1, 8, 2},
                {2, 8, 4},
                {2}
        };

        int[][] path;
        KeyPath keyPath = new KeyPath();
        keyPath.input(graph);
        path = keyPath.getPath();
        for (int i = 0; i < keyPath.getLen(); i++) {
            System.out.println("边:" + path[i][0] + "-" + path[i][1] + "权值:" + path[i][2]);
        }

    }
}
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

书香水墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值