天天看點

2021寒假每日一題《01背包問題》

01背包問題

題目來源:背包九講

時間限制:1000ms 記憶體限制:64mb

題目描述

有 \(N\) 件物品和一個容量是 \(V\) 的背包。每件物品 隻能使用一次 。

第 \(i\) 件物品的體積是 \(v_i\),價值是 \(w_i\)。

求解将哪些物品裝入背包,可使這些物品的總體積不超過背包容量,且總價值最大。

輸出最大價值。

輸入格式

第一行兩個整數,\(N\),\(V\),用空格隔開,分别表示物品數量和背包容積。

接下來有 \(N\) 行,每行兩個整數 \(v_i\),\(w_i\),用空格隔開,分别表示第 \(i\) 件物品的體積和價值。

輸出格式

輸出一個整數,表示最大價值。

資料範圍

0 < \(N\),\(V\) ≤ 1000

0 < \(v_i\),\(w_i\) ≤ 1000

樣例輸入

4 5
1 2
2 4
3 4
4 5
           

樣例輸出

8
           

解題思路1:暴力破解

嘗試各種可能的商品組合,并找出價值最高的組合。

使用 \(N\) 位二進制字串表示物品是否放入背包,枚舉所有的可能,然後算出每種可能的價值,取其最大值輸出。

解法不足:速度非常慢。在隻有3件商品的情況下,你需要計算8個不同的集合;當有4件商品的時候,你需要計算16個不同的集合。每增加一件商品,需要計算的集合數都将翻倍。

對于每一件商品,都有選或不選兩種可能,即這種算法的運作時間是 \(O(2^n)\) 。

IO競賽(例如:藍橋杯)當中,如果實在想不起來動态規劃,可以使用這個方法拿到一部分分數,但是在ACM競賽當中,就不能使用這種方法了。

解題代碼1-Java

import java.util.*;

public class Main {
    static int getSum(int n, int v, StringBuilder binString, int[][] items) {
        int sumV = 0, sumN = 0;
        for (int j = 0; j < n; j++) {
            if (binString.charAt(j) == \'1\') {
                sumV += items[j][0];
                sumN += items[j][1];
            }
            if (sumV > v) {
                return -1;
            }
        }
        return sumN;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int v = input.nextInt();
        int totalV = 0, totalN = 0;
        int[][] items = new int[n][2];
        for (int i = 0; i < n; i++) {
            items[i][0] = input.nextInt();
            items[i][1] = input.nextInt();
            totalV += items[i][0];
            totalN += items[i][1];
        }
        input.close();
        if (totalV <= v) {
            System.out.println(totalN);
            return;
        }
        int sumN = 0;
        for (long i = 0; i < Math.pow(2, n); i++) {
            StringBuilder binString = new StringBuilder(Long.toBinaryString(i));
            long len = binString.length();
            while (len < n) {
                binString.insert(0, "0");
                len++;
            }
            sumN = Math.max(sumN, getSum(n, v, binString, items));
        }
        System.out.println(sumN);
    }
}
           

解題思路2:動态規劃

對于動态規劃算法,可以去這篇文章裡學習:經典中的經典算法:動态規劃(詳細解釋,從入門到實踐,逐漸講解)

每個動态規劃都從一個網格開始。

在本題中,網格的各行表示商品,各列代表不同容量的背包(從1到V)。

網格最初是空的。你将填充其中的每個格子,網格填滿後,就找到了問題的答案!

比如本題樣例的網格如下圖:

2021寒假每日一題《01背包問題》

一、畫好網格之後,先來看第一行。

在每個一格子你都要做出一個選擇:放不放進這一行對應的物品。

物品1所占空間為 \(1\) ,也就是說物品1能放進容量為 \(1\) 的這個背包,是以這個單元格包含物品1。是以往第一行第一列中裝入物品1,價值為2。

2021寒假每日一題《01背包問題》

這行的其他單元格也一樣。别忘了,這是第一行,隻有一個物品可供你選擇,換而言之,你假裝現在還沒有打算放進其他物品。

填充完之後如下圖:

2021寒假每日一題《01背包問題》

此時你很可能心存疑惑:原題說的是容量為5的背包,我們為何要考慮容量為1、2、3、4的背包呢?動态規劃從子問題着手,逐漸解決大問題。這裡解決的子問題将幫助你解決大問題。

這行表示的是目前的最大價值,并不是最終解。随着算法往下執行,将逐漸修改最大價值。

