动态规划之——01 背包和最短路径

1. 什么是动态规划

动态规划一般用来求解最优问题,这些问题一般都可以分为若干个决策阶段,每次决策对应着一组状态。我们要做的就是寻找出一组决策序列,这组决策序列能产生我们期望的最优解。

能用动态规划求解的问题有以下三个特征:

  • 最优子结构,当一个问题的优化解包含了子问题的优化解时,我们说这个问题具有最优子结构,也就是我们能通过前面阶段的状态得出后面阶段的状态。

  • 无后效性,一是表示前面状态不受后面状态的影响,二是表示后面的状态只和前面的状态值有关,而不管这个状态值是怎么得到的。

  • 重复子问题,在问题的求解过程中,会遇到重复的状态,很多子问题的解将被多次使用。

2. 0-1 背包问题

假设我们有 5 个物品,重量分别为 2,2,4,6,3,背包总容量为 9 Kg。我们来看动态规划是怎么解决这个问题的。

我们定义状态变量 \(state[i][j]\) 表示决策完 \(i+1\) 个物品放入背包的情况时,背包的重量是否可以为 \(j\),是为真反之为假。这样,我们在状态变量的最后一行找到一个最大的真值也就找到了问题的解。

针对本例,第一个物品的重量为 2,我们可以选择放入或者不放入, 所以有 \(state[0][0]=1,state[0][2]=1\);第二个物品的重量为 2,我们同样可以选择放入或者不放入, 注意此时背包的重量有 0 和 2 两种情况,所以决策完前两个物品放入背包的情况后我们就有 \(state[1][0]=1,state[1][2]=1,state[1][4]=1\);继续这个过程,决策完所有物品放入背包的情况,问题的答案也就找到了。

import numpy as np

# items = [10, 52, 31, 20, 35, 26, 15, 60]
# biggest_weight = 100
# n = 8

items = [2, 2, 4, 10, 4]
biggest_weight = 9
n = 5


def bag(items, n, biggest_weight):

    state = np.zeros((n, biggest_weight+1), 'uint8')
    state[0][0] = 1
    if items[0] <= biggest_weight:
        state[0][items[0]] = 1

    for i in range(1, n):
        for j in range(biggest_weight):
            if state[i-1][j] == 1:
                state[i][j] = 1
                if j + items[i] <= biggest_weight:
                    state[i][j + items[i]] = 1

    for j in range(biggest_weight, -1, -1):
        if state[n-1][j] == 1:
            return j


print(bag(items, n, biggest_weight))

这个算法的时间复杂度为 \(O(n*w)\),n 为物品个数,w 为背包承重量,相比于回溯 \(O(2^n)\) 指数级的复杂度低了很多。但是,我们需要申请一个大小为 n*(w+1) 的数组,对空间消耗比较多,实际上对于这个问题,我们只需要申请一个大小为 w+1 的数组即可。因为每次我们更新完状态的时候,前面的状态也就没用了,我们可以共用一个状态数组。

def bag(items, n, biggest_weight):

    state = [0] * (biggest_weight + 1)
    state[0] = 1
    if items[0] <= biggest_weight:
        state[items[0]] = 1

    for i in range(1, n):
        # 注意这里要倒序遍历
        for j in range(biggest_weight, -1, -1):
            if state[j] == 1:
                if j + items[i] <= biggest_weight:
                    state[j + items[i]] = 1

    for j in range(biggest_weight, -1, -1):
        if state[j] == 1:
            return j

3. 最短路径

假设我们有一个 n*n 的矩阵,矩阵中每个元素都是正数。现在我们要从左上角走到右下角,每次只能选择向下走或者向右走,怎么走才能使得最后经过的路径上的数字之和最小?

image.png

我们定义状态 \(dist[i][j]\) 为走到位置 \((i, j)\) 所需的最小路径,由于我们只能从 \((i, j)\) 的左边位置 \((i, j-1)\) 或者上边位置 \((i-1, j)\) 走过来,所以可以得到:

\[dist[i][j] = min(dist[i][j-1], dist[i-1][j]) + data[i][j]\]

import numpy as np

data = np.array([[1, 3, 5, 9], [2, 1, 3, 4], [5, 2, 6, 7], [6, 8, 4, 3]])


def shortest_path(data):

    n = data.shape[0]
    dist = np.zeros((n, n))
    sum = 0

    # 第一行只能从左边走过来
    for i in range(n):
        sum += data[0][i]
        dist[0][i] = sum

    sum = 0
    # 第一列只能从上边走过来
    for i in range(n):
        sum += data[i][0]
        dist[i][0] = sum

    for i in range(1, n):
        for j in range(1, n):
            dist[i][j] = min(dist[i-1, j], dist[i, j-1]) + data[i][j]

    print(dist)
    return dist[n-1][n-1]


print(shortest_path(data))

获取更多精彩,请关注「seniusen」!

猜你喜欢

转载自www.cnblogs.com/seniusen/p/11991041.html