《算法导论》-- 单源最短路径+PHP实现

1.几个定义

1.1 最短路径

定义从节点u到节点v的最短路径权重δ(u, v)如下:

image.png

从节点u到节点v的最短路径则定义为任何一条权重为w(p) = δ(u, v)的从u到v的路径p。

1.2 负权重的边

某些单源最短路径问题可能包括权重为负值的边。但如果图G(V,E)不包含从源结点s可以到达的权重为负值的环路,则对于所有的结点v∈V,最短路径权重δ(u, v)都有精确定义,即使其取值为负数,如果G包含从s可到达的权重为负值的环路,则最短路径权重无定义。从s到该环路的任一结点的路径都不可能是最短路径,因为我们只要不沿着任何“最短”路径再遍历一次权重为负值的环路,则总是可以找到一条权重更小的路径。

1.3 环路

一条最短路径不能包含权重为负值的环路,也不能包含权重为正值的环路,因为只要将环路从路径上删除就可以得到一条源结点和终结点与原来路径相同的一条权重更小的路径。

1.4 松弛操作

对于每个结点v来说,我们维持一个属性v.d,用来记录源结点s到节点v的最短路径权重的上界。我们称v.d为s到v的最短路径估计。我们使用下面运行时间为Θ(V)的算法来对最短路径估计和前驱结点进行初始化:

INIT(G, s)
for each vertex v ∈ G.V
    v.d = ∞
    v.π = NIL
s.d = 0

对一条边(u, v)的松弛过程为:首先测试一下是否可以对从s到v的最短路径进行改善。测试的方法是:将当下s到u的最短路径距离加上u到v之间的边权重,并与当前的s到v的最短路径估计进行比较,如果前者更小,则对v.d和v.π进行更新。

RELEX(u, v,  w)
if v.d > u.d + w(u, v)
    v.d = u.d + w(u, v)
    v.π = u

2. Bellman-Ford算法(贝尔曼-福特算法)

Bellman-ford算法解决的是一般情况下的单源最短路径问题,在这里,边的权重可以为负值,但不能包含负环路,如果存在负环路则返回false。效率较低,为O(VE)。

注:为什么要进行V-1次循环对边松弛,这是因为V个顶点的话,最长路径可能是包含V-1条边(非环路),这意味在这种情况下,最多要进行V-1次松弛。

2.1 伪代码实现
BELLMAN-FORD(G, w ,s)
INIT(G, s)
for i = 1 to |G.V| - 1
    for each edge(u, v) ∈ G.E
        RELAX(u, v, w)
for each edge(u, v) ∈ G.E
    if v.d > u.d + w(u, v)
          return FALSE
return TRUE
2.2 伪代码执行过程

书中给出的流程是这样的:

image.png

这让我非常困惑,困扰了我很久,经过查证,看看这个神奇的网站visualgo, 代码真正的执行流程。
image.png

其实书中给出的不是伪代码的执行流程,这应该是用队列优化后的执行流程,即类似广度优先地对边进行松弛。网上很多人贴上伪代码,就画个优化后的执行流程图,其实是对不上的,一点都不严谨。

2.3 PHP 实现(队列优化版本——SPFA算法)

原版本的算法效率比较低,就不写了,写一个队列优化版本的,被称为SPFA算法。
首先随手画了个有向图,渣渣水平将就,怕画简单了又说明不了问题,索性画一个复杂一点的:


image.png

接下来是写程序:

<?php
class Node {
    public $val; //值
    public $d; //距离
    public $p; //父节点
    public $linkedNodes = [];//连接的顶点
    public function __construct($val)
    {
        $this->val = $val;
    }
}


/**
* SPFA
**/
class Bellman
{
    /**
     * 生成图
     * @param array $vertex
     * @return array
     */
    public function buildGraph($vertex)
    {
        $graph = [];
        $nodes = array_keys($vertex);
        foreach ($nodes as $node) {
            $graph[$node] = new Node($node);
        }
        foreach ($vertex as $key => $item) {
            foreach ($item as $value) {
                if (isset($graph[$value])) {
                    $graph[$key]->linkedNodes[] = $graph[$value];
                }
            }
        }
        return $graph;
    }

    /**
     * 初始化操作
     * @param array $graphNodes
     * @param Node $s
     */
    public function init(&$graphNodes, &$s)
    {
        foreach ($graphNodes as $graphNode) {
            $graphNode->d = 9999999;
        }
        $s->d = 0;
    }

    /**
     * 松弛操作
     * @param Node $u
     * @param Node $v
     * @param Array $w
     * @return bool
     */
    public function relax(&$u, &$v, $w)
    {
        $d = $u->d + $w[$u->val . '_' . $v->val];
        if($v->d > $d) {
            $v->d = $d;
            $v->p = $u;
            return true;
        }
        return false;
    }

    /**
     * Bellman-Ford 算法 [队列优化后的,非原始算法]
     * 问题1:没考虑到负环路
     * @param $graphNodes
     * @param $s
     */
    public function bellManFord(&$graphNodes, $s, $w)
    {
        $num = count($graphNodes) - 1;//最大松弛次数
        $this->init($graphNodes, $s);
        $queue = [];
        $relaxNum = []; //边的松弛次数
        array_push($queue, $s);
        while (!empty($queue)) {
            $node = array_shift($queue);
            foreach ($node->linkedNodes as $linkedNode) {
                if($this->relax($node, $linkedNode, $w)) {
                    $edge = $node->val . '_' . $linkedNode->val;
                    $relaxNum[$edge] = isset($relaxNum[$edge]) ? $relaxNum[$edge] + 1 : 1; //松弛次数+1
                    if($relaxNum[$edge] > $num) {
                        throw new Exception('存在负环!');
                    }
                    if(!in_array($linkedNode, $queue)) {
                        array_push($queue, $linkedNode);
                    }
                }
            }
        }
    }
}


//顶点
$vertex = [
    'a' => ['b', 'd'],
    'b' => ['c'],
    'd' => ['e'],
    'c' => ['f', 'b', 'e'],
    'e' => [],
    'f' => ['c']
];
//边的权重
$w = [
    'a_b' => 2,
    'a_d' => 1,
    'b_c' => 1,
    'd_e' => 3,
    'c_b' => -1,
    'c_f' => 1,
    'c_e' => 2,
    'f_c' => -3
];

$g = new Bellman();
$graphNodes = $g->buildGraph($vertex);
$g->bellManFord($graphNodes, $graphNodes['a'], $w);

好,运行一下代码,嚓!!!


image.png

MMP,我画图画的那么辛苦,居然画了个有负环的,还傻乎乎的画了路径,将错就错也懒的改了。
负环在这里:


image.png
改下数据,把 'f_c' => -3改成-1,运行一下:
image.png

3. Dijkstra算法(迪杰斯特拉算法)

Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有的边的权重都为非负值。

3.1 伪代码实现
DIJKSTARA(G, w, s)
INIT(G, s)
S = ∅
Q =G.V
while Q ≠ ∅
    u = EXTRACT-MIN(Q) //最小优先队列Q,排序关键字为顶点的d值
    S = S U {u}
    for each vertex v ∈ G.Adj[u]
        RELAX(u, v, w)
3.2 伪代码执行过程
image.png
3.3 PHP 实现

图:


image.png

a是起点,最短距离:


image.png
<?php
class Node {
    public $val; //值
    public $d; //距离
    public $p; //父节点
    public $linkedNodes = [];//连接的顶点
    public function __construct($val)
    {
        $this->val = $val;
    }
}


/**
 * Dijkstra
 **/
class DijkstraAlgorithm
{
    /**
     * 生成图
     * @param array $vertex
     * @return array
     */
    public function buildGraph($vertex)
    {
        $graph = [];
        $nodes = array_keys($vertex);
        foreach ($nodes as $node) {
            $graph[$node] = new Node($node);
        }
        foreach ($vertex as $key => $item) {
            foreach ($item as $value) {
                if (isset($graph[$value])) {
                    $graph[$key]->linkedNodes[] = $graph[$value];
                }
            }
        }
        return $graph;
    }

    /**
     * 初始化操作
     * @param array $graphNodes
     * @param Node $s
     */
    public function init(&$graphNodes, &$s)
    {
        foreach ($graphNodes as $graphNode) {
            $graphNode->d = 9999999;
        }
        $s->d = 0;
    }

    /**
     * 松弛操作
     * @param Node $u
     * @param Node $v
     * @param Array $w
     * @return bool
     */
    public function relax(&$u, &$v, $w)
    {
        $d = $u->d + $w[$u->val . '_' . $v->val];
        if($v->d > $d) {
            $v->d = $d;
            $v->p = $u;
            return true;
        }
        return false;
    }

    /**
     * 从队列取出最小距离的顶点
     * @param array $queue
     */
    public function extractMin(&$queue)
    {
        $result = [];
        foreach ($queue as $item) {
            $result[$item->d] = $item;
        }
        ksort($result);
        $queue = array_values($result);
        return array_shift($queue);
    }

    /**
     * Dijkstra 算法
     * 问题1:没考虑到负环路
     * @param $graphNodes
     * @param $s
     */
    public function dijkstra(&$graphNodes, $s, $w)
    {
        $this->init($graphNodes, $s);
        $gather = [];
        $queue = $graphNodes;
        while (!empty($queue)) {
            $node = $this->extractMin($queue);
            $gather[] = $node;
            foreach ($node->linkedNodes as $linkedNode) {
                if($this->relax($node, $linkedNode, $w)) {
                    if(!in_array($linkedNode, $queue)) {
                        array_push($queue, $linkedNode);
                    }
                }
            }
        }
    }
}


//顶点
$vertex = [
    'a' => ['b', 'c'],
    'b' => ['d'],
    'd' => ['e'],
    'c' => ['e'],
    'e' => [],
];
//边的权重
$w = [
    'a_b' => 1,
    'a_c' => 3,
    'b_d' => 2,
    'd_e' => 2,
    'c_e' => 4,
];

$g = new DijkstraAlgorithm();
$graphNodes = $g->buildGraph($vertex);
$g->dijkstra($graphNodes, $graphNodes['a'], $w);

执行结果:


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

推荐阅读更多精彩内容

  • 参见贪心算法——最短路径Dijkstra算法参见动态规划 目录 0.最短路径问题0.1 最短路径问题描述 0.1....
    王侦阅读 4,742评论 1 9
  • 1 概述 最短路径是图中的常见问题,最典型的应用是:当我们用百度地图或高德地图引导我们去某个地方时,它通常会给出一...
    CodingTech阅读 1,487评论 4 9
  • 所有结点对的最短路径问题 Floyd算法 前提条件: 可以有负权重边, 但是不能有负权重的环. 特点: 动态规划,...
    陈码工阅读 1,744评论 0 1
  • 目录 1.流网络及最大流问题1.1 流网络1.2 最大流问题1.3 最大流问题的三种解法对比 2.Ford-Ful...
    王侦阅读 4,558评论 0 3
  • 目录 1.基本图算法参见基本的图算法参见深度优先搜索和广度优先搜索专题 2.最小生成树——无向图参见最小生成树 3...
    王侦阅读 1,494评论 0 1