看了一天的带权图,终于是理解了。不过还只是理解而已。
因为本人一直追随着倪升武的博客,但是可能由于本人算法基础较差,因此在“带权图”的算法的理解上,走了好多弯路(再次证明本人的无能啊啊啊,哈哈)。
带权图
在图定义的基础上,然后将每个边加上一定的权重,这就构成了带权图。其实在带权图这部分,重要的是其2个重要的问题:
- 最小生成树问题
- 最短路径问题
下面就这2个问题分别解释一下
最小生成树问题
注意:下面我们讨论的图都是连通图。
定义:在图中我们也有最小生成树这个概念。而在带权图中的最小生成树,指的是这棵树不仅要满足该图对应的非带权图的最小生成树的条件,还要求边的权值和最小。
算法的过程:
首先我们要假定2个容器,一个树容器,一个优先级队列的容器。我们的任务就是从放有边的队列容器中取出边放到树容器中。当树容器的顶点数目等于图的顶点的数目时,树容器内部放置的就是我们需要的最小生成树的边。而放置的过程就是我们产生最小生成树的过程。
- 1.选定一个顶点为源,然后将该源相关的所有Edge(成员有src:起始顶点,des:末尾顶点,distance:边的权值)添加进优先级队列容器里。当然,添加时,我们会控制每个Edge在队列中的顺序,使其按照权值来进行有序排列
- 2.从队列容器中找到权值最小的Edge,然后将其取出并放入树容器中。并且将我们所取的这个Edge对象的des顶点设置为当前节点。
- 3.因为第二步中当前顶点改变了,因此我们再重复第1步和第2步的操作,直至树容器内部Edge的数目=图的顶点数目-1。
以上就是这个算法的基本过程。下面是取自倪升武的博客的代码。我会在其基础上再详细的添加一些注释帮助大家理解。
//边界路径类,主要记录了边的始末顶点,以及边的权值
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;
}
//从这个方法我们可以看出,queArray这个优先级队列是按照权值从大到小的顺序排列的。队列的最后是权值最小的Edge。
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() { //删除最小的边界并返回
//这里的‘删除’,是将标志队列数目的size减1,这样最后一个元素的索引值就不在我们的使用范围之内了。也就达到了删除的效果。下面这行注释的代码是我自己添加的,可以有助于大家的理解。
//queArray[size-1] = 0;
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(" ");
}
}
//这个方法是将一个Edge放入优先级队列,保证队列中每个Edge的des顶点是不同的。
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
}
}
}
简要说一下上面的工作流程:就是选定一个源,根据优先级队列有序排列这个特点,选取队列末尾这个权值最小的Edge加入树容器,再将该Edge的des顶点选定为新的源,重复上述的过程,直至得到最小生成树。
最短路径问题
所谓的最短路径,即2点之间的路径要求权值最小。
实现最短路径的算法有很多,比如Dijkstra算法,(迪杰斯特拉),Floyd(弗洛伊德)算法等。这里我们仅介绍Dijkstra算法。
建议往下阅读之前,首先阅读绿岩的这篇博客,尤其是里面的框图,我认为值得一读。
下面是倪升武的代码,如果不理解Dijkstra算法过程,下面的代码我估计你是不容易读明白的(大神请无视)。
另外,为了分析方便,下面的代码有些量未完全引入,完整代码请移步倪升武的博客
/************************** 最短路径问题 ****************************/
/**
* 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("");
}