上一节我们已经看到了图的边可以有方向,这一节里,我们将探讨边的另一个特性:权值。例如,如果带权图的顶点代表城市,边的权可能代表城市之间的距离,或者城市之间的路费,或者之间的车流量等等。
带权图归根究底还是图,上一节那些图的基本操作,例如广度优先搜索和深度优先搜索等都是一样的,在这一节里,我们主要来探讨一下带权图的最小生成树最短路径问题。
最小生成树问题
首先探讨下最小生成树问题,它与上一节所提到的最小生成树不同。上一节的最小生成树是个特例,即所有边的权值都一样。那么算法如何设计呢?建议用优先级队列来实现这个反复选择最小的路径,而不是链表或数组,这是解决最小生成树的有效方式。在正式的程序中,优先级队列可能基于堆来实现(关于堆,可参见第8节内容),这会加快在较大的优先级队列中的操作。但是在本例中,我们使用数组实现优先级队列,仅仅为了说明算法。算法要点如下:
从一个顶点开始,把它放入树的集合中,然后重复做下面的事情:
1. 找到从最新的顶点到其他顶点的所有边,这些顶点不能在树的集合中,把这些边放入优先级队列中。
2. 找出权值最小的边,把它和它所达到的顶点放入树的集合中。
重复这些步骤,直到所有的顶点都在树的集合中,这时工作完成。
下面先看一下最小生成树的代码,然后再解释一些细节上的问题://边界路径类,主要记录了边的始末顶点,以及边的权值
class Edge {
public int srcVert; //index of vertex starting edge
public int destVert; //index of vertex ending edge
public int distance; //distance from src to dest
public Edge(int sv, int dv, int d) {
srcVert = sv;
destVert = dv;
distance = d;
}
}
//自定义优先队列,用来存储边
class PriorityQ {
private final int SIZE = 20;
private Edge[] queArray; //存储边界的数组
private int size;
public PriorityQ() {
queArray = new Edge[SIZE];
size = 0;
}
public void insert(Edge item) { //有序的插入边界
int j;
for(j = 0; j < size; j++) { //找到插入的位置,从0到size-1,逐渐减小
if(item.distance >= queArray[j].distance)
break;
}
//比item.distance小的往后挪一位,给腾出个空间
for(int k = size-1; k >= j; k--) {
queArray[k+1] = queArray[k];
}
queArray[j] = item; //插入item
size++;
}
public Edge removeMin() { //删除最小的边界并返回
return queArray[--size];
}
public void removeN(int n) { //删除n位置的边界
for(int j = n; j < size-1; j++) {
queArray[j] = queArray[j+1];
}
size--;
}
public Edge peekMin() { //返回最小边界,不删除
return queArray[size-1];
}
public Edge peekN(int n) { //返回n位置的边界
return queArray[n];
}
public int size() {
return size;
}
public boolean isEmpty() {
return (size == 0);
}
public int find(int findDex) { //寻找特定disVert的边界索引
for(int j = 0; j < size; j++) {
if(queArray[j].destVert == findDex)
return j;
}
return -1;
}
}
//带权图类
public class WeightedGraph {
private final int MAX_VERTS = 20; //最大顶点数
private final int INFINITY = 100000; //最远距离...表示无法达到
private Vertex[] vertexArray; //存储顶点的数组
private int adjMat[][]; //存储顶点之间的边界
private int nVerts; //顶点数量
private int currentVert; //当前顶点索引
private PriorityQ thePQ; //存储边的优先级队列
private int nTree; //最小生成树中的顶点数量
public WeightedGraph() {
vertexArray = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][MAX_VERTS];
for(int i = 0; i < MAX_VERTS; i++) {
for(int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = INFINITY; //初始化所有边界无穷远
}
}
thePQ = new PriorityQ();
}
public void addVertex(char lab) { //添加顶点
vertexArray[nVerts++] = new Vertex(lab);
}
public void addEdge(int start, int end, int weight) {//添加带权边
adjMat[start][end] = weight;
adjMat[end][start] = weight;
}
public void displayVertex(int v) {
System.out.print(vertexArray[v].label);
}
/*
* 带权图的最小生成树,要选择一条最优的路径
*/
public void MinSpanningTree() {
currentVert = 0; //从0开始
while(nTree < nVerts-1) { //当不是所有节点都在最小生成树中时
//isInTree是上一节Vertex类中新添加的成员变量 private boolean isInTree;
//表示有没有加入到树中,初始化为false
vertexArray[currentVert].isInTree = true; //将当前顶点加到树中
nTree++;
//往PQ中插入与当前顶点相邻的一些边界
for(int i = 0; i < nVerts; i++) {
if(i == currentVert) //如果是本顶点,跳出
continue;
if(vertexArray[i].isInTree) //如果顶点i已经在树中,跳出
continue;
int distance = adjMat[currentVert][i]; //计算当前顶点到i顶点的距离
if(distance == INFINITY)
continue; //如果当前顶点与i顶点无穷远,跳出
putInPQ(i, distance); //将i节点加入PQ中
}
if(thePQ.size() == 0) { //如果PQ为空,表示图不连接
System.out.println("Graph not connected!");
return;
}
Edge theEdge = thePQ.removeMin();
int sourceVert = theEdge.srcVert;
currentVert = theEdge.destVert;
System.out.print(vertexArray[sourceVert].label);
System.out.print(vertexArray[currentVert].label);
System.out.print(" ");
}
}
private void putInPQ(int newVert, int newDist) {
int queueIndex = thePQ.find(newVert);//判断PQ中是否已经有到相同目的顶点的边界
if(queueIndex != -1) { //如果有则与当前顶点到目的顶点的距离作比较,保留短的那个
Edge tempEdge = thePQ.peekN(queueIndex);//get edge
int oldDist = tempEdge.distance;
if(oldDist > newDist) { //如果新的边界更短
thePQ.removeN(queueIndex); //删除旧边界
Edge theEdge = new Edge(currentVert, newVert, newDist);
thePQ.insert(theEdge);
}
}
else { //如果PQ中没有到相同目的顶点的边界
Edge theEdge = new Edge(currentVert, newVert, newDist);
thePQ.insert(theEdge);//直接添加到PQ
}
}
}
算法在while循环中执行,循环结束条件是所有顶点都已在树中。
1. 当前顶点放在树中。
2. 连接这个顶点的边放到优先级队列中(如果合适)。
3. 从优先级队列中删除权值最小的边,这条边的目的顶点变成当前顶点。
再看看这些步骤的细节:1中,通过标记currentVert所指顶点的isInTree字段来表示该顶点放入树中,2中,连接这个顶点的边插入优先级队列。通过在邻接矩阵中扫描行号是currentVert的行寻找需要的边。只要下面任意一个条件为真,这条边就不能放入队列中:
1.源点和终点相同;
2. 终点在树中;
3. 源点和终点之间没有边(邻接矩阵中对应的值等于无穷大)。
如果没有一个条件为真,调用putInPQ()方法把这条边放入队列中。实际上并不一定会将这条边放入队列中,还得进行判断。步骤3中,将最小权值的边从优先级队列中删除。把这条边和该边的重点加入树,并显示源点和终点。
最后,所有顶点的isInTree变量被重置,即从树中删除。在该程序这样做,是因为根据这些数据只能创建一棵树。然后在完成一项工作后,最好把数据恢复到原始的形态。
接下来探讨下最短路径问题:
最短路径问题
在带权图中最常遇到的问题就是寻找两点间的最短路径问题,这个问题的解决方法可应用于现实生活中的很多地方。但是它比前面遇到的问题更加复杂一些。为了解决最短路径问题而提出的方法叫做Djikstra算法。这个算法的实现基于图的邻接矩阵表示法,它不仅能够找到任意两点间的最短路径,还可以找到某个指定点到其他所有顶点的最短路径。
为了实现这个算法,首先得建一个辅助类DistPar类,这个类中封装了到初始顶点的距离以及父顶点的信息。//DistPar类记录了当前顶点到起始顶点点的距离和当前顶点的父顶点另外还得有个数组,这是最短路径算法中的一个关键数据结构,它保持了从源点到其他顶点(终点)的最短路径。在算法的执行过程中这个距离是变化的,知道最后,它存储了从源点开始的真正最短距离。这个数组定义为WeightedGraph的一个私有成员变量:
class DistPar {
public int distance; //distance from start to this vertex
public int parentVert; //current parent of this vertex
public DistPar(int pv, int d) {
distance = d;
parentVert = pv;
}
}
private DistPar[] sPath; //存储最短路径数据,存储的是上面的DistPar对象
private int startToCurrent; //到当前顶点的距离
另外需要在构造函数中将其初始化:sPath =new DistPar[MAX_VERTS];
下面详细分析最短路径算法中涉及的几个方法,这都是WeightedGraph类中的方法,在这里我抽出来分析的,最后会附上完整的WeightedGraph类代码
/************************** 最短路径问题 ****************************/
/**
* path()方法执行真正的最短路径算法。
*/
public void path() { //寻找所有最短路径
/*
* 源点总在vertexArray[]数组下标为0的位置,path()方法的第一个任务就是把这个顶点放入树中。
* 算法执行过程中,将会把其他顶点也逐一放入树中。把顶点放入树中的操作是设置一下标志位即可。
* 并把nTree变量增1,这个变量记录了树中有多少个顶点。
*/
int startTree = 0; //从vertex 0开始
vertexArray[startTree].isInTree = true;
nTree = 1;
/*
* path()方法把邻接矩阵的对应行表达的距离复制到sPath[]中,实际总是先从第0行复制
* 为了简单,假定源点的下标总为0。最开始,所有sPath[]数组中的父节点字段为A,即源点。
*/
for(int i = 0; i < nVerts; i++) {
int tempDist = adjMat[startTree][i];
//sPath中保存的都是到初始顶点的距离,所以父顶点默认都是初始顶点,后面程序中会将其修改
sPath[i] = new DistPar(startTree, tempDist);
}
/*
* 现在进入主循环,等到所有的顶点都放入树中,这个循环就结束,这个循环有三个基本动作:
* 1. 选择sPath[]数组中的最小距离
* 2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert
* 3. 根据currentVert的变化,更新所有的sPath[]数组内容
*/
while(nTree < nVerts) {
//1. 选择sPath[]数组中的最小距离
int indexMin = getMin(); //获得sPath中的最小路径值索引
int minDist = sPath[indexMin].distance; //获得最小路径
if(minDist == INFINITY) {
System.out.println("There are unreachable vertices");
break;
}
//2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert
else { //reset currentVert
currentVert = indexMin;
startToCurrent = sPath[indexMin].distance;
}
vertexArray[currentVert].isInTree = true;
nTree++;
//3. 根据currentVert的变化,更新所有的sPath[]数组内容
adjust_sPath();
}
displayPaths();
nTree = 0;
for(int i = 0; i < nVerts; i++) {
vertexArray[i].isInTree = false;
}
}
//获取sPath中最小路径的索引
private int getMin() {
int minDist = INFINITY;
int indexMin = 0;
for(int i = 0; i < nVerts; i++) {
if(!vertexArray[i].isInTree && sPath[i].distance < minDist) {
minDist = sPath[i].distance;
indexMin = i;
}
}
return indexMin;
}
/*调整sPath中存储的对象的值,即顶点到初始顶点的距离,和顶点的父顶点
* 这是Dijkstra算法的核心
*/
private void adjust_sPath() {
int column = 1;
while(column < nVerts) {
if(vertexArray[column].isInTree) {
column++;
continue;
}
int currentToFringe = adjMat[currentVert][column]; //获得当前顶点到其他顶点的距离,其他顶点不满足isInTree
int startToFringe = startToCurrent + currentToFringe; //计算其他顶点到初始顶点的距离=当前顶点到初始顶点距离+当前顶点到其他顶点的距离
int sPathDist = sPath[column].distance; //获得column处顶点到起始顶点的距离,如果不与初始顶点相邻,默认值都是无穷大
if(startToFringe < sPathDist) {
sPath[column].parentVert = currentVert; //修改其父顶点
sPath[column].distance = startToFringe; //以及到初始顶点的距离
}
column++;
}
}
//显示路径
private void displayPaths() {
for(int i = 0; i < nVerts; i++) {
System.out.print(vertexArray[i].label + "=");
if(sPath[i].distance == INFINITY)
System.out.print("infinity");
else
System.out.print(sPath[i].distance);
char parent = vertexArray[sPath[i].parentVert].label;
System.out.print("(" + parent + ") ");
}
System.out.println("");
}
由于图的表示法有两种,邻接矩阵和邻接表。是的图的算法效率问题变得相对复杂。如果使用邻接矩阵,前面讨论的算法大多需要O(V2)的时间级,V表示顶点数量。因为这些算法几乎都检查了一遍所有的顶点,具体方法是在邻接矩阵中扫描每一行,一次查看每一条边。换句话说,邻接矩阵的V2个单元都被扫描过。
对于大规模的矩阵,O(V2)的时间基本是非常好的性能。如果图是密集的,那就没什么提高性能的余地了(密集意味着图有很多边,而它的邻接矩阵的许多或大部分单元被占)。然而,许多图是稀疏的,其实并没有一个确定数量的定义说明多少条边的图才是密集的或稀疏的,但如果在一个大图中每个顶点只有很少几条边相连,那么这个图通常被认为是稀疏的。
在稀疏图中,使用邻接表的表示方法代替邻接矩阵,可以改善运行时间,因为不必浪费时间来检索邻接矩阵中没有边的单元。对于无权图,邻接表的深度优先搜索需要O(V+E)的时间级,V是顶点数量,E是边数。对于带权图,最小生成树算法和最短路径算法都需要O(E+V)logV)的时间级,在大型的稀疏图中,与邻接矩阵方法的时间级O(V2)相比,这样的时间级可使性能大幅提升,但是算法会复杂一些。完整代码
下面附上有权图的完整代码和测试代码
package graph;下面是测试用例:
/**
* @desciption 带权图的完整代码
* @author eson_15
*/
//边界路径类
class Edge {
public int srcVert; //index of vertex starting edge
public int destVert; //index of vertex ending edge
public int distance; //distance from src to dest
public Edge(int sv, int dv, int d) {
srcVert = sv;
destVert = dv;
distance = d;
}
}
//优先队列
class PriorityQ {
private final int SIZE = 20;
private Edge[] queArray; //存储边界的数组
private int size;
public PriorityQ() {
queArray = new Edge[SIZE];
size = 0;
}
public void insert(Edge item) { //有序的插入边界
int j;
for(j = 0; j < size; j++) { //找到插入的位置,从0到size-1,逐渐减小
if(item.distance >= queArray[j].distance)
break;
}
//比item.distance小的往后挪一位,给腾出个空间
for(int k = size-1; k >= j; k--) {
queArray[k+1] = queArray[k];
}
queArray[j] = item; //插入item
size++;
}
public Edge removeMin() { //删除最小的边界并返回
return queArray[--size];
}
public void removeN(int n) { //删除n位置的边界
for(int j = n; j < size-1; j++) {
queArray[j] = queArray[j+1];
}
size--;
}
public Edge peekMin() { //返回最小边界,不删除
return queArray[size-1];
}
public Edge peekN(int n) { //返回n位置的边界
return queArray[n];
}
public int size() {
return size;
}
public boolean isEmpty() {
return (size == 0);
}
public int find(int findDex) { //寻找特定disVert的边界索引
for(int j = 0; j < size; j++) {
if(queArray[j].destVert == findDex)
return j;
}
return -1;
}
}
//DistPar类记录了当前顶点到起始顶点点的距离和当前顶点的父顶点
class DistPar {
public int distance; //distance from start to this vertex
public int parentVert; //current parent of this vertex
public DistPar(int pv, int d) {
distance = d;
parentVert = pv;
}
}
//带权图类
public class WeightedGraph {
private final int MAX_VERTS = 20; //最大顶点数
private final int INFINITY = 100000; //最远距离...表示无法达到
private Vertex[] vertexArray; //存储顶点的数组
private int adjMat[][]; //存储顶点之间的边界
private int nVerts; //顶点数量
private int currentVert; //当前顶点索引
private PriorityQ thePQ; //
private int nTree; //最小生成树中的顶点数量
private DistPar[] sPath; //存储最短路径数据
private int startToCurrent; //到当前顶点的距离
public WeightedGraph() {
vertexArray = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][MAX_VERTS];
for(int i = 0; i < MAX_VERTS; i++) {
for(int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = INFINITY; //初始化所有边界无穷远
}
}
thePQ = new PriorityQ();
sPath = new DistPar[MAX_VERTS];
}
public void addVertex(char lab) { //添加顶点
vertexArray[nVerts++] = new Vertex(lab);
}
public void addEdge(int start, int end, int weight) {//添加带权边界
adjMat[start][end] = weight;
adjMat[end][start] = weight; //最优路径的时候不需要这句
}
public void displayVertex(int v) {
System.out.print(vertexArray[v].label);
}
/************************ 带权图的最小生成树 ***************************/
public void MinSpanningTree() {
currentVert = 0; //从0开始
while(nTree < nVerts-1) { //当不是所有节点都在最小生成树中时
vertexArray[currentVert].isInTree = true; //将当前顶点加到树中
nTree++;
//往PQ中插入与当前顶点相邻的一些边界
for(int i = 0; i < nVerts; i++) {
if(i == currentVert) //如果是本顶点,跳出
continue;
if(vertexArray[i].isInTree) //如果顶点i已经在树中,跳出
continue;
int distance = adjMat[currentVert][i]; //计算当前顶点到i顶点的距离
if(distance == INFINITY)
continue; //如果当前顶点与i顶点无穷远,跳出
putInPQ(i, distance); //将i节点加入PQ中
}
if(thePQ.size() == 0) { //如果PQ为空,表示图不连接
System.out.println("Graph not connected!");
return;
}
Edge theEdge = thePQ.removeMin();
int sourceVert = theEdge.srcVert;
currentVert = theEdge.destVert;
System.out.print(vertexArray[sourceVert].label);
System.out.print(vertexArray[currentVert].label);
System.out.print(" ");
}
}
private void putInPQ(int newVert, int newDist) {
int queueIndex = thePQ.find(newVert);//判断PQ中是否已经有到相同目的顶点的边界
if(queueIndex != -1) { //如果有则与当前顶点到目的顶点的距离作比较,保留短的那个
Edge tempEdge = thePQ.peekN(queueIndex);//get edge
int oldDist = tempEdge.distance;
if(oldDist > newDist) { //如果新的边界更短
thePQ.removeN(queueIndex); //删除旧边界
Edge theEdge = new Edge(currentVert, newVert, newDist);
thePQ.insert(theEdge);
}
}
else { //如果PQ中没有到相同目的顶点的边界
Edge theEdge = new Edge(currentVert, newVert, newDist);
thePQ.insert(theEdge);//直接添加到PQ
}
}
/************************** 最短路径问题 ****************************/
/**
* path()方法执行真正的最短路径算法。
*/
public void path() { //寻找所有最短路径
/*
* 源点总在vertexArray[]数组下标为0的位置,path()方法的第一个任务就是把这个顶点放入树中。
* 算法执行过程中,将会把其他顶点也逐一放入树中。把顶点放入树中的操作是设置一下标志位即可。
* 并把nTree变量增1,这个变量记录了树中有多少个顶点。
*/
int startTree = 0; //从vertex 0开始
vertexArray[startTree].isInTree = true;
nTree = 1;
/*
* path()方法把邻接矩阵的对应行表达的距离复制到sPath[]中,实际总是先从第0行复制
* 为了简单,假定源点的下标总为0。最开始,所有sPath[]数组中的父节点字段为A,即源点。
*/
for(int i = 0; i < nVerts; i++) {
int tempDist = adjMat[startTree][i];
//sPath中保存的都是到初始顶点的距离,所以父顶点默认都是初始顶点,后面程序中会将其修改
sPath[i] = new DistPar(startTree, tempDist);
}
/*
* 现在进入主循环,等到所有的顶点都放入树中,这个循环就结束,这个循环有三个基本动作:
* 1. 选择sPath[]数组中的最小距离
* 2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert
* 3. 根据currentVert的变化,更新所有的sPath[]数组内容
*/
while(nTree < nVerts) {
//1. 选择sPath[]数组中的最小距离
int indexMin = getMin(); //获得sPath中的最小路径值索引
int minDist = sPath[indexMin].distance; //获得最小路径
if(minDist == INFINITY) {
System.out.println("There are unreachable vertices");
break;
}
//2. 把对应的顶点(这个最小距离所在列的题头)放入树中,这个顶点变成“当前顶点”currentVert
else { //reset currentVert
currentVert = indexMin;
startToCurrent = sPath[indexMin].distance;
}
vertexArray[currentVert].isInTree = true;
nTree++;
//3. 根据currentVert的变化,更新所有的sPath[]数组内容
adjust_sPath();
}
displayPaths();
nTree = 0;
for(int i = 0; i < nVerts; i++) {
vertexArray[i].isInTree = false;
}
}
//获取sPath中最小路径的索引
private int getMin() {
int minDist = INFINITY;
int indexMin = 0;
for(int i = 0; i < nVerts; i++) {
if(!vertexArray[i].isInTree && sPath[i].distance < minDist) {
minDist = sPath[i].distance;
indexMin = i;
}
}
return indexMin;
}
/*调整sPath中存储的对象的值,即顶点到初始顶点的距离,和顶点的父顶点
* 这是Dijkstra算法的核心
*/
private void adjust_sPath() {
int column = 1;
while(column < nVerts) {
if(vertexArray[column].isInTree) {
column++;
continue;
}
int currentToFringe = adjMat[currentVert][column]; //获得当前顶点到其他顶点的距离,其他顶点不满足isInTree
int startToFringe = startToCurrent + currentToFringe; //计算其他顶点到初始顶点的距离=当前顶点到初始顶点距离+当前顶点到其他顶点的距离
int sPathDist = sPath[column].distance; //获得column处顶点到起始顶点的距离,如果不与初始顶点相邻,默认值都是无穷大
if(startToFringe < sPathDist) {
sPath[column].parentVert = currentVert; //修改其父顶点
sPath[column].distance = startToFringe; //以及到初始顶点的距离
}
column++;
}
}
//显示路径
private void displayPaths() {
for(int i = 0; i < nVerts; i++) {
System.out.print(vertexArray[i].label + "=");
if(sPath[i].distance == INFINITY)
System.out.print("infinity");
else
System.out.print(sPath[i].distance);
char parent = vertexArray[sPath[i].parentVert].label;
System.out.print("(" + parent + ") ");
}
System.out.println("");
}
}
package test;
import graph.WeightedGraph;
public class Test {
public static void main(String[] args){
WeightedGraph arr = new WeightedGraph();
arr.addVertex('A');
arr.addVertex('B');
arr.addVertex('C');
arr.addVertex('D');
arr.addVertex('E');
arr.addEdge(0, 1, 50); //AB 50
arr.addEdge(0, 3, 80); //AD 80
arr.addEdge(1, 2, 60); //BC 60
arr.addEdge(1, 3, 90); //BD 90
arr.addEdge(2, 4, 40); //CE 40
arr.addEdge(3, 2, 20); //DC 20
arr.addEdge(3, 4, 70); //DE 70
arr.addEdge(4, 1, 50); //EB 50
arr.MinSpanningTree(); //最小生成树
System.out.println(" ");
arr.path(); //最短路径
}
}
输出结果为:
AB BE EC CD
A=infinity(A) B=50(A) C=100(D) D=80(A) E=100(B)
带权图就探讨到这吧,如果有问题请留言指正~
_____________________________________________________________________________________________________________________________________________________
-----乐于分享,共同进步!