明白动态规划Dijkstra方法的Python实现和问题的解决步骤译

8次阅读

共计 2641 个字符,预计需要花费 7 分钟才能阅读完成。

原作者:金子冴
校阅:内野良一
翻译:叶子
原文链接

目录

  • 什么是动态规划(Dynamic Programming)
  • 例题:用 Dijkstra 的方法解决最短路径问题(Python 实现)
  • 使用动态规划解决问题的步骤
  • 参考

什么是动态规划(Dynamic Programming)

动态规划概要

动态规划是一种解题手法的总称。它通过将一个无法解决的大问题分解成复数个小问题 (也叫子问题),然后在解决这些小问题的基础之上来解决原始的大问题。通过使用动态规划,我们能将一部分在多项式时间内无法解决的问题,在类似多项式的时间内求得最优解(稍后会进行说明)。
判断一个问题是否可以通过动态规划来解决的时,我们需要判断该问题是否满足可分治 (分而治之) 和可记忆 (将阶段性成果进行缓存,便于重复利用) 两个条件。首先,让我们先去理解: 多项式时间、分而治之、以及记忆化(Memoization)。

什么是多项式时间,什么是多项式时间算法

多项式时间是指由多项式表示的计算时间。多项式时间算法是指当入力的大小 (长度或者个数) 是 n 的时候,计算时间 (执行步数) 的上限在 n 的多项式时间内能够表示的算法。比如,计算九九乘法表的算法的计算时间可以表示为 9 ×9。将其扩展到 nxn 的时候,计算时间用大 O 记法来表示的话,可以表示为 O(n2)。这表明该算法的计算时间的上限可以用 n2 来表示,因此计算 nxn 的乘法的算法可以说是多项式算法。
但是,在多项式时间内无法解决的问题也是存在的,比如说接下来将要说明的最短路径问题,在多项式时间内就无法解决。如下图所示的加权路线图,找一个从 START 开始到到达 GOAL 的花费最短 (权重最小) 的路线。

为了求最短路线,我们需要考虑全部路线的排列组合,在此基础之上进行花费的计算,要使得花费最小,那就需要找到最短的路径。像这样的问题,入力的规模每增大一点,路线的组合就呈指数级增加,因此计算全部路线的花费是不现实的。但是,如果使用了动态规划,就可以求得类似最短路径这样的在多项式时间内无法解决的问题的最优解。计算时会使用分而治之和记忆化两种手法。

什么是分而治之(分治)

分治指的是将目标问题分割成复数个子问题的手法。让我们试着将刚才提到的最短路径问题进行子问题分解。对于刚才提到的例子,首先不要去考虑从 START 开始能够到达 END 的所有路线,而应该只考虑在某个时间点能够推进的路线。所以对于最开始的路线,只需要考虑 START 到 a,b,c,d 这四条。考虑到我们要解决的是最短路径的问题,这里我们选择从 START 开始花费最小的 START->b 路线开始。接着,我们只需考虑从 b 点出发能够推进的路线,这里我们也是选择花费最少的路线,b->g 路线。

像这样,将一个需要考虑全部路径的问题转换为只考虑某个时间点能够推进的路线的问题 (子问题) 的分治手法,叫做分而治之。

什么是记忆化

记忆化是指将计算结果保存到内存上,之后再次利用的手法。作为解释记忆化的例子,让我们来思考一下斐波那契数列的问题。这里我们省略斐波那契数列数列的说明。使用 python 进行斐波那契数列计算的场合,代码编写如下所示:

清单 1

CulcFibonacci.py

import sys

# フィボナッチ数の計算
def culc_fibonacci(n):
    if n > 1:
        return culc_fibonacci(n-1) + culc_fibonacci(n-2)
    elif n == 1:
        return 1
    else:
        return 0

def main():
    # 1~10 番目フィボナッチ数列を表示
    #     ⇒ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    for n in range(10):
        fibonacci_n = culc_fibonacci(n)
        print(fibonacci_n, end='')
        if not  n == 9:
            print(',', end='')


if __name__ == '__main__':
    main()
    sys.exit(0)

但是,清单 1 所示代码,在计算 n =10 的时候,必须去计算 n =9~1,因此计算时间是 O(αn:α 的 n 次幂)(α: 实数),所以当 n 变大的时候,相关的计算量会呈指数级增长。
下图表示的是斐波那契数列的计算过程。从下图我们可以看出,除了 f(10)之外的所有计算都不止一次。

将清单所示代码用记忆化进行优化的时候,如何减少复数次计算是重点。为了进行记忆化,我们需要做一个记忆化表,将第一次计算的值存储到该表之中。

这样,当我们需要再次计算某个值的时候,直接去该表当中查询之前计算过得值即可。这样就防止了进行多次同样的计算。

如下所示清单 2 的源代码,对清单 1 的源代码进行了记忆化优化。

清单 2

CulcFibonacciMemo.py

import sys

# メモ化テーブル(辞書形式)
fibonacci_list = {}

# フィボナッチ数の計算(メモ化あり)
def culc_fibonacci_memo(n):
    global fibonacci_list
    if n == 1:
        return 1
    elif n == 0:
        return 0
    if not n in fibonacci_list:
        fibonacci_list[n] = culc_fibonacci_memo(n-1) + culc_fibonacci_memo(n-2)
    return fibonacci_list[n]

def main():
    # 1~10 番目フィボナッチ数列を表示
    #     ⇒ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    for n in range(10):
        fibonacci_n = culc_fibonacci_memo(n)
        print(fibonacci_n, end='')
        if not  n == 9:
            print(',', end='')

if __name__ == '__main__':
    main()
    sys.exit(0)

记忆化的最大优点是通过减少计算量,从而减少了计算的时间。清单 2 所示代码会将第一次计算的斐波那契数存储起来,之后通过再次利用之前的计算结果来减少计算量。实际上,笔者在自己的 PC 上计算 f(40)的斐波那契数的时候,清单 1 没有进行记忆化优化的程序用了 101.9 秒,而清单 2 进行了记忆化优化的程序只用了 0.2 秒,两者的计算时间相比,后者的计算时间大幅度缩减。由于动态规划是以递归的方式计算子问题,因此这种存储优化非常重要。
对于动态规划的概要说明到此为止,接下来的章节我们将尝试用 Dijkstra 算法 (动态规划的一种) 来解决最短路径的问题。

下一节将介绍用 Dijkstra 的方法解决最短路径问题(Python 实现)。

正文完
 0