Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法

在讲Bellman-Ford之前先介绍另一种存储图的方法:邻接表。

邻接表

先上数据,以下是一个包含4个顶点,5条边的图。n = 4(顶点编号为1~4), m = 5,接下来每一行3个数xyz,表示顶点x到顶点y的边的权值为z。现在用邻接表来存储一个读出这个图:


4 5
1 4 9
2 4 6
1 2 5
4 3 8
1 3 7

  • 创建三个数组,u,v,w用来记录每条边的信息,即u[i],v[i],w[i]表示第i条边是从第u[i]号顶点到v[i]号顶点(u[i]->v[i]),且权值为w[i]。

  • 再创建两耳数组first和next,first数组的1n号分别是用来存储1n号顶点的第一条边的编号,初始的时候因为没有边加入所以都是-1,即first[u[i]]保存顶点u[i]的第一条边的编号,next[i]存储编号为i的边的“下一条边”的编号。

  • 将每条边读入上面五个数组,uvw直接赋值即可,核心为first以及next的插入,插入算法为:


    next[i] = first[u[i]]
    first[u[i]] = i

  • 插入的步骤如下图所示:

Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第1张图片
Paste_Image.png
  • 创建完成之后的读取就比较容易了,因为first[u[i]]保存顶点u[i]的第一条边的编号,next[i]存储编号为i的边的“下一条边”的编号。我们只需要从first[u[i]]开始遍历就可以了。
Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第2张图片
Paste_Image.png


for i in 0...4 {
var k = first[i]
while k != -1 {
print("(u[k]) (v[k]) (w[k])")
k = next[k]
}
}

  • 完整的程序:


//4个顶点,5条边,如下【1,4,9】表示顶点1到顶点4,权重为9的一条边
let map = [[1,4,9],
[2,4,6],
[1,2,5],
[4,3,8],
[1,3,7]]
//u,v,w数组的大小要比图的边的数目大1
var u:[Int] = Array.init(repeatElement(0, count: map.count + 1))
var v:[Int] = Array.init(repeatElement(0, count: map.count + 1))
var w:[Int] = Array.init(repeatElement(0, count: map.count + 1))
//first和next数组的大小要比图的顶点的数目大1
var first:[Int] = Array.init(repeatElement(-1, count: 5))
var next:[Int] = Array.init(repeatElement(-1, count: 5))

//创建邻接表
func creatMap(map:[Array]) {

for i in map.indices {
    
    u[i] = map[i][0]
    v[i] = map[i][1]
    w[i] = map[i][2]
    
    next[i] = first[u[i]]
    first[u[i]] = i
}

}

//遍历邻接表

func listMap() {

for i in 0...4 {
    var k = first[i]
    while k != -1 {
       print("\(u[k]) \(v[k]) \(w[k])")
        k = next[k]
    }
}

}

creatMap(map: map)
listMap()
/*
1 3 7
1 2 5
1 4 9
2 4 6
4 3 8
*/

Bellman-Ford算法

Bellman-Ford算法可以解决带有负权边(边的权值为负数)的图。算法非常简单:


