维特比算法

维特比(Viterbi)算法是一种动态规划算法,在处理隐马尔可夫(HMM)最优路径问题时经常被使用.

动态规划算法

动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法,通常情况下应用于最优化的问题,
这类问题一般有很多可行的解,每个解有一个值,而我们希望从中找到最优的答案.

在计算机科学领域,应用动态规划的思想解决的最基本的一个问题就是寻找有向无环图(DAG)当中两个点之间的最短路径
实际应用于地图导航、语音识别、分词、机器翻译等等.

问题

看下面一个简易版的地图


图1.png

假设从北京驱车前往广州需要跨越 a,b,c,d,e五省,每个省均有4个城市1,2,3,4
各城市间的距离与图上城市间距离一致,求北京前往广州的最短路径.

穷举法

首先想到的就是穷举法,非别计算出所有可能的路径距离,然后求最优解.对于这个问题,需要计算4*4*4*4*4=1024次.
在实际应用中,随着节点数的增加,计算的复杂度也相应的增加,图2为上例中所有可能出现的结果(为便于观察,微调了下位置)

图2.png

假设两地间隔N个省,每个省有M个城市,那么实际的计算复杂度为O(N^M)

维特比算法

维特比算法假设,从北京到上海有一条最短路径(图3中红色部分),这条路径经过e2,那么包含在这条路径中,从北京到e2的这条子路径,也一定是北京到e2间的最短路径.


图3

这个很好证明,如果有从北京到e2的更短的路径,那么原先的最短路径也就不是最短路径了.
同理,那么从北京到d2,c2,b2的路径也一定是最短子路径.

那么如果要求北京到e2的最短路径,那么只需要计算从北京到d层每个城市的最短路径,
然后再计算d层每个城市到e2的距离,即可得到北京到e2间的最短路线,对于最初的问题而言
先计算北京距离e层的各个点的最短距离,在计算e层各个点到上海的距离,即可得出结果.

分解算法

第一步,计算北京到a层所有城市的距离S(ai),这里计算了4次

第二步,计算北京到b层的距离,从北京出发可能经过a层的任意一个点,前往b层的任意点,
因而可能的路线有16种(4*4), 如图4


图4.png

从这16中结果中,即可得到从北京出发,分别前往b1,b2,b3,b4的最短路线.

第三步,需要计算北京到c层各个点的距离,如果采用穷举法,那么c层所有可能的路线64次(444).

但根据维特比算法的假设,从北京出发,经过b层某个点到达c层某个点的最短路径,一定包含从北京出发,到达b层这个点的最短路径.

比较绕,举个具体点的例子,假设从北京出发,经过a2,b2到达c2为北京到c2间的最短路径,那么北京经过a2到b2这条子路径,一定是北京到b2的最短路径.

如此,即可在第二步计算的16中结果中,只保留b层4个点的最短路径,如图4-2


图4-2.png

再分别计算这些点到c层各个点的路径(共16次 44),如此,原本c层需要再b层的计算的所有结果(16个)上再4,现在变为+16,总的计算次数变为了32,如图5

图5.png

第四步,重复第三步的方式,计算北京到d层各个点的距离,如图6


图6.png

在到达shenzhen之前,后续每一层都只需要再前一步计算的最优路线上(4个)进行4次(到1,2,3,4每个点)运算,如图7


图7.png

最终,即可得到唯一的一条路径,如图8

8.png

维特比算法将原本层与层之间的*法变为了+法,例子中的第一层北京只有1个点,到e点总的计算次数就变为了64次

1*4 + 4*4 + 4*4 + (4*4) + (4*4) + 1*4
bj到a a到b  b到c   c到d     d到e    e到shenzhen

一个层数为L,每层有N个点的篱笆图,运用维特比算法的复杂度即为O(LN^2),

而原本为

4   *   4   *   4   *  4  *  4   *   1
bj到a  bj到b    bj到c    bj到d  bj到e bj到shenzhen

代码验证

简化一下上面的问题,剔除e层,保留a,b,c,d层与起始点(beijing,shenzhen),然后绘制在坐标系上的,
坐标随便写的,能表示出层与层之间即可,如下图9


9.png
  • 根据推测使用穷举法需要计算 1*4*4*4*4*1 = 256次
  • 使用维特比算法需要计算 1*4+4*4+4*4+4*4+4*1 = 56次 (前后两个4因为beijing,shenzheng只有1个节点)

下面进行代码验证,首先使用穷举法,计算出所有可能的结果

# -*- coding: utf-8 -*-
import psutil
import os

# 标记每个城市在的坐标
a = {"a1": (1.1, 2.5), "a2": (1.8, 2.2), "a3": (1.3, 1.5), "a4": (1.9, 1)}
b = {"b1": (3.4, 2.4), "b2": (3.9, 1.8), "b3": (3.5, 1.1), "b4": (4, 0.4)}
c = {"c1": (5.6, 2.5), "c2": (5.7, 2), "c3": (5.9, 1.5), "c4": (5.8, 0.65)}
d = {"d1": (7.3, 2.5), "d2": (7.8, 2.2), "d3": (7.5, 1.5), "d4": (7.7, 0.95)}

# 假设北京的坐标为0,0 广州的坐标为 10,10
beijing = (0, 0)
shanghai = (10, 10)


# 计算路线总长度
def sum_way(way):
    result = 0
    for key in way:
        result += way[key]
    return round(result, 2)


# 定义一个dict保存所有可能的路线
all = {}
n = 0

# step1 计算北京到a各个点的距离,三角函数 ((x2-x1)^2 + (y2-y1)^2) ** 0.5
# 所有保留2位小树
for i in a:
    for j in b:
        for k in c:
            for m in d:
                n += 1
                all["beijing_" + i + "_" + j + "_" + k + "_" + m] = {
                    "detail": {
                        # 北京到a
                        "beijing_" + i: round((a[i][0] ** 2 + a[i][1] ** 2) ** 0.5, 2),
                        # a到b
                        i + "_" + j: round(((b[j][0] - a[i][0]) ** 2 + (b[j][1] - a[i][1]) ** 2) ** 0.5, 2),
                        # b到c
                        j + "_" + k: round(((c[k][0] - b[j][0]) ** 2 + (c[k][1] - b[j][1]) ** 2) ** 0.5, 2),
                        k + "_" + m: round(((d[m][0] - c[k][0]) ** 2 + (d[m][1] - c[k][1]) ** 2) ** 0.5, 2),
                        m + "_shenzhen": round(((10 - d[m][0]) ** 2 + (10 - d[m][1]) ** 2) ** 0.5, 2)
                    }
                }
                all["beijing_" + i + "_" + j + "_" + k + "_" + m]["length_all"] = \
                    sum_way(all["beijing_" + i + "_" + j + "_" + k + "_" + m]["detail"])

short_way = sorted(all.values(), key=lambda value: value["length_all"], reverse=True).pop()
print("最优路线:", "_".join([i.split("_")[0] for i in short_way["detail"]]))
print("总长度", short_way["length_all"])
print("节点计算次数", n, "次")
print('内存使用:', round(psutil.Process(os.getpid()).memory_info().rss / 1000, 2), "kb")

运算结果:

最优路线: beijing_a4_b2_c2_d1
总长度 15.76
节点计算次数 256 次
内存使用: 9576.45 kb

然后使用维特比算法

# -*- coding: utf-8 -*-
import psutil
import os
# 所有要计算的所有层节点组成一个网格
grid = [
    {"a1": (1.1, 2.5), "a2": (1.8, 2.2), "a3": (1.3, 1.5), "a4": (1.9, 1)},
    {"b1": (3.4, 2.4), "b2": (3.9, 1.8), "b3": (3.5, 1.1), "b4": (4, 0.4)},
    {"c1": (5.6, 2.5), "c2": (5.7, 2), "c3": (5.9, 1.5), "c4": (5.8, 0.65)},
    {"d1": (7.3, 2.5), "d2": (7.8, 2.2), "d3": (7.5, 1.5), "d4": (7.7, 0.95)},
    {"shanghai": (10, 10)}
]

# 上一节点最短路径 初始化时设置为开始位置
shortest_path = [
    {
        "tag": "beijing",
        "way": ["beijing"],
        "step": [(0, 0)],
        "step_length": [0],
        "length": 0
    }
]

# 对每一步计算的可能路径进行过滤,只保留到这层各个点最短路径
def get_shortest_path(temp_list):
    # 先排序
    temp_list.sort(key=lambda value: value["length"], reverse=True)
    # 再分组
    temp_dict = {}
    for i in temp_list:
        if temp_dict.get(i["tag"]) is None:
            temp_dict[i["tag"]] = list()
        temp_dict[i["tag"]].append(i)
    shortest_path_temp = []
    # 再保留每组最小值
    for k, v in temp_dict.items():
        shortest_path_temp.append(v.pop())
    return shortest_path_temp


n = 0
for no, info in enumerate(grid):
    temp_list = []
    for key, value in info.items():
        for path in shortest_path:
            # 记录计算次数
            n += 1;
            # 取起始点坐标
            start = path["step"].copy().pop()
            # 取目标点坐标
            end = value
            # 目标点名字
            name = key
            # 定义一个起始点的一个tag 分组用
            tag = path["tag"].split("_")[0] + "_" + name
            # 记录经过的路径
            way = path["way"].copy()
            way.append(key)
            # 记录每一步的坐标
            step = path["step"].copy()
            step.append(end)
            # 计算起始点到目标点长度
            length = round(((end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2) ** 0.5, 2)
            # 记录这一步的长度
            step_length = path["step_length"].copy()
            # 总长度
            step_length.append(length)
            length_num = round(path["length"] + length, 2)
            temp_list.append({"tag": tag, "way": way, "step": step, "step_length": step_length, "length": length_num})
    # 分组 保留最优解
    shortest_path = get_shortest_path(temp_list)

result = shortest_path.pop()
print("最优路线:", "_".join(result["way"]))
print("总长度:", result["length"])
print("节点计算次数", n, "次")
info = psutil.virtual_memory()
print('内存使用:', round(psutil.Process(os.getpid()).memory_info().rss / 1000, 2), "kb")

运算结果:

最优路线: beijing_a4_b2_c2_d1_shanghai
总长度: 15.76
节点计算次数 56 次
内存使用: 9211.9 kb

总结

  1. 维特比算法是一种动态规划算法,维特比算法总体思想就是,再前一步计算的最优解上,计算后一步所有可能出现的情况
  2. 对于一个长度(层数)为M,每层最大节点数为N的DAG,复杂度最差情况下为O(MN^2)
  3. 维特比算法仅解决了隐马尔可夫第三问,替代方法还有很多,不能直接将维特比算法和隐马尔可夫问题划等号
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 不知从什么时候开始,整句识别成为了拼音输入法的标配,几乎每一款输入法都有整句输入功能。重码量巨大的拼音输入也因此逐...
    罗立青阅读 2,475评论 0 7
  • 前言:作为堂堂数学系的学生,竟然很多算法都不会。表示对自己很失望,今天学习了一下维特比算法,发现很简单,而且隐约中...
    风之子doudou阅读 441评论 0 0
  • 动态规划求最短路径算法,与穷举法相比优点在于大大降低了时间复杂度; 假如从起点A到终点S的最短路径Road经过点B...
    NemoHo阅读 1,035评论 2 4
  • 街道,天桥,红绿灯,路口还是那个路口,转角的包子铺依旧热气腾腾,公交站台依旧拥挤不堪,高楼在建,行人匆匆……我是那...
    林若风阅读 551评论 0 0
  • 青岛市从2017年开始,大面积推广招投标工作的网上评标系统,把投标单位从繁重的纸面化投标渐渐地向轻便的电子化投标转...
    盼好运阅读 2,263评论 0 0