天天看点

常用算法解析--动态规划

    动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。

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

  1. 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理
  2. 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
  3. 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

    动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。

    动态规划一般都是使用表格法进行求解。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

    这里我已背包问题为例详细介绍如何利用动态规划解决实际问题:

0/1背包问题

    假设有一个小偷,有一个承重容量为35kg的背包,可以盗窃的物品有三种,每种只有一个,包括:A(30kg,价值3000元)、B(20kg,价值2000元)、C(15kg,价值1500元)。怎么选择才能使装入背包的物品价值最高?

    接下来我用表格演示动态规划过程,如下图:

  1. 第一行,考虑物品A。容量为15、20,25时,都无法装入背包,此时最大价值为0;当容量为30、35时可以装入,所以此时最大价值为3000元;
  2. 第二行,考虑物品A、B。容量为15时,都无法装入背包,此时最大价值为0;容量为20、25时,只能把物品B装入背包,此时价值最大为2000元;容量为30时,A与B都可以装入,但是装入A时价值更大,此时最大价值为3000元;容量为35时,A与B都可以装入,但是装入A时价值更大,此时最大价值为3000元;
  3. 第三行,考虑物品A、B、C。容量为15时,只能放入C,此时最大价值为1500元;容量为20时,B与C都可以放入,但是装入B时价值更大,此时最大价值为2000元;容量为25时,B与C都可以放入,但是装入B时价值更大,此时最大价值为2000元;容量为30时,A、B与C都可以放入,但是装入A时价值更大,此时最大价值为3000元;容量为35时,A、B与C都可以放入,但是同时装入B和C时价值更大,此时最大价值为3500元;
常用算法解析--动态规划

    通过以上三步,我们就得到最大的方案,也就是把B和C同时放入背包时价值最大,最大为3500元。在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:

当 前 最 大 价 值 = max ⁡ { 当 前 容 量 对 应 的 上 一 行 单 元 格 的 价 值 当 前 物 品 的 价 值 + 剩 余 容 量 对 应 的 上 一 行 单 元 格 对 应 的 价 值 当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases} 当前最大价值=max{当前容量对应的上一行单元格的价值当前物品的价值+剩余容量对应的上一行单元格对应的价值​

    这里我们用数学公式来表达,方便程序编写。用 g i g_i gi​表示物品种类,用 w i w_i wi​表示物品对应的重量,用 v i v_i vi​表示物品对应的价值。用 V V V表示当前表格,维度为 ( M (M (Mx N ) N) N),M为物品的种类,N为容量划分数量(可以根据自己需要划分), V m a x V_{max} Vmax​表示当前的最大值, G G G表示背包容量,则用公式表示为:

V [ m ] [ n ] = max ⁡ ( V [ m − 1 ] [ n ] ,    v [ m ] + V [ m − 1 ] [ G − g [ m ] ] ) V[m][n]=\max(V[m-1][n],\space\space v[m]+V[m-1][G-g[m]]) V[m][n]=max(V[m−1][n],  v[m]+V[m−1][G−g[m]])

    代码实现如下:

def package_1(weights, values, capacities):
    """
    该函数是利用动态规划解决0/1背包问题
    :param weights: 每个物品的重量
    :param values: 每个物品对应的价值
    :param capacities: 预先设定的容量变化列表
    :return: 
    """
    table_value = [[ 0 for _ in range(len(capacities))] for _ in range(len(weights))]
    # 填充表格第一行
    for k in range(len(capacities)):
        if weights[0] <= capacities[k]:
            table_value[0][k] = values[0]
    for i in range(1, len(weights)):
        for j in range(len(capacities)):
            if weights[i] < capacities[j]:
                continue
            # 假定放入第i个物品后,剩余容量对应的索引
            ind = capacities.index(capacities[-1] - weights[i])
            table_value[i][j] = max(table_value[i-1][j], values[i]+table_value[i-1][ind])

           
完全背包问题

    假设有一个小偷,有一个承重容量为10kg的背包,可以盗窃的物品有三种,每种都不限数量,包括:A(3kg,价值30元)、B(2kg,价值20元)、C(1kg,价值15元)。怎么选择才能使装入背包的物品价值最高?

    接下来我用表格演示动态规划过程,如下图:

填写步骤和0/1背包问题相似,唯一不同的地方就是可以选择不同数量的值,也就是考虑0-n个某种物品的重量,具体表格步骤就不想说了,情况太多,不好描述,直接看表吧(注:这里的最优以第一次出现的最优为准)。

常用算法解析--动态规划

    在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:

当 前 最 大 价 值 = max ⁡ { 当 前 容 量 对 应 的 上 一 行 单 元 格 的 价 值 当 前 k 个 物 品 的 价 值 + 剩 余 容 量 对 应 的 上 一 行 单 元 格 对 应 的 价 值 当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前k个物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases} 当前最大价值=max{当前容量对应的上一行单元格的价值当前k个物品的价值+剩余容量对应的上一行单元格对应的价值​

    这里我们用数学公式来表达,方便程序编写。用 g i g_i gi​表示物品种类,用 w i w_i wi​表示物品对应的重量,用 v i v_i vi​表示物品对应的价值。用 V V V表示当前表格,维度为 ( M (M (Mx N ) N) N),M为物品的种类,N为容量划分数量(可以根据自己需要划分), V m a x V_{max} Vmax​表示当前的最大值, G G G表示背包容量, k k k表示当前物品选取的数量,则用公式表示为:

V [ m ] [ n ] = max ⁡ ( V [ m − 1 ] [ n ] ,    v [ m ] ∗ k + V [ m − 1 ] [ G − g [ m ] ∗ k ] ) V[m][n]=\max(V[m-1][n],\space\space v[m]*k+V[m-1][G-g[m]*k]) V[m][n]=max(V[m−1][n],  v[m]∗k+V[m−1][G−g[m]∗k])

    代码实现如下:

def package_2(weights, values, capacities):
    """
    该函数是利用动态规划解决0/1背包问题
    :param weights: 每个物品的重量
    :param values: 每个物品对应的价值
    :param capacities: 预先设定的容量变化列表
    :return:
    """
    table_value = [[0 for _ in range(len(capacities))] for _ in range(len(weights))]
    # 填充表格第一行
    for k in range(len(capacities)):
        num = capacities[k] // weights[k]
        table_value[0][k] = num * values[0]
    for i in range(1, len(weights)):
        for n in range(1000):
            # 提前终止
            if n * weights[i] > capacities[-1]:
                break
            for j in range(len(capacities)):
                if weights[i]*n < capacities[j]:
                    continue
                # 假定放入第i个物品后,剩余容量对应的索引
                ind = capacities.index(capacities[-1] - weights[i]*n)
                table_value[i][j] = max(table_value[i - 1][j], values[i] + table_value[i - 1][ind])

           
多重背包问题

    假设有一个小偷,有一个承重容量为10kg的背包,可以盗窃的物品有三种,每种物品数量都有限制,包括:A(3kg,价值30元,2个)、B(2kg,价值20元,3个)、C(1kg,价值15元,3个)。怎么选择才能使装入背包的物品价值最高?

    接下来我用表格演示动态规划过程,如下图:

填写步骤和完全背包问题相似,唯一不同的地方就是物品的数量有限,也就是考虑0-所有个某种物品的重量,具体表格步骤就不想说了,情况太多,不好描述,直接看表吧(注:这里的最优以第一次出现的最优为准)。

常用算法解析--动态规划

    在表格推理过程中我们隐藏了一个细节,在计算每个单元格的 价值时,都会遵循一下公式:

当 前 最 大 价 值 = max ⁡ { 当 前 容 量 对 应 的 上 一 行 单 元 格 的 价 值 当 前 k 个 物 品 的 价 值 + 剩 余 容 量 对 应 的 上 一 行 单 元 格 对 应 的 价 值 当前最大价值= \max \begin{cases} 当前容量对应的上一行单元格的价值\\ 当前k个物品的价值 + 剩余容量对应的上一行单元格对应的价值 {} \end{cases} 当前最大价值=max{当前容量对应的上一行单元格的价值当前k个物品的价值+剩余容量对应的上一行单元格对应的价值​

    这里我们用数学公式来表达,方便程序编写。用 g i g_i gi​表示物品种类,用 w i w_i wi​表示物品对应的重量,用 v i v_i vi​表示物品对应的价值,用 s i s_i si​表示物品对应的最大数量。用 V V V表示当前表格,维度为 ( M (M (Mx N ) N) N),M为物品的种类,N为容量划分数量(可以根据自己需要划分), V m a x V_{max} Vmax​表示当前的最大值, G G G表示背包容量, k k k( k < s i k<s_i k<si​)表示当前物品选取的数量,则用公式表示为:

V [ m ] [ n ] = max ⁡ ( V [ m − 1 ] [ n ] ,    v [ m ] ∗ k + V [ m − 1 ] [ G − g [ m ] ∗ k ] )      其 中 : k < s [ m ] V[m][n]=\max(V[m-1][n],\space\space v[m]*k+V[m-1][G-g[m]*k]) \space\space\space\space 其中:k < s[m] V[m][n]=max(V[m−1][n],  v[m]∗k+V[m−1][G−g[m]∗k])    其中:k<s[m]

    代码实现如下:

def package_3(weights, values, limits, capacities):
    """
    该函数是利用动态规划解决0/1背包问题
    :param weights: 每个物品的重量
    :param values: 每个物品对应的价值
    :param limits: 每个物品对应的数量
    :param capacities: 预先设定的容量变化列表
    :return:
    """
    table_value = [[0 for _ in range(len(capacities))] for _ in range(len(weights))]
    # 填充表格第一行
    for k in range(len(capacities)):
        num = capacities[k] // weights[k]
        if num > limits[0]:
            if k != 0:
                table_value[0][k] = table_value[0][k-1]
        else:
            table_value[0][k] = num * values[0]
    for i in range(1, len(weights)):
        for n in range(limits[i]+1):
            # 提前终止
            if n * weights[i] > capacities[-1]:
                break
            for j in range(len(capacities)):
                if weights[i]*n < capacities[j]:
                    continue
                # 假定放入第i个物品后,剩余容量对应的索引
                ind = capacities.index(capacities[-1] - weights[i]*n)
                table_value[i][j] = max(table_value[i - 1][j], values[i] + table_value[i - 1][ind])

           

    代码还有很大的优化空间,后续会继续优化,另外我将结合一个实际工程案例,更深入的理解动态规划,敬请期待!