for _ in 1..<(n-1) {
for i in 1...m {
if dist[v[i]] > (dist[u[i]] + w[i]) {
dist[v[i]] = (dist[u[i]] + w[i])
}
}

  • 看过我上一篇Swift最短路径之Dijkstra(单源最短路)算法文章的应该知道


if dist[v[i]] > (dist[u[i]] + w[i]) {
dist[v[i]] = (dist[u[i]] + w[i])
}

这两句代码的意思其实松弛源点到v[i]这条边。

  • 接下来就是对每一条边就是松弛一遍。松弛一遍,


    for i in 1...m {
    if dist[v[i]] > (dist[u[i]] + w[i]) {
    dist[v[i]] = (dist[u[i]] + w[i])
    }

  • 我们来看一下具体的过程。
    首先给出如下所示的一个图,右边是按顺序给出的边

Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第3张图片
Paste_Image.png

接下来用一个dist数组来存储1号顶点到所有顶点的距离,然后开始进行4轮对所有的边进行松弛

Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第4张图片
Paste_Image.png

这里有个问题,需要进行多少轮松弛呢,答案是最多n-1轮,n是顶点的个数,注意是最多!!如上图所示,进行第4轮松弛后,结果并未更改。因此,我们可以在这个地方进行优化。添加一个变量来标记在本轮松弛中是否发生了变化,如果没有发生变化,则可以提前跳出循环。

  • 完整代码如下


let max:Int = 10000
//5个顶点,5条边,顶点从1开始算,边也从1开始算
let n = 5, m = 5
let map = [[0,0,0],
[2,3,2],
[1,2,-3],
[1,5,5],
[4,5,2],
[3,4,3]]
//u,v,w数组的大小要比图的边的数目大1
var u:[Int] = Array.init(repeatElement(0, count: 6))
var v:[Int] = Array.init(repeatElement(0, count: 6))
var w:[Int] = Array.init(repeatElement(0, count: 6))

func bellmanFord(map:[Array]) {

for i in 1...5 {
    
    u[i] = map[i][0]
    v[i] = map[i][1]
    w[i] = map[i][2]
}

var dist:[Int] = Array.init(repeatElement(max, count: 6))
dist[1] = 0

var check:Int //检测dist是否有更新

for _ in 1..<(n-1) {
    check = 0
    for i in 1...m {
        if dist[v[i]] > (dist[u[i]] + w[i]) {
            dist[v[i]] = (dist[u[i]] + w[i])
            
            check = 1
        }
    }
    
    if check == 0 { //如果数组dist没有更新,提前退出循环结束算法
        break
    }
}

print(dist)

}
bellmanFord(map: map) //0, -3, -1, 2, 4

Bellman-Ford的队列优化算法

在上面介绍的Bellman-Ford算法中,在每实施一次松弛操作之后,就会有一些顶点已经求得其最短路径,此后,这些顶点的最短路径的值就会一直保持不变,不再受到后续松弛操作的影响,但是每次还要判断是否需要松弛,这里浪费了时间。因此,我们可以考虑每次仅针对最短路径值发生了变化的顶点的所有出边执行松弛操作。这就是Bellman-Ford的队列优化算法。
那么,如何知道当前哪些点的最短路程发生了变化呢?
用一个队列来维护这些点,每次选取队首顶点u,对顶点u的所有出边进行松弛操作。假如有一条u->v的边,如果松弛成功(dist[v] > dist[v] + e[u][v]),则将顶点v放入队尾,需要注意的是,同一个顶点不能同时在队列中出现多次,但是允许一个顶点在出队后再入队。在将顶点u的所有的出边松弛完毕后,就将顶点u出队。接下来不断从队列中取出新的队首顶点进行如上操作,直至队列为空。这就是Bellman-Ford的队列优化算法。下面举一个例子。

  • 新建一个图

    //5个顶点,7条边
    [1,2,2],
    [1,5,10],
    [2,3,3],
    [2,5,7],
    [3,4,4],
    [4,5,5],
    [5,3,6]]
  • 初始化dist数组和队列。
Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第5张图片
Paste_Image.png
  • 选取队首顶点1,对顶点1所有的出边就行松弛。顶点2,和5松弛成功,分别将2和5入队。
Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第6张图片
Paste_Image.png
  • 顶点1出队,然后选取队首顶点2进行如上处理。注意2->5这条边松弛成功,但因为5号顶点已经在队列中,所以不进行入队操作。
Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第7张图片
Paste_Image.png
  • 在对2号顶点处理完毕后,将2号顶点出队,再选取队首5号顶点进行以上处理,知道队列为空,具体步骤就省略了,最后处理的结果如下:
Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法_第8张图片
Paste_Image.png

完整代码如下:


let max:Int = 10000
//5个顶点,7条边,顶点从1开始算,边也从1开始算
let map = [[0,0,0],
[1,2,2],
[1,5,10],
[2,3,3],
[2,5,7],
[3,4,4],
[4,5,5],
[5,3,6]]
//u,v,w数组的大小要比图的边的数目大1
var u:[Int] = Array.init(repeatElement(0, count: 8))
var v:[Int] = Array.init(repeatElement(0, count: 8))
var w:[Int] = Array.init(repeatElement(0, count: 8))
//first和next数组的大小要比图的顶点的数目大1
var first:[Int] = Array.init(repeatElement(-1, count: 9))
var next:[Int] = Array.init(repeatElement(-1, count: 9))

//创建邻接表
func creatMap(map:[Array]) {

for i in 1...7 {
    
    u[i] = map[i][0]
    v[i] = map[i][1]
    w[i] = map[i][2]
    
    next[i] = first[u[i]]
    first[u[i]] = i
}

}

func bellman_ford(map:[Array]) {

creatMap(map: map)

var dist:[Int] = Array.init(repeatElement(max, count: 6))
dist[1] = 0

var book:[Int] = Array.init(repeatElement(0, count: 8))
book[1] = 1

var queue:[Int] = Array.init(repeatElement(0, count: 100))
var head = 1,tail = 1

queue[tail] = 1
tail+=1

var k = 0
while head < tail {
    k = first[queue[head]]
    while k != -1 {
        if dist[v[k]] > (dist[u[k]] + w[k]) {
            dist[v[k]] = dist[u[k]] + w[k]
            
                if book[v[k]] == 0 {
                queue[tail] = v[k]
                    tail+=1
            }
        }
        k = next[k]
    }
    book[queue[head]] = 0
    head+=1
}

print(dist)

}

bellman_ford(map: map) //0, 2, 5, 9, 9

你可能感兴趣的:(Swift最短路径之Bellman-Ford和Bellman-Ford的队列优化算法)