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

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

邻接表

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

  • 创建三个数组,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的插入,插入算法为:
    <pre>
    next[i] = first[u[i]]
    first[u[i]] = i
    </pre>

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

Paste_Image.png
  • 创建完成之后的读取就比较容易了,因为first[u[i]]保存顶点u[i]的第一条边的编号,next[i]存储编号为i的边的“下一条边”的编号。我们只需要从first[u[i]]开始遍历就可以了。
Paste_Image.png

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

  • 完整的程序:

<pre>
//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<Int>]) {

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
*/
</pre>

Bellman-Ford算法

Bellman-Ford算法可以解决带有负权边(边的权值为负数)的图。算法非常简单:
<pre>
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])
}
}
</pre>

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

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

  • 接下来就是对每一条边就是松弛一遍。松弛一遍,
    <pre>
    for i in 1...m {
    if dist[v[i]] > (dist[u[i]] + w[i]) {
    dist[v[i]] = (dist[u[i]] + w[i])
    }
    </pre>

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

Paste_Image.png

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

Paste_Image.png

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

  • 完整代码如下

<pre>
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<Int>]) {

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
</pre>

Bellman-Ford的队列优化算法

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

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

完整代码如下:

<pre>
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<Int>]) {

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<Int>]) {

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
</pre>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容