1. 程式人生 > 其它 >大前端演算法篇之揹包問題

大前端演算法篇之揹包問題

簡述:揹包問題是動態規劃演算法中的一個經典問題,分為01揹包和完全揹包,01揹包就是不能放入同一件物品,完全揹包是可以放入同一個物品

下面將要講的是01揹包問題

動態規劃中最重要的是先分析思路,然後總結出規律,最後得出一個公式

案例:假設有一個揹包,可以放入單位重量5的物品,然後我們有三個物品

物品編號/屬性 物品1 物品2 物品3
單位重量 1 3 4
價值 2 4 5

如果在不超過揹包最大承受重量的情況下如何放入物品,能夠使得揹包內物品的價值最大化。

對於這種問題,我們可以先填一個表格,我們用r(row)表示行,c(col)表示列,w[r]表示第r個物品的重量,c就是揹包的容量,v[r][c]表示前r個物品可以放入當前揹包重量c的最大價值,singleV(r) 代表第r個物品的價值

揹包容積(單位重量)/物品編號 0 1 2 3 4 5
沒有物品 0 0 0 0 0 0
物品1 0 2 2 2 2 2
物品2 0 2 2 4 6 6
物品3 0 2 2 4 6 7

對錶格的解讀:

  1. v[r][0] = 0 ;v[0][c] = 0;表示在揹包容量為0,或者沒有物品時可能放入的最大價值就是0
  2. 如果當前要放入的物品大於揹包的容量,那就直接取上一次揹包的最大價值,也就是 w[r] > c 那麼 v[r][c] = v[r-1][c]
  3. 如果當前要放入的物品小於等於揹包的容量,那就取 當前放入的價值加上剩餘空間可以放入的最大價值不放入當前物品時的價值 兩者之間取最大值,也就是 w[r] <= c 那麼 v[r][c] = max(v[r-1][c], singleV(r) + v[r-1][c-w[r])

我們可以隨便拿表格中的最右下角那個值來驗證一下,根據我們上面的推斷:

此時 r = 3, w[r] = 4 , c = 5 符號第3點規則 ,v[3][5] = max(v[3-1][5], singleV(3) + v[3-1][5-4]) = max(6,5 + 2) = 7 整合就是表格中的值

接下來用程式碼寫一下這個演算法

// 揹包問題演算法

function knapsackProblem() {
    let itemWeights = [1, 3, 4] // 物品的重量
    let itemValus = [2, 4, 5] // 物品的價值

    let weight = 5 // 揹包的容積
    let nums = itemWeights.length // 物品的個數

    // 代表放入最大價值的二維陣列
    let v = Array.from({ length: nums + 1 }).map(item => Array.from({ length: weight + 1 }))

    // 列印輸出一下上面的表格
    for (let r = 0; r < v.length; r++) {
        for (let c = 0; c < v[0].length; c++) {
            v[r][c] = 0
        }
    }
    // console.log(v)
    // 這時輸出的全是0 ,因為我們沒有判斷是否可以放入物品
    /**
     * [
        [ 0, 0, 0, 0, 0, 0 ],
        [ 0, 0, 0, 0, 0, 0 ],
        [ 0, 0, 0, 0, 0, 0 ],
        [ 0, 0, 0, 0, 0, 0 ]
        ]
     */

    // 接下來根據我們上面的規則放入物品,因為第一行和第一列全是0,所以我們行和列從1開始
    for (let r = 1; r < v.length; r++) {
        for (let c = 1; c < v[0].length; c++) {
            if (itemWeights[r - 1] > c) {
                v[r][c] = v[r - 1][c]
            } else {
                v[r][c] = Math.max(v[r - 1][c], itemValus[r - 1] + v[r - 1][c - itemWeights[r - 1]])
            }
        }
    }
    console.log(v)
    /**
     * 這裡我們就得到了上面的表格,這裡我們可以看出7就是能夠放入的最大價值,
     * 但此時我們還不知道里面放入了那些物品,所以我們還得記錄放入的物品
     * [
        [ 0, 0, 0, 0, 0, 0 ],
        [ 0, 2, 2, 2, 2, 2 ],
        [ 0, 2, 2, 4, 6, 6 ],
        [ 0, 2, 2, 4, 6, 7 ] 
        ]
     */

    // 用來記錄
    let records = Array.from({ length: nums + 1 }).map(item => Array.from({ length: weight + 1 }))

    for (let r = 1; r < v.length; r++) {
        for (let c = 1; c < v[0].length; c++) {
            if (itemWeights[r - 1] > c) {
                v[r][c] = v[r - 1][c]
            } else {
                // v[r][c] = Math.max(v[r - 1][c], itemValus[r - 1] + v[r - 1][c - itemWeights[r - 1]])
                if (v[r - 1][c] < itemValus[r - 1] + v[r - 1][c - itemWeights[r - 1]]) {
                    v[r][c] = itemValus[r - 1] + v[r - 1][c - itemWeights[r - 1]]
                    // 只在此記錄,因為這裡是已經把當前物品放入了
                    records[r][c] = true
                } else {
                    v[r][c] = v[r - 1][c]
                }
            }
        }
    }

    // for (let i = 0; i < records.length; i++) {
    //     for (let j = 0; j < records[0].length; j++) {
    //         if (records[i][j]) {
    //             console.log(`第${i}個物品放入揹包`)
    //         }
    //     }
    // }
    /**
     * 通過上面的列印我們還是不知道具體是哪些物品放入了揹包,
     * 我們可以從後往前遍歷
     * 第1個物品放入揹包
        第1個物品放入揹包
        第1個物品放入揹包
        第1個物品放入揹包
        第1個物品放入揹包
        第2個物品放入揹包
        第2個物品放入揹包
        第2個物品放入揹包
        第3個物品放入揹包
     */

    let i = records.length - 1
    let j = records[0].length - 1
    while (i > 0) {
        if (records[i][j]) {
            console.log(`第${i}個物品放入揹包`)
            j -= itemWeights[i - 1]
        }
        i--
    }
    /**
     * 這裡我們就知道哪些物品放入揹包了
     * 第3個物品放入揹包
        第1個物品放入揹包
     */
}

knapsackProblem()