前端與演算法-動態規劃之01揹包問題淺析與實現

小黎也發表於2018-07-29

去年因為工作中的某個應用場景,需要使用到動態規劃,為此花了些時間啃了啃揹包演算法

為啥去年的東西,今年才寫粗來,也許大概是懶吧

動態規劃 Dynamic Programming

先簡單說下什麼是動態規劃

引用維基百科的一句話 通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法,通俗的理解就是,將一個複雜的問題拆解成相似的多個子問題,然後依次解決各個子問題,同時因為子問題的相似性,在計算過程中,需要同時將子問題的結果儲存起來,在下一個同樣的子問題時,直接查詢即可,這樣在同樣的問題上就不會再次花費計算時間,其核心思想就是拆解與記錄

那麼什麼情況下可以使用動態規劃呢 ? 需具備以下特徵

  1. 有最優子結構,如果一個問題最優的解決方案可以通過最優它的子問題的解決方案來獲取,那麼這個問題就有最優子結構的屬性,簡單的理解就是用子問題的最優解來構造原問題的最優解

  2. 子問題無後效性,即子問題的解一旦算出,就不會受到後續的子問題的影響而發生改變

  3. 子問題重疊性質,即每次產生的子問題都不是一個全新的問題,是在前一個子問題的基礎上變化而來的,所以子問題之間需要具有重疊性,這也是動態規劃高效率的一個原因

總結如下:

  • 有最優解的結構
  • 找到子問題
  • 以“自底向上”的方式計算最優解的值
  • 可以從已計算的資訊中構建出最優解的路徑

揹包問題

先看一個場景:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內,我們如何選擇,才能使得物品的總價格最高?

那麼為解決這一類問題的演算法就稱為揹包演算法。

適用於揹包問題的場景需要具備以下特徵

  • 有一個固定容量的揹包
  • 商品有兩個屬性,體積和價值
  • 求揹包能容納下的最大價值或放入的具體商品

揹包問題與動態規劃

我們可以將揹包的容量(v)拆解,分為0到v不同的揹包,那麼子問題就是,容量為(0-v)的揹包,我們要算出對應的各個揹包所能容納下的最大價值,聰明的小夥伴這時候一定想起了上面我們介紹的動態規劃,沒錯!揹包問題的最優解法就是動態規劃,它完全符合動態規劃的特徵。

揹包演算法分類

揹包演算法分類主要分為三種,其他的場景是在這三種的基礎上混合和擴充套件

  1. 01揹包,每種物品僅有一件,可以選擇放或不放
  2. 完全揹包,每種物品有無限件,每種物品可以取n件
  3. 多重揹包,每件物品數量都是有限的

目前只瞭解01揹包演算法,下面就只針對01揹包演算法做介紹

01揹包

01揹包:有N件物品和一個容量為V的揹包。每種物品均只有一件,可以選擇放或不放,第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使價值總和最大。

01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的型別的揹包問題往往也可以轉換成01揹包問題求解

演算法的核心是狀態轉移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

至於這個方程怎麼得來的,暫時超粗我的能力範圍啦~歡迎懂得小夥伴教教

解釋一下上面的方程:f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值,即算出子問題f[i][v]的解。

舉個栗子,有編號分別為a,b,c,d,e的五件物品,它們的重量分別是3,6,3,8,6,它們的價值分別是4,6,6,12,10現在給你個承重為10的揹包,如何讓揹包裡裝入的物品具有最大的價值

根據狀態轉移方程,我們先弄個表格,用於觀察子問題的解

name weight value 1 2 3 4 5 6 7 8 9 10
a 3 4
b 6 6
c 3 6
d 8 12
e 6 10

第一步,f[a][1],a物品放入容量為1的揹包中,求其最大價值,為方便描述就簡稱 a1單元格,很顯然放不下,那麼解為0

再算 a2單元格,根據方程 f[a][2]=max{f[a-1][2],f[a-1][2-c[a]]+w[a]} = max{0,0} 可得 a2 = 0

再算 a3, 根據方程 f[a][3]=max{f[a-1][3],f[a-1][3-c[a]]+w[a]} = max{0,0+4}可得 a3 = 4

依次算出 a行的值,接下來到b行的

f[b][1]=max{f[b-1][1],f[b-1][1-c[b]]+w[b]} = max{0,0+} 可得 b1 = 0

f[b][3]=max{f[b-1][3],f[b-1][3-c[b]]+w[b]} = max{4,0} 可得 b1 = 4

f[b][6]=max{f[b-1][6],f[b-1][6-c[b]]+w[b]} =max{f[a][6],f[a][6-6]+6} = max{4,0+6} 可得 b1 = 6

f[b][9]=max{f[b-1][9],f[b-1][9-c[b]]+w[b]} =max{f[a][9],f[a][9-6]+6} = max{4,4+6} 可得 b1 = 10

依次類推,直到把表填滿

name weight value 1 2 3 4 5 6 7 8 9 10
a 3 4 0 0 4 4 4 4 4 4 4 4
b 6 6 0 0 4 4 4 6 6 6 10 10
c 3 6 0 0 6 6 6 10 10 10 12 12
d 8 12 0 0 6 6 6 10 10 12 12 12
e 6 10 0 0 6 6 6 10 10 12 16 16

那麼可以知道容量為10的揹包,最大的價值為16,那麼對應的是那些物品呢?可通過回溯法,找回對應的商品,從表格右下角開始找起

例項講解-紅包組合

場景

最優紅包組合:使用者在下單結算的時候,在使用者的n多個紅包中,為使用者算出最優的紅包組合,合併給使用者一起使用,紅包有兩個要素:紅包金額、紅包最低使用金額,且組合的紅包中,紅包最低使用金額的總和不能大於訂單金額,那麼哪幾個紅包組合才是最大的優惠呢?該問題簡單一句話概括就是:

根據下單金額 W 從 N 個紅包中選出一個或多個紅包,算出當前下單金額下最優的紅包組合

問題分析

這是一個揹包問題,滿足01揹包的特徵,將下單金額看成是揹包容量,每個紅包是一個物品,物品的價值是紅包的金額,物品的重量是紅包的最低適用金額,子問題就是,求金額(0-w)下適用的最優紅包組合

紅包物件

{
    "amount": "39.00", // 紅包金額-商品的價值
    "rangeBegin": "6000.00", // 紅包最低適用金額-商品的重量
}
複製程式碼

具體程式碼實現如下

/**
* @description
* 01揹包演算法
* @private
* @param {any} dataList 紅包列表 
* @param {any} all 下單金額
* @returns 
*/
private knapsack(dataList, all) {
    const returnList = [];
    for (let i = 0; i < dataList.length; i++) {
        // 構建二維陣列
        returnList[i] = [];
        for (let j = 0; j < all; j++) { // 分割揹包
            const currentBagWeight = j + 1; // 此時揹包重量
            const currentWeight = dataList[i].rangeBegin; // 此時物品重量
            const currentValue = dataList[i].amount; // 此時的價值
            const lastW = currentBagWeight - currentWeight; // 此時揹包重量減去此時要新增的物品後的重量

            // 求子問題最優解,並記錄
            let fV = lastW >= 0 ? currentValue : 0;
            fV = fV + (i > 0 && returnList[i - 1][lastW - 1] ? returnList[i - 1][lastW - 1] : 0);
            const nV = i > 0 && returnList[i - 1][j] ? returnList[i - 1][j] : 0;
            returnList[i][j] = Math.max(fV, nV);
        }
    }

    // 回溯演算法,算出選擇的商品
    let y = all - 1;
    const selectItem = [];
    let i = dataList.length - 1;

    while (i > -1) {
        if (returnList[i][y] === (returnList[i - 1] && returnList[i - 1][y - dataList[i].rangeBegin] || 0) + dataList[i].amount) {
            selectItem.push(dataList[i]);
            y -= dataList[i].rangeBegin;
        }
        i--;
    }

    return selectItem;
}
複製程式碼

小結

誰說前端狗就不用懂演算法的~ 哈哈~ 不要給自己設限~

參考文章 揹包問題九講 動態規劃之01揹包問題

相關文章