二、接下來看第二行。

在第二行中,你有兩種物品選擇。先看第一個單元格,容量為1,之前裝進去的最大的價值為2。

這一個單元格該不該放入第二個物品呢?答案顯然是不行,因為容量為1的口袋無法裝下占空間2的物品。

是以第一列的最大價值保持不變。

2021寒假每日一題《01背包問題》

接下來看第二行第二列,這個格子所對應背包的容量為2,現在能夠裝下第二個物品了,對比一下價值,比之前決定的物品1價值高,是以将原來的物品1換為物品2。

再看後面的第三列,這個格子容量為3,可以同時裝下物品1和物品2,是以都放進去。

之後的列也進行一樣的操作,最後操作完第二行的結果為:

2021寒假每日一題《01背包問題》

三、做完第二行,接下來看第三行

以同樣的方式處理物品3,物品3占用空間3,價值為4。

其中在第五列時,容量為5,原本決定的為(物品1+物品2),現在有物品3可以選擇了,是以考慮将物品1換為價值更高的物品3,是以此行第五列結果為8

做完這一步就得到了下面的網格:

2021寒假每日一題《01背包問題》

四、第四行也一樣

2021寒假每日一題《01背包問題》

五、最終結果

可以總結得到一個公式:\(

cell[i][j](i和j代指行和列) = 兩者中較大的一項

\begin{cases}

1.上一個單元格的值(即:cell[i-1][j]的值) \\

2.目前物品的價值+剩餘空間的價值(即:cell[i-1][j-目前物品的占用空間] + 目前物品的價值)

\end{cases}

\)

你可以使用這個公式來計算每個單元格的價值,最終的網格将與前一個網格相同。

這裡回答前面抛出的問題:為什麼要求解子問題?——因為你可以合并兩個子問題的解來得到更大問題的解。

相信看到這裡,并且親手推導過網格,應該對動态規劃的狀态轉移方程背後的邏輯有了更深的了解。現在,再回頭看01背包問題的經典描述,并實作代碼。

六、程式實作

聲明一個數組dp[n+1][v+1]表示初始網格,首行為0,表示不放入任何物品,同時也為了代碼閱讀性,從下标1開始處理。

2021寒假每日一題《01背包問題》

根據第五步的公式,對于編号為 \(i\) 的物品:

  • 如果将\(i\)放入,\(目前背包的最大價值 = 第i号物品的價值 + 出去i号物品占用空間後剩餘的空間所能存放的最大價值\)。

    即:\(valueWith\_i = w_i + dp[i-1][j-v_i];\)

  • 如果不放入\(i\),\(目前背包的價值 = 前i-1個物品存放在背包中的最大價值\)。

    即:\(valueWithout\_i = dp[i-1][j];\)

  • 最終,\(dp[i][j]\)的結果取兩者的較大值。

    即:\(dp[i][j] = Math.max(valueWith\_i, valueWithout\_i);\)

解題代碼2-Java

import java.util.*;

public class Main {
    static int maxValue(int n, int v, int[][] items) {
        if (n == 0) {
            return 0;
        }
        int[][] dp = new int[n + 1][v + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= v; j++) {
                int valueWith_i = (j - items[i - 1][0] >= 0) ? (items[i - 1][1] + dp[i - 1][j - items[i - 1][0]]) : 0;
                int valueWithout_i = dp[i - 1][j];
                dp[i][j] = Math.max(valueWith_i, valueWithout_i);
            }
        }
        return dp[n][v];
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int v = input.nextInt();
        int totalV = 0, totalN = 0;
        int[][] items = new int[n][2];
        for (int i = 0; i < n; i++) {
            items[i][0] = input.nextInt();
            items[i][1] = input.nextInt();
            totalV += items[i][0];
            totalN += items[i][1];
        }
        input.close();
        if (totalV <= v) {
            System.out.println(totalN);
            return;
        }
        System.out.println(maxValue(n, v, items));
    }
}
           

解題代碼3-Java:對于解題代碼2的優化

import java.util.*;

public class Main {
    public static int N = 1010;

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        int v = input.nextInt();
        int[] dp = new int[N];
        for (int i = 1; i <= n; i++) {
            int vi = input.nextInt();
            int wi = input.nextInt();
            for (int j = v; j >= vi; j--) {
                dp[j] = Math.max(dp[j], dp[j - vi] + wi);
            }
        }
        System.out.println(dp[v]);
    }
}