目录

31 初识动态规划

编程思想之动态规划初识

1. 动态规划

动态规划是几个编程思想中最难的一个,它与回溯密切相关。回溯问题是在一组可能的解中,搜索满足期望的解;采用的方法类似枚举,找出所有解,筛选符合要求的解;而动态规划比较适合用来求解最优问题,比如求最大值、最小值等等。基本上所有的动态规划问题都能用回溯算法解决,但是动态规划能有效避免回溯算法中的重复计算,提高代码执行效率。

1.1 解决思路

上一节我们用回溯算法解决了0-1背包问题,并阐述了回溯算法中可能存在重复计算的问题,借助于对子问题的缓存,我们能有效避免重复计算。但是需要注意的是这种方法并不是总是有效。

与回溯算法类似,动态规划中,我们同样把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的状态集合并去重,然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,直至达到最终状态,并从中选择一个最优解。通过记录每个阶段的所有可达状态并去重来避免重复计算。

尽管动态规划的执行效率提高了,但是动态规划的空间复杂度也提高了,所以,很多时候,我们会说,动态规划是一种空间换时间的算法思想。

2.1 应用

2.1 动态规划解0-1背包问题

现在我们用动态规划来解决上一节的0-1背包问题,我们把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。

我们把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。我们可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过 w 个(w 表示背包的承载重量),也就是例子中的 9。于是,我们就成功避免了每层状态个数的指数级增长。

我们用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。n表示第n个物品,w+1 表示当前背包的重量。 /images/algo/dp/0_1.jpg /images/algo/dp/0_1_a.jpg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def rucksack_hold(items, weight):
    status = [[0] * (weight + 1) for i in range(len(items))]
    status[0][0] = 1
    status[0][items[0]] = 1
    for i in range(1, len(items)):
        for j in range(weight + 1):
            if status[i - 1][j]:
                status[i][j] = status[i - 1][j]
                if j + items[i] <= weight:
                    status[i][j + items[i]] = 1
    for l in status:
        print l

    # 判断可放置的最大重量
    j = weight
    n = len(items) - 1
    while j >= 0:
        if status[n][j]:
            break
    print j
    # 打印最大重量,放置的物品
    for i in range(n, 1, -1):
        if j - items[i] >= 0 and status[i - 1][j - items[i]]:
            print i, items[i]
            j -= items[i]


rucksack_hold([2, 2, 4, 6, 3], 9)

实际上我们可以有一个比上面空间复杂度更小的解法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def rucksack_hold_2(items, weight):
    status = [0] * (weight + 1)
    status[0] = 1
    status[items[0]] = 1
    print status
    for i in range(1, len(items)):
        for j in range(weight - items[i], -1, -1):
            if status[j]:
                status[j + items[i]] = 1
        print status


rucksack_hold_2([2, 2, 4, 6, 3], 9)

2.2 升级的 0-1 背包问题

这次我们引入物品价值,要求计算在满足背包最大重量限制的前提下,背包中可装入物品的最大总价值。

使用动态规划的求解过程与上面类似,只不过现在 status 数组记录的不再是0或1,而是当前状态对应的最大总价值。我们把每一层中 (i, cw) 重复的状态(节点)合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层的状态。如果用回溯算法,这个问题就没法再用“备忘录”解决了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def rucksack_hold_3(items, weight, values):
   status = [[None] * (weight + 1) for i in range(len(items))]
   status[0][0] = 0
   status[0][items[0]] = values[0]
   for i in range(1, len(items)):
       for j in range(weight + 1):
           if status[i - 1][j] >= 0:
               status[i][j] = status[i - 1][j]
               if j + items[i] <= weight:
                   v = status[i - 1][j] + values[i]
                   if status[i][j + items[i]] < v:
                       status[i][j + items[i]] = v
   for l in status:
       print l

print '------------------'
a = [3, 4, 8, 9, 6]
# a = [1, 1, 1, 1, 1]
rucksack_hold_3([2, 2, 4, 6, 3], 9, a)

3. 练习

3.1 练习一杨辉三角

我们对杨辉三角进行一些改造。每个位置的数字可以随意填写,经过某个数字只能到达下面一层相邻的两个数字。假设你站在第一层,往下移动,我们把移动到最底层所经过的所有数字之和,定义为路径的长度。请你编程求出从最高层移动到最底层的最短路径长度。

/images/algo/dp/yang.jpg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def path_pascal_triangle(pt):
    """
    :param pt:
    :return: 计算杨辉三角的最短路径
    """
    n = len(pt)
    status = []
    for i in range(0, n):
        s = [float('inf')] * (i + 1)
        row = pt[i]
        if i == 0:
            s[0] = row[0]
            s[-1] = row[-1]
        else:
            s[0] = row[0] + status[i - 1][0]
            s[-1] = row[-1] + status[i - 1][-1]
        status.append(s)
    print status

    for i in range(2, n):
        for j in range(1, i):
            left = j - 1
            right = j
            status[i][j] = min(status[i - 1][left], status[i-1][right]) + pt[i][j]
    print status
    return min(status[-1])

ss = [
    [3],
    [1, 2],
    [5, 6, 7],
    [1, 1, 1, 1]
]

print path_pascal_triangle(ss)

参考: