算法-五大常用算法:动态规划(DP)算法【先求解小数据量下问题的结果,然后层层递推,求解更大的数据量的问题的结果】【维特比算法(Viterbi Algorithm)属于动态规划算法】

在这里插入图片描述
在这里插入图片描述

一、动态规划(Dynamic Programming)算法

1、基本概念

动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

2、基本思想与策略

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

3、适用的情况

能采用动态规划求解的问题的一般要具有3个性质:

  1. 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

  2. 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

  3. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

4、求解的基本步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

  1. 划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

  2. 确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

  3. 确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

  4. 寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

  1. 分析最优解的性质,并刻画其结构特征。

  2. 递归的定义最优解。

  3. 以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

  4. 根据计算最优值时得到的信息,构造问题的最优解

5、算法实现的说明

动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

使用动态规划求解问题,最重要的就是确定动态规划三要素:

  1. 问题的阶段
  2. 每个阶段的状态
  3. 从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

f ( n , m ) = m a x { f ( n − 1 , m ) , f ( n − 1 , m − w [ n ] ) + P ( n , m ) } f(n,m)=max\{f(n-1,m), f(n-1,m-w[n])+P(n,m)\} f(n,m)=max{f(n1,m),f(n1,mw[n])+P(n,m)}

在这里插入图片描述

6、动态规划案例

6.1 最小编辑距离

在这里插入图片描述

import numpy as np

ins_cost = 1
del_cost = 1
rep_cost = 1
trp_cost = 1


def min_distannce(source, target):
    print("source = {0}----target = {1}".format(source, target))
    m, n = len(source), len(target)
    print("m = {0}----n = {1}".format(m, n))
    if m == 0 and n == 0:
        return 0
    if m * n == 0:
        return m + n

    dp = np.zeros((m + 1, n + 1), dtype=int)  # initialize cost matrix with zeros and dimensions (m+1,n+1) 【纵向表示source,横向表示target】
    # [[0 0 0 0 0]
    #  [0 0 0 0 0]
    #  [0 0 0 0 0]
    #  [0 0 0 0 0]
    #  [0 0 0 0 0]
    #  [0 0 0 0 0]]
    print("\n初始化:dp = \n{0}".format(dp))

    # Fill in column 0, from row 1 to row m, both inclusive【当 target为空字符串,source变为target要做删除操作,直到删除干净】
    for row in range(1, m + 1):
        dp[row, 0] = dp[row - 1, 0] + 1

    # Fill in row 0, for all columns from 1 to n, both inclusive【 source为空字符串,source变为target要一直做插入操作,直到变为target】
    for col in range(1, n + 1):
        dp[0, col] = dp[0, col - 1] + 1
    # [[1 2 3 4 5]
    #  [2 0 0 0 0]
    #  [3 0 0 0 0]
    #  [4 0 0 0 0]
    #  [5 0 0 0 0]
    #  [6 0 0 0 0]]
    print("\n填充首行、首列后:dp = \n{0}".format(dp))

    # 当source、target 都不为空的时候,有2中可能的操作
    # 当source[row] = target[col]的时候,
    #       dp[row, col] = dp[row-1, col-1],也就是不用做任何操作就匹配了【直接继承前序即可】
    # 当source[row] ≠ target[col]的时候:
    #       dp[row, col] = min(dp[row-1, col-1], dp[row-1, col], dp[row, col-1]) + 1
    # 也就是可以进行3种操作来实现source[row]转为target[col]:
    #       dp[row-1, col-1] + 1 ----> 替换操作
    #       dp[row-1, col] + 1 ----> 删除操作
    #       dp[row, col-1] + 1 ----> 插入操作

    # Loop through row 1 to row m, both inclusive
    for row in range(1, m + 1):
        # Loop through column 1 to column n, both inclusive
        for col in range(1, n + 1):
            # Check to see if source character at the previous row matches the target character at the previous column,
            if source[row - 1] == target[col - 1]:
                dp[row, col] = dp[row - 1][col - 1]
            else:
                # Update the cost at row, col based on previous entries in the cost matrix
                dp[row, col] = min(dp[row - 1][col - 1], dp[row - 1][col], dp[row][col - 1]) + 1
    print("\n动态填充结束:dp = \n{0}".format(dp))
    min_distance = dp[-1][-1]
    return min_distance


if __name__ == "__main__":
    min_edit_distannce = min_distannce("oppa", "apple")
    print("\nmin_edit_distannce = {0}".format(min_edit_distannce))

打印结果:

source = oppa----target = apple
m = 4----n = 5

初始化:dp = 
[[0 0 0 0 0 0]
 [0 0 0 0 0 0]
 [0 0 0 0 0 0]
 [0 0 0 0 0 0]
 [0 0 0 0 0 0]]

填充首行、首列后:dp = 
[[0 1 2 3 4 5]
 [1 0 0 0 0 0]
 [2 0 0 0 0 0]
 [3 0 0 0 0 0]
 [4 0 0 0 0 0]]

动态填充结束:dp = 
[[0 1 2 3 4 5]
 [1 1 2 3 4 5]
 [2 2 1 2 3 4]
 [3 3 2 1 2 3]
 [4 3 3 2 2 3]]

min_edit_distannce = 3

Process finished with exit code 0

6.2 最长上升子序列

最长上升子序列是动规的经典题目,这里dp[i]是可以根据 dp[j] (j < i)推导出来的,那么依然用动规五部曲来分析详细一波:

  1. d p [ i ] dp[i] dp[i] 的定义
    dp[i]表示i之前包括i的最长上升子序列。
  2. 状态转移方程
    位置 i i i 的最长升序子序列等于 j j j 0 0 0 i − 1 i-1 i1 各个位置的最长升序子序列 + 1 的最大值。
    所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
    注意这里不是要 d p [ i ] dp[i] dp[i] d p [ j ] + 1 dp[j] + 1 dp[j]+1 进行比较,而是我们要取 d p [ i ] 、 d p [ j ] + 1 dp[i]、dp[j] + 1 dp[i]dp[j]+1 二者中的最大值。
  3. d p [ i ] dp[i] dp[i] 的初始化
    每一个 i i i,对应的 d p [ i ] dp[i] dp[i](即最长上升子序列)起始大小至少都是是1。
  4. 确定遍历顺序
    d p [ i ] dp[i] dp[i] 是由 0 0 0 i − 1 i-1 i1 各个位置的最长升序子序列 推导而来,那么遍历 i i i 一定是从前向后遍历。
    j j j 其实就是 0 0 0 i − 1 i-1 i1,遍历 i i i 的循环在外层,遍历 j j j 则在内层,代码如下:
    for (int i = 1; i < nums.size(); i++) {
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) 
            	dp[i] = max(dp[i], dp[j] + 1);
        }
        if (dp[i] > result) 
        	result = dp[i]; // 取长的子序列
    }
    
  5. 举例推导dp数组
    输入:[0,1,0,3,2],dp数组的变化如下:
    在这里插入图片描述
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        dp=[1]*len(nums)	# 初始化dp
        res=1
        for i in range(1,len(nums)):
            for j in range(0, i):
                if nums[i]>nums[j]:
                    dp[i]=max(dp[i], dp[j]+1)
            res=max(res, dp[i]) # 取更长的子序列
        return res
from typing import List


class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        dp = [1] * len(nums)  # 初始化
        for i in range(len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        print("dp = ", dp)
        return max(dp)


nums = [10, 9, 2, 5, 3, 7, 101, 18]

solution = Solution()
nums = [10, 9, 2, 5, 3, 7, 101, 18]
result = solution.lengthOfLIS(nums)
print("result = ", result)

二、维特比算法(Viterbi Algorithm)

1、维特比算法(Viterbi Algorithm)讲解方式01:篱笆网络(Lattice)的最短路径问题

已知下图的篱笆网络,每个节点之间的数字表示相邻节点之间的距离,举个例子来说,如果我走,这个距离是。那么如果让你从A走到E,最短路径是哪一条呢?
在这里插入图片描述
显然大家都知道,通过穷举的方法是很容易得到最短路径,可是问题就在于如果穷举的话,需要的加法次数不用算你也知道实在是太多啦(每条路径需要计算次加法,一共条路径共次计算)!像这种没几层的篱笆网络也就罢了,如果每层13个节点,一共12层(然而这个规模对于标注问题来说也依然根本不算什么),可想而知那个线有多乱,如果仅仅穷举的话,这个计算量(大致是每条次计算,一共条路径共大约次计算)怕是超级计算机也吃不消。

如下图,假如你从S和E之间找一条最短的路径,除了遍历完所有路径,还有什么更好的方法?答案:viterbi (维特比)算法。

在这里插入图片描述

viterbi维特比算法解决的是篱笆型的图的最短路径问题,图的节点按列组织,每列的节点数量可以不一样,每一列的节点只能和相邻列的节点相连,不能跨列相连,节点之间有着不同的距离,距离的值就不在图上一一标注出来了,大家自行脑补。

为了找出S到E之间的最短路径,我们先从S开始从左到右一列一列地来看。

首先起点是S,从S到A列的路径有三种可能:S-A1、S-A2、S-A3,如下图:

在这里插入图片描述

我们不能武断地说S-A1、S-A2、S-A3中的哪一段必定是全局最短路径中的一部分,目前为止任何一段都有可能是全局最短路径的备选项。

我们继续往右看,到了B列。按B列的B1、B2、B3逐个分析。

先看B1:

在这里插入图片描述
在这里插入图片描述

如上图,经过B1的所有路径只有3条:

S-A1-B1

S-A2-B1

S-A3-B1

以上这三条路径,各节点距离加起来对比一下,我们就可以知道其中哪一条是最短的。假设 S-A3-B1 是最短的,那么我们就知道了经过B1的所有路径当中S-A3-B1是最短的,其它两条路径路径S-A1-B1和S-A2-B1都比S-A3-B1长,绝对不是目标答案,可以大胆地删掉了。删掉了不可能是答案的路径,就是viterbi算法(维特比算法)的重点,因为后面我们再也不用考虑这些被删掉的路径了。现在经过B1的所有路径只剩一条路径了,如下图:

在这里插入图片描述

接下来,我们继续看B2:

在这里插入图片描述

同理,如上图,经过B2的路径有3条:

S-A1-B2

S-A2-B2

S-A3-B2

这三条路径中,各节点距离加起来对比一下,我们肯定也可以知道其中哪一条是最短的,假设S-A1-B2是最短的,那么我们就知道了经过B2的所有路径当中S-A1-B2是最短的,其它两条路径路径S-A2-B2和S-A3-B1也可以删掉了。经过B2所有路径只剩一条,如下图:

在这里插入图片描述

接下来我们继续看B3:

在这里插入图片描述

同理,如上图,经过B3的路径也有3条:

S-A1-B3

S-A2-B3

S-A3-B3

这三条路径中我们也肯定可以算出其中哪一条是最短的,假设S-A2-B3是最短的,那么我们就知道了经过B3的所有路径当中S-A2-B3是最短的,其它两条路径路径S-A1-B3和S-A3-B3也可以删掉了。经过B3的所有路径只剩一条,如下图:

在这里插入图片描述
现在对于B列的所有节点我们都过了一遍,B列的每个节点我们都删除了一些不可能是答案的路径,看看我们剩下哪些备选的最短路径,如下图:

在这里插入图片描述
上图是我们删掉了其它不可能是最短路径的情况,留下了三个有可能是最短的路径:S-A3-B1、S-A1-B2、S-A2-B3。现在我们将这三条备选的路径放在一起汇总到下图:

在这里插入图片描述
S-A3-B1、S-A1-B2、S-A2-B3都有可能是全局的最短路径的备选路径,我们还没有足够的信息判断哪一条一定是全局最短路径的子路径。

如果我们你认为没毛病就继续往下看C列,如果不理解,回头再看一遍,前面的步骤决定你是否能看懂viterbi算法(维特比算法)。

接下来讲到C列了,类似上面说的B列,我们从C1、C2、C3一个个节点分析。

经过C1节点的路径有:

S-A3-B1-C1、

S-A1-B2-C1、

S-A2-B3-C1

在这里插入图片描述
和B列的做法一样,从这三条路径中找到最短的那条(假定是S-A3-B1-C1),其它两条路径同样道理可以删掉了。那么经过C1的所有路径只剩一条,如下图:

在这里插入图片描述

同理,我们可以找到经过C2和C3节点的最短路径,汇总一下:

在这里插入图片描述
到达C列时最终也只剩3条备选的最短路径,我们仍然没有足够信息断定哪条才是全局最短。

最后,我们继续看E节点,才能得出最后的结论。

到E的路径也只有3种可能性:

在这里插入图片描述
E点已经是终点了,我们稍微对比一下这三条路径的总长度就能知道哪条是最短路径了。

在这里插入图片描述
在效率方面相对于粗暴地遍历所有路径,viterbi 维特比算法到达每一列的时候都会删除不符合最短路径要求的路径,大大降低时间复杂度。

2、维特比算法(Viterbi Algorithm)讲解方式02:分词算法

维特比算法(Viterbi Algorithm)本质上还是动态规划(Dynamic Programming)

例子:“经常有意见分歧”

我们仍然是有以下几个数据:

词典:["经常","有","意见","意","见","有意见","分歧","分","歧"]
概率P(x):{"经常":0.08,"有":0.04,"意见":0.08,"意":0.01,"见":0.005,"有意见":0.002,"分歧":0.04,"分":0.02, "歧":0.005}
-ln(P(x)):{"经常":2.52,"有":3.21,"意见":2.52,"意":4.6,"见":5.29,"有意见":6.21,"分歧":3.21,"分":3.9, "歧":5.29}

如果某个词不在字典中,我们将认为其 − l n [ P ( x ) ] −ln[P(x)] ln[P(x)] 值为20。

我们构建以下的DAG(有向图),每一个边代表一个词,我们将 − l n [ P ( x ) ] -ln[P(x)] ln[P(x)]的值标到边上,
在这里插入图片描述

− l n [ P ( x ) ] −ln[P(x)] ln[P(x)] 的最小值问题,就转变为求最短路径的问题。

由图可以看出,路径 0—>②—>③—>⑤—>⑦ 所求的值最小,所以其就是最优结果:经常 / 有 / 意见 / 分歧

那么我们应该怎样快速计算出来这个结果呢?

我们设 f ( n ) f(n) f(n) 代表从起点 0 0 0 到结点 n n n 的最短路径的值,所以我们想求的就是 f ( 7 ) f(7) f(7),从DAG图中可以看到,到结点⑦有2条路径:

  • 从结点⑤—>结点⑦: f ( 7 ) = f ( 5 ) + 3.21 f(7)=f(5)+3.21 f(7)=f(5)+3.21
  • 从结点⑥—>结点⑦: f ( 7 ) = f ( 6 ) + 5.29 f(7)=f(6)+5.29 f(7)=f(6)+5.29

我们应该从2条路径中选择路径短的。

在上面的第1条路径中, f ( 5 ) f(5) f(5) 还是未知的,我们要 f ( 5 ) f(5) f(5),同理我们发现到结点⑤的路径有3条路径:

  • 从结点②—>结点⑤: f ( 5 ) = f ( 2 ) + 6.21 f(5)=f(2)+6.21 f(5)=f(2)+6.21
  • 从结点③—>结点⑤: f ( 5 ) = f ( 3 ) + 2.52 f(5)=f(3)+2.52 f(5)=f(3)+2.52
  • 从结点④—>结点⑤: f ( 5 ) = f ( 4 ) + 20 f(5)=f(4)+20 f(5)=f(4)+20

我们同样从3条路径中选择路径短的。以此类推,直到结点0,所有的路径值都可以算出来。我们维护一个列表来表示 f ( n ) f(n) f(n) 的各值:

结点1234567
f(n)202.525.7325.738.2512.511.46
结点的上一个结点00

第2行代表从起点0到该结点的最短路径的值,第3行代表在最短路径中的该节点的上一个结点。

通过表,我们可以找到结点⑦的上一个结点⑤,结点⑤的上一个结点③,结点③的上一个结点②,结点②的上一个结点0,即路径:0—>②—>③—>⑤—>⑦

# -*- coding: utf-8 -*-
import math
import collections


# 维特比算法(viterbi)
def word_segmentation(text):
    ####################################################################################################################################################################
    word_dictionaries = ["经常", "有", "意见", "意", "见", "有意见", "分歧", "分", "歧"]
    probability = {"经常": 0.08, "有": 0.04, "意见": 0.08, "意": 0.01, "见": 0.005, "有意见": 0.002, "分歧": 0.04, "分": 0.02, "歧": 0.005}
    probability_ln = {key: -math.log(probability[key]) for key in probability}
    # probability_ln = {'经常': 2.5257286443082556, '有': 3.2188758248682006, '意见': 2.5257286443082556, '意': 4.605170185988091, '见': 5.298317366548036, '有意见': 6.214608098422191, '分歧': 3.2188758248682006, '分': 3.912023005428146, '歧': 5.298317366548036}
    print("probability_ln = {0}".format(probability_ln))
    # 构造图的代码并没有实现,以下只是手工建立的图【如果某个词不在字典中,我们将认为其 −ln[P(x)] 值为20。】,为了说明 维特比算法
    ####################################################################################################################################################################
    # 有向五环图,存储的格式:key是结点名,value是一个结点的所有上一个结点(以及边上的权重)
    graph = {
        0: {0: (0, "")},
        1: {0: (20, "经")},
        2: {0: (2.52, "经常"), 1: (20, "常")},
        3: {2: (3.21, "有")},
        4: {3: (20, "意")},
        5: {2: (6.21, "有意见"), 3: (2.52, "意见"), 4: (5.30, "见")},
        6: {5: (3.9, "分")},
        7: {5: (3.21, "分歧"), 6: (5.29, "歧")}
    }
    # =====================================================================利用“维特比算法”构建各个节点的最优路径:开始=====================================================================
    print("#"*50, "利用“维特比算法”构建各个节点的最优路径:开始", "#"*50)
    f = collections.OrderedDict()  # 保存结点n的f(n)以及实现f(n)的上一个结点【f(n):代表从起点 0 到结点 n 的最短路径的值】
    for key, value in graph.items():  # 遍历有向图graph中的所有节点
        print("\nkey = {0}----value = {1}".format(key, value))
        tuple_temp_list = []
        for pre_node_key, pre_node_value in value.items():  # 遍历当前节点的所有上一个节点【pre_node_key:上一个节点的节点号,pre_node_value:本节点距离上一个节点的距离】
            # print("本节点的节点号:key = {0}----上一个节点的节点号:pre_node_key = {1}----本节点距离上一个节点的距离:pre_node_value = {2}".format(key, pre_node_key, pre_node_value))
            distance_from_0 = 0
            if pre_node_key not in f:  # 当遍历到0节点时,该节点的上一个结点还没有计算f(n);
                distance_from_0 = pre_node_value[0]  # 0节点的上一节点(依旧时0节点)的距离
            else:  # 当遍历到0节点之后的节点
                distance_from_0 = pre_node_value[0] + f[pre_node_key][0]  # pre_node_value[0]:当前节点距离上一节点的距离;f[pre_node_key][0]:当前节点的上一节点“pre_node_key”距离0节点的最短距离
                print("本节点的节点号:key = {0}----本节点可触及的上一节点号:pre_node_key = {1}----本节点距离上一个节点“节点{1}”的距离:pre_node_value = {2}----上一节点“节点{1}”距离0节点的最短距离:f[pre_node_key][0] = {3}----本节点路径上一节点“节点{1}”距离0节点的距离:distance_from_0 = {4}".format(key, pre_node_key, pre_node_value, f[pre_node_key][0], distance_from_0))
            tuple_temp = (distance_from_0, pre_node_key)  # 【pre_node_value[0]:本节点距离0节点的最短距离;pre_node_key:本节点实现距离0节点距离最短时的上一个节点的节点号】
            tuple_temp_list.append(tuple_temp)
        min_temp = min(tuple_temp_list)  # 比较比较当前节点路径所触及的所有上一节点到达0节点的距离,得出当前节点 key 距离0节点的最短距离
        # min_temp = min((pre_node_value[0], pre_node_key) if pre_node_key not in f else (pre_node_value[0] + f[pre_node_key][0], pre_node_key) for pre_node_key, pre_node_value in value.items())  # 高阶写法
        print("本节点的节点号:key = {0}----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = {1}----当前节点 key 距离0节点的最短距离:min_temp = {2}".format(key, tuple_temp_list, min_temp))
        f[key] = min_temp
        print("将当前节点{0}距离0节点的(最短距离,路径的节点号)= ({0},{1}) 加入f---->f = {2}".format(key, min_temp, f))  # f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)), (4, (25.73, 3)), (5, (8.25, 3)), (6, (12.15, 5)), (7, (11.46, 5))])
    print("#" * 50, "利用“维特比算法”构建各个节点的最优路径:结束", "#" * 50)
    # =====================================================================利用“维特比算法”构建各个节点的最优路径:结束=====================================================================

    # =====================================================================提取最优最优路径:开始=====================================================================
    print("\n", "#" * 50, "提取最优路径:开始", "#" * 50)
    last = next(reversed(f))  # 最后一个结点7
    first = next(iter(f))  # 第一个结点0
    path_result = [last, ]  # 保存路径,最后一个结点先添入
    pre_last = f[last]  # 最后一个结点的所有前一个结点
    print("最后一个结点7:last = {0}----第一个结点0:first = {1}----初始化最优路径:path_result = {2}----最后一个结点的所有前一个结点:pre_last = {3}".format(last, first, path_result, pre_last))

    while pre_last[1] is not first:  # 没到达第一个结点就一直循环,查找上一个节点的上一个节点号
        path_result.append(pre_last[1])  # 加入一个路径结点X
        pre_last = f[pre_last[1]]  # 定位到路径结点X的上一个结点
    path_result.append(first)  # 第一个结点添入
    print("最优路径:path_result = {0}".format(path_result))  # 结果:[7, 5, 3, 2, 0]
    print("#" * 50, "提取最优路径:结束", "#" * 50)
    # =====================================================================提取最优最优路径:结束=====================================================================

    # =====================================================================通过最优路径得到分词结果:开始=====================================================================
    print("\n", "#" * 50, "通过最优路径得到分词结果:开始", "#" * 50)
    text_result = []
    for index, num in enumerate(path_result):  # 找到路径上边的词
        if index + 1 == len(path_result):
            break
        word = graph[num][path_result[index + 1]][1]
        print("最优路径:path_result = {0}----index = {1}----当前节点号:num = {2}----在最优路径里,当前节点号的上一个节点号:path_result[index + 1] = {3}----当前节点号{2}与上一节点号{3}之间的词汇:{4}".format(path_result, index, num, path_result[index + 1], word))
        text_result.append(word)
    print("text_result = {0}".format(text_result))
    text_result.reverse()  # 翻转一下
    print("翻转后:text_result = {0}".format(text_result))
    print("#" * 50, "通过最优路径得到分词结果:结束", "#" * 50)
    return "".join(word + "/" for word in text_result)
    # =====================================================================通过最优路径得到分词结果:结束=====================================================================


if __name__ == '__main__':
    content = "经常有意见分歧"
    word_segmentation_result = word_segmentation(content)
    print("word_segmentation_result:", word_segmentation_result)

打印结果:

probability_ln = {'经常': 2.5257286443082556, '有': 3.2188758248682006, '意见': 2.5257286443082556, '意': 4.605170185988091, '见': 5.298317366548036, '有意见': 6.214608098422191, '分歧': 3.2188758248682006, '分': 3.912023005428146, '歧': 5.298317366548036}
################################################## 利用“维特比算法”构建各个节点的最优路径:开始 ##################################################
key = 0----value = {0: (0, '')}
本节点的节点号:key = 0----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(0, 0)]----当前节点 key 距离0节点的最短距离:min_temp = (0, 0)
将当前节点0距离0节点的(最短距离,路径的节点号)= (0,(0, 0)) 加入f---->f = OrderedDict([(0, (0, 0))])

key = 1----value = {0: (20, '经')}
本节点的节点号:key = 1----本节点可触及的上一节点号:pre_node_key = 0----本节点距离上一个节点“节点0”的距离:pre_node_value = (20, '经')----上一节点“节点0”距离0节点的最短距离:f[pre_node_key][0] = 0----本节点路径上一节点“节点0”距离0节点的距离:distance_from_0 = 20
本节点的节点号:key = 1----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(20, 0)]----当前节点 key 距离0节点的最短距离:min_temp = (20, 0)
将当前节点1距离0节点的(最短距离,路径的节点号)= (1,(20, 0)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0))])

key = 2----value = {0: (2.52, '经常'), 1: (20, '常')}
本节点的节点号:key = 2----本节点可触及的上一节点号:pre_node_key = 0----本节点距离上一个节点“节点0”的距离:pre_node_value = (2.52, '经常')----上一节点“节点0”距离0节点的最短距离:f[pre_node_key][0] = 0----本节点路径上一节点“节点0”距离0节点的距离:distance_from_0 = 2.52
本节点的节点号:key = 2----本节点可触及的上一节点号:pre_node_key = 1----本节点距离上一个节点“节点1”的距离:pre_node_value = (20, '常')----上一节点“节点1”距离0节点的最短距离:f[pre_node_key][0] = 20----本节点路径上一节点“节点1”距离0节点的距离:distance_from_0 = 40
本节点的节点号:key = 2----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(2.52, 0), (40, 1)]----当前节点 key 距离0节点的最短距离:min_temp = (2.52, 0)
将当前节点2距离0节点的(最短距离,路径的节点号)= (2,(2.52, 0)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0))])

key = 3----value = {2: (3.21, '有')}
本节点的节点号:key = 3----本节点可触及的上一节点号:pre_node_key = 2----本节点距离上一个节点“节点2”的距离:pre_node_value = (3.21, '有')----上一节点“节点2”距离0节点的最短距离:f[pre_node_key][0] = 2.52----本节点路径上一节点“节点2”距离0节点的距离:distance_from_0 = 5.73
本节点的节点号:key = 3----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(5.73, 2)]----当前节点 key 距离0节点的最短距离:min_temp = (5.73, 2)
将当前节点3距离0节点的(最短距离,路径的节点号)= (3,(5.73, 2)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2))])

key = 4----value = {3: (20, '意')}
本节点的节点号:key = 4----本节点可触及的上一节点号:pre_node_key = 3----本节点距离上一个节点“节点3”的距离:pre_node_value = (20, '意')----上一节点“节点3”距离0节点的最短距离:f[pre_node_key][0] = 5.73----本节点路径上一节点“节点3”距离0节点的距离:distance_from_0 = 25.73
本节点的节点号:key = 4----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(25.73, 3)]----当前节点 key 距离0节点的最短距离:min_temp = (25.73, 3)
将当前节点4距离0节点的(最短距离,路径的节点号)= (4,(25.73, 3)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)), (4, (25.73, 3))])

key = 5----value = {2: (6.21, '有意见'), 3: (2.52, '意见'), 4: (5.3, '见')}
本节点的节点号:key = 5----本节点可触及的上一节点号:pre_node_key = 2----本节点距离上一个节点“节点2”的距离:pre_node_value = (6.21, '有意见')----上一节点“节点2”距离0节点的最短距离:f[pre_node_key][0] = 2.52----本节点路径上一节点“节点2”距离0节点的距离:distance_from_0 = 8.73
本节点的节点号:key = 5----本节点可触及的上一节点号:pre_node_key = 3----本节点距离上一个节点“节点3”的距离:pre_node_value = (2.52, '意见')----上一节点“节点3”距离0节点的最短距离:f[pre_node_key][0] = 5.73----本节点路径上一节点“节点3”距离0节点的距离:distance_from_0 = 8.25
本节点的节点号:key = 5----本节点可触及的上一节点号:pre_node_key = 4----本节点距离上一个节点“节点4”的距离:pre_node_value = (5.3, '见')----上一节点“节点4”距离0节点的最短距离:f[pre_node_key][0] = 25.73----本节点路径上一节点“节点4”距离0节点的距离:distance_from_0 = 31.03
本节点的节点号:key = 5----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(8.73, 2), (8.25, 3), (31.03, 4)]----当前节点 key 距离0节点的最短距离:min_temp = (8.25, 3)
将当前节点5距离0节点的(最短距离,路径的节点号)= (5,(8.25, 3)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)), (4, (25.73, 3)), (5, (8.25, 3))])

key = 6----value = {5: (3.9, '分')}
本节点的节点号:key = 6----本节点可触及的上一节点号:pre_node_key = 5----本节点距离上一个节点“节点5”的距离:pre_node_value = (3.9, '分')----上一节点“节点5”距离0节点的最短距离:f[pre_node_key][0] = 8.25----本节点路径上一节点“节点5”距离0节点的距离:distance_from_0 = 12.15
本节点的节点号:key = 6----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(12.15, 5)]----当前节点 key 距离0节点的最短距离:min_temp = (12.15, 5)
将当前节点6距离0节点的(最短距离,路径的节点号)= (6,(12.15, 5)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)), (4, (25.73, 3)), (5, (8.25, 3)), (6, (12.15, 5))])

key = 7----value = {5: (3.21, '分歧'), 6: (5.29, '歧')}
本节点的节点号:key = 7----本节点可触及的上一节点号:pre_node_key = 5----本节点距离上一个节点“节点5”的距离:pre_node_value = (3.21, '分歧')----上一节点“节点5”距离0节点的最短距离:f[pre_node_key][0] = 8.25----本节点路径上一节点“节点5”距离0节点的距离:distance_from_0 = 11.46
本节点的节点号:key = 7----本节点可触及的上一节点号:pre_node_key = 6----本节点距离上一个节点“节点6”的距离:pre_node_value = (5.29, '歧')----上一节点“节点6”距离0节点的最短距离:f[pre_node_key][0] = 12.15----本节点路径上一节点“节点6”距离0节点的距离:distance_from_0 = 17.44
本节点的节点号:key = 7----当前节点路径所触及的所有上一节点到达0节点的距离:tuple_temp_list = [(11.46, 5), (17.44, 6)]----当前节点 key 距离0节点的最短距离:min_temp = (11.46, 5)
将当前节点7距离0节点的(最短距离,路径的节点号)= (7,(11.46, 5)) 加入f---->f = OrderedDict([(0, (0, 0)), (1, (20, 0)), (2, (2.52, 0)), (3, (5.73, 2)), (4, (25.73, 3)), (5, (8.25, 3)), (6, (12.15, 5)), (7, (11.46, 5))])
################################################## 利用“维特比算法”构建各个节点的最优路径:结束 ##################################################

 ################################################## 提取最优路径:开始 ##################################################
最后一个结点7:last = 7----第一个结点0:first = 0----初始化最优路径:path_result = [7]----最后一个结点的所有前一个结点:pre_last = (11.46, 5)
最优路径:path_result = [7, 5, 3, 2, 0]
################################################## 提取最优路径:结束 ##################################################

 ################################################## 通过最优路径得到分词结果:开始 ##################################################
最优路径:path_result = [7, 5, 3, 2, 0]----index = 0----当前节点号:num = 7----在最优路径里,当前节点号的上一个节点号:path_result[index + 1] = 5----当前节点号7与上一节点号5之间的词汇:分歧
最优路径:path_result = [7, 5, 3, 2, 0]----index = 1----当前节点号:num = 5----在最优路径里,当前节点号的上一个节点号:path_result[index + 1] = 3----当前节点号5与上一节点号3之间的词汇:意见
最优路径:path_result = [7, 5, 3, 2, 0]----index = 2----当前节点号:num = 3----在最优路径里,当前节点号的上一个节点号:path_result[index + 1] = 2----当前节点号3与上一节点号2之间的词汇:有
最优路径:path_result = [7, 5, 3, 2, 0]----index = 3----当前节点号:num = 2----在最优路径里,当前节点号的上一个节点号:path_result[index + 1] = 0----当前节点号2与上一节点号0之间的词汇:经常
text_result = ['分歧', '意见', '有', '经常']
翻转后:text_result = ['经常', '有', '意见', '分歧']
################################################## 通过最优路径得到分词结果:结束 ##################################################
word_segmentation_result: 经常//意见/分歧/

Process finished with exit code 0



参考资料:
Dynamic Programming – Edit Distance Problem
Dynamic Programming Practice Problems
什么是动态规划(Dynamic Programming)?动态规划的意义是什么?
漫画:什么是动态规划?(整合版)
如何通俗地讲解 viterbi 算法?
小白给小白详解维特比算法(一)
编辑距离 计算
使用编辑距离计算文本相似度
最小编辑距离(Levenshtein)的 Python 实现
编辑距离算法详解:Levenshtein Distance算法
一个快速、高效的Levenshtein算法实现
使用优化的Levenshtein算法查找最接近的邻居

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值