天天看點

常用算法解析--動态規劃

    動态規劃算法通常用于求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應于一個值,我們希望找到具有最優值的解。

    能采用動态規劃求解的問題的一般要具有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])

           

    代碼還有很大的優化空間,後續會繼續優化,另外我将結合一個實際工程案例,更深入的了解動态規劃,敬請期待!