背包九講(Java) 清晰註釋版

背包問題

如果求組合數就是外層for循環遍歷物品,內層for遍歷背包體積重量等限制

如果求排列數就是外層for遍歷背包體積重量等限制,內層for循環遍歷物品

參考影片:

  1. https://www.bilibili.com/video/BV1qt411Z7nE?p=1
  2. ttps://www.bilibili.com/video/BV1qt411Z7nE?p=2
  3. https://www.bilibili.com/video/BV1Qt411R7v8?p=2

0/1 背包問題

題目:https://www.acwing.com/problem/content/2/

在这里插入图片描述

每種物品只有一樣,每種物品只有取和不取兩種選擇

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 個物品中,背包容量為 j j j 時的最大價值( 0 ≤ i ≤ N 0 \leq i \leq N 0iN, 0 ≤ j ≤ V 0 \leq j \leq V 0jV)

0/1 背包的狀態轉移方程可以表示為:

d p [ i ] [ j ] = { 0  如果  i = 0  或  j = 0 d p [ i − 1 ] [ j ]  如果  i > 0  且  j < v i max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v i ] + w i )  如果  i > 0  且  j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i-1][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]= 0dp[i1][j]max(dp[i1][j],dp[i1][jvi]+wi) 如果 i=0  j=0 如果 i>0  j<vi 如果 i>0  jvi

  • i i i 等於 0 0 0 j j j 等於 0 0 0 時,表示沒有物品可選或背包容量為 0 0 0,此時最大價值為 0 0 0
  • i i i 大於 0 0 0 j j j 小於 v i v_i vi 時,背包容量不足以放入第 i i i 個物品,所以最大價值與前 i − 1 i-1 i1 個物品時的最大價值相同
  • i i i 大於 0 0 0 j j j 大於等於第 i i i 個物品的體積 v i v_i vi 時,對於當前的物品 i i i,有放入和不放入背包兩種選擇。我們考慮的是 d p [ i − 1 ] [ j − v i ] dp[\textcolor{orange}{i - 1}][j-v_i] dp[i1][jvi] 的最大價值加上當前物品的價值 w i w_i wi,以及 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]。取兩者中的較大值作為 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值

以下代碼:

// 二維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(N * V)
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N, V;
        
        N = scanner.nextInt();
        V = scanner.nextInt();
        
        int[] volume = new int[N + 1];
        int[] worth = new int[N + 1];
        int[][] dp = new int[N + 1][V + 1];
        
        for(int i = 1; i <= N; ++i) {
            volume[i] = scanner.nextInt();
            worth[i] = scanner.nextInt();
        }
        
        scanner.close();
        
        for(int i = 1; i <= N; ++i) {
            // 必須從 0 ~ V,且在迴圈內在進行判斷
            for(int j = 0; j <= V; ++j) {
                // 在迴圈內才判斷是為了將 dp[i][j] 的內容有延續(就算下方條件不成立,這裡還是必須延續,這樣下一輪如果需要更新最大值才不會錯;下一輪如果不需更新最大值也要繼續延續)
                dp[i][j] = dp[i - 1][j];
                // 在此判斷,避免 out of bounds
                if(j - volume[i] >= 0)
                    // 找出最大值存入
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - volume[i]] + worth[i]);
            }
        }
        
        // 為什麼可以直接返回最後一個值
        // 首先是因為這是最後一個比較最大值的,上面註解有寫說這是一路延續下來的,所以 dp[N][V] 肯定和 dp[N - 1][V] 比較過大小,同理 dp[N - 1][V] 和 dp[N - 2][V] 比較過大小,所以無需再找 dp[0~N][V] 的最大值,因為上面的巢狀迴圈已處理過,所以保證最後一個一定是最大;再者 dp[N][V] 是最大體積的,也是最後一個在巢狀迴圈中比較的數字,所以一路比下來我們可以說它是最大的。
        System.out.println(dp[N][V]);
    }
}
// 降維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(V)
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N, V;
        
        N = scanner.nextInt();
        V = scanner.nextInt();
        
        int[] volume = new int[N + 1];
        int[] worth = new int[N + 1];
        int[] dp = new int[V + 1];
        
        for(int i = 1; i <= N; ++i) {
            volume[i] = scanner.nextInt();
            worth[i] = scanner.nextInt();
        }
        
        scanner.close();
        
        // 這裡能降維的關鍵是在只有 2 個 row 的陣列在跑
        for(int i = 1; i <= N; ++i) {
            // 這裡 j 必須要倒序的主要原因是因為 dp[j - volume[i]],實際我們要的是上一輪的內容(dp[i - 1][j - volume[i]]),但這裡降維了,所以這裡的 j 要使用倒序來避免被此輪的更新染污 dp[j - volume[i]]
            // 至於右側比較大小的 dp[j] 也是上一輪的拿來比較後才進行更新,所以 dp[j] 在這裡的正序倒序皆不影響,只是因為 dp[j - volume[i]] 所以要倒序

            // V ~ volume[i],這裡也與二維的不同,二維是必須完整 run 1 ~ V,之後才在迴圈裡判斷;這裡則是因為 dp[j] 已經承襲了上一輪的 dp[j](這也是降維的特點,自動延續),所以可以直接在迴圈中排除不可能(造成陣列 out of bound)的情況
            for(int j = V; j >= volume[i]; --j) {
                    dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
            }
        }
        
        // 為什麼這裡可以直接返回最後一個值呢,其實和二維的思路差不多,但這裡可能有些人會有疑問,我的 dp[V] 在上方的巢狀迴圈是最後一個 row 的第一個數字,難道說最後一個 row 的第一個比完之後,後面比的都沒用嗎?感覺就說不上來的怪啊?
        // 但其實這是沒有問題的,你再仔細觀察我們二維的狀態轉移式,再看一維就沒問題,二維時我們的 dp[i][j] 都是從 dp[i - 1][...] 而來,也就是說我們當前 row 的所有值全部都取決於上一輪而非當前輪
        // 在最大體積時的值一定會是當前輪最大的(可以體會一下這句話),這是動態規劃保證的特性(在二維時有些人會比較 dp[0~N][V] 的最大值時,其實也用到了這個特性,因為已經確保了最大體積能在當前輪求得最大值,所以這樣去比較求得總體最大值的思路沒錯,但我已經在二維時說了,不用這樣去比較,直接返回最後一個值即可)。
        // 融合以下兩個特性,當前輪都是上一輪而來 + 最大體積時一定會是當前輪最大的,那麼"最後一個 row 的第一個數字"是最大值,也是應當返回的值,就相當合理了
        System.out.println(dp[V]);
    }
}

完全背包問題

題目:https://www.acwing.com/problem/content/description/3/

在这里插入图片描述

每種物品有無限多種,在有限體積中求最大價值

{ d p [ i , j ] = max ⁡ (   d p [ i − 1 ,   j ] ,   d p [ i − 1 ,   j − v ] + w ,   d p [ i − 1 ,   j − 2 v ] + 2 w ,   d p [ i − 1 ,   j − 3 v ] + 3 w ,   … ) d p [ i ,   j − v ] = max ⁡ (     d p [ i − 1 ,   j − v ] ,   d p [ i − 1 ,   j − 2 v ] +    w ,   d p [ i − 1 ,   j − 3 v ] + 2 w ,   … ) \begin{cases} dp[i, \quad j \quad ] = \max(\ dp[i-1, \ j], \ dp[i-1, \ j-v] + w, \ dp[i-1, \ j-2v] + 2w, \ dp[i-1, \ j-3v] + 3w, \ \ldots) \\ dp[i, \ j-v] = \max(\quad\quad\quad\quad\quad \ \ \ dp[i-1, \ j-v], \quad\quad \ dp[i-1, \ j-2v] + \ \ w, \ dp[i-1, \ j-3v] + 2w, \ \ldots) \end{cases} {dp[i,j]=max( dp[i1, j], dp[i1, jv]+w, dp[i1, j2v]+2w, dp[i1, j3v]+3w, )dp[i, jv]=max(   dp[i1, jv], dp[i1, j2v]+  w, dp[i1, j3v]+2w, )

觀察上方兩式(兩者長很像,除去第一個不看之外,其它只是差了一個 w,所以最後補上),可得出如下:

d p [ i , j ] = max ⁡ (   d p [ i − 1 ,   j ] ,   d p [ i − 1 ,   j − v ] + w ,   d p [ i − 1 ,   j − 2 v ] + 2 w ,   d p [ i − 1 ,   j − 3 v ] + 3 w ,   … ) = max ⁡ (   d p [ i − 1 ,   j ] , d p [ i , j − v ] + w ) \begin{align*} & dp[i, \quad j \quad ] = \max(\ dp[i-1, \ j], \ dp[i-1, \ j-v] + w, \ dp[i-1, \ j-2v] + 2w, \ dp[i-1, \ j-3v] + 3w, \ \ldots) \\ & \quad\quad\quad\quad\quad = \max(\ dp[i-1, \ j], \quad\quad\quad\quad\quad\quad\quad\quad dp[i, \quad j - v ] + w) \end{align*} dp[i,j]=max( dp[i1, j], dp[i1, jv]+w, dp[i1, j2v]+2w, dp[i1, j3v]+3w, )=max( dp[i1, j],dp[i,jv]+w)

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在前 i i i 個物品中,背包容量為 j j j 時的最大價值( 0 ≤ i ≤ N 0 \leq i \leq N 0iN, 0 ≤ j ≤ V 0 \leq j \leq V 0jV)

完全背包的狀態轉移方程可以表示為:

d p [ i ] [ j ] = { 0  如果  i = 0  或  j = 0 d p [ i − 1 ] [ j ]  如果  i > 0  且  j < v i max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v i ] + w i )  如果  i > 0  且  j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]= 0dp[i1][j]max(dp[i1][j],dp[i][jvi]+wi) 如果 i=0  j=0 如果 i>0  j<vi 如果 i>0  jvi

  • i i i 等於 0 0 0 j j j 等於 0 0 0 時,表示沒有物品可選或背包容量為 0 0 0,此時最大價值為 0 0 0
  • i i i 大於 0 0 0 j j j 小於 v i v_i vi 時,背包容量不足以放入第 i i i 個物品,所以最大價值與前 i − 1 i-1 i1 個物品時的最大價值相同
  • i i i 大於 0 0 0 j j j 大於等於第 i i i 個物品的體積 v i v_i vi 時,對於當前的物品 i i i,我們可以選擇將其放入背包中多次,直到背包容量無法再容納它為止。我們考慮的是 d p [ i ] [ j − v i ] dp[\textcolor{orange}i][j-v_i] dp[i][jvi] 的值加上當前物品的價值 w i w_i wi(只有橘色的地方與 0/1 背包不同),以及 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]。取兩者中的較大值作為 d p [ i ] [ j ] dp[i][j] dp[i][j] 的值
// 二維 dp
// 時間複雜度: O(N*V)
// 空間複雜度: O(N*V)
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] volume = new int[N + 1];
        int[] worth = new int[N + 1];
        int[][] dp = new int[N + 1][V + 1];
        
        for(int i = 1; i <= N; ++i) {
            volume[i] = scanner.nextInt();
            worth[i] = scanner.nextInt();
        }
        
        scanner.close();
        
        for(int i = 1; i <= N; ++i) {
            // 為了延續,所以從 1 ~ V,就算 (j - v[i]) < 0 也須將上一輪的值更新至當前輪
            for(int j = 1; j <= V; ++j) {
                dp[i][j] = dp[i - 1][j];
                // 滿足放入條件,放入後比較大小,大的存入
                if(j - volume[i] >= 0)
                    dp[i][j] = Math.max(dp[i][j], dp[i][j - volume[i]] + worth[i]);
            }
        }
        
        System.out.println(dp[N][V]);
    }
}
// 降維 dp
// 時間複雜度: O(N*V)
// 空間複雜度: O(V)
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N, V;
        
        N = scanner.nextInt();
        V = scanner.nextInt();
        
        int[] volume = new int[N + 1];
        int[] worth = new int[N + 1];
        int[] dp = new int[V + 1];
        
        for(int i = 1; i <= N; ++i) {
            volume[i] = scanner.nextInt();
            worth[i] = scanner.nextInt();
        }
        
        scanner.close();

        for(int i = 1; i <= N; ++i) {        
            // j 正序 => 每一輪從前方走到後方時,都嘗試在放入此輪的物品 w[i] 後與 dp[j] 比大小(因為是正序,所以後方放入的是前方已經遍歷過的,細品一下,dp[j - v[i]] + w[i] 的 dp[j - v[i] 是不是在此輪的前方才遍歷過,也就是在此輪中不斷嘗試重覆放入找最大,亦即二維的 dp[i][j - v[i]])。在此輪的每一個 j 遍歷的體積中,盡可能都去塞入 w[i] 和 dp[j] 比較一下大小,大的存入 dp[j]

            // 在當前輪中,更新每一體積的最大值,在此輪每個 j 中嘗試塞入所有 w[i] 的可能,和 dp[j](這裡的 dp[j] 因為尚未更新,指的就是上一輪的 dp[j],即二維的 dp[i - 1][j]) 比較大小,所以在一輪中可能塞入多個 w[i](如果塞了 w[i] 後都比上一輪大的話)
            for(int j = volume[i]; j <= V; ++j) {
                dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
            }
        }
        
        System.out.println(dp[V]);
    }
}

多重背包

題目:https://www.acwing.com/problem/content/4/

在这里插入图片描述

從此題開始除特殊情況外,只保留降維 dp 的寫法

每種物品有 0 ~ s[i] 種可能,每種物品的最大數量皆可能不同,求物品能放入背包的最大價值

多重背包的樣態應該會像這樣,其實就是從 0/1 背包擴展出來,只是每件物品有 s[i] 個,即每件物品有從 0 ~ s[i] 共 s[i] + 1 選擇(選或不選,都不選就是該物品選 0 樣)

for(int i = 1; i <= N; ++i) {
    for(int j = V; j >= v[i]; --j) {
        // dp[j] 是上一輪的 dp[j],就是都不選,後面是選 1 種 ~ 選 s[i] 種
        dp[j] = max(dp[j], dp[j - v[i]] + w[i], dp[j - 2 * v[i]] + 2 * w[i] + ... dp[j - s[i] * v[i]] + s[i] * w[i])
    }
}

降維後的狀態轉移式(此處省略了遍歷物品,但記得代碼要寫遍歷物品)

d p [ j ] = 0 如果  j = 0 d p [ j ] = max ⁡ ( d p [ j ] , d p [ j − k ⋅ v [ i ] ] + k ⋅ w [ i ] ) 如果  j ≥ k ⋅ v [ i ] ,其中  1 ≤ k ≤ s [ i ] \begin{align*} dp[j] &= 0 \quad && \text{如果 } j = 0 \\ dp[j] &= \max(dp[j], dp[j-k \cdot v[i]] + k \cdot w[i]) \quad && \text{如果 } j \geq k \cdot v[i] \text{,其中 } 1 \leq k \leq s[i] \end{align*} dp[j]dp[j]=0=max(dp[j],dp[jkv[i]]+kw[i])如果 j=0如果 jkv[i],其中 1ks[i]

// 時間複雜度:O(N * V * K),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(V)


import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[] s = new int[N + 1];
        int[] dp = new int[V + 1];
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
            s[i] = scanner.nextInt();
        }
        
        for(int i = 1; i <= N; ++i) {
            // 0/1 背包的思路(選或不選)再加入 k 個該物品的遍歷可能,也就是該物品有 0 ~ k 個選擇
            for(int j = V; j >= v[i]; --j) {
                for(int k = 1; k <= s[i] && (j - k * v[i]) >= 0; ++k) {
                    dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(dp[V]);
    }
}

多重背包 二進制優化

題目:https://www.acwing.com/problem/content/description/5/

在这里插入图片描述

當數據量大時,必須要做優化,這裡使用二進制優化

  • 如果我們要遍歷 7,使用 3 個二進位數字即可表示
  • 如果我們要遍歷 13,使用 4 個二進位數字即可表示,多餘的放進最高位即可
13 = 1 + 2 + 4 + 6
0 ~ 7 使用 1, 2, 4 表示
8 使用 2 + 6
9 使用 1 + 2 + 6
10 使用 4 + 6
11 使用 1 + 4 + 6
12 使用 2 + 4 + 6
13 使用 1 + 2 + 4 + 6

10 = 1 + 2 + 4 + 3
0 ~ 7 使用 1, 2, 4 表示
8 使用 1 + 4 + 3
9 使用 2 + 4 + 3
10 使用 1 + 2 + 4 + 3

如果數字不滿足 2^N - 1,將剩餘的數字放入最高位即可用 log(2^N) 的位數列出所有可能
// 二進制優化
// 時間複雜度:O(N * log(K) * V),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(Max(V, N * log(K)))

import java.util.*;

public class Main {
    
    // 定義靜態類,方便等下放入 v 和 w
    public static class Goods {
        private int v;
        private int w;
        public Goods(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[] s = new int[N + 1];
        int[] dp = new int[V + 1];
        
        ArrayList<Goods> GoodsArr = new ArrayList<>();
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
            s[i] = scanner.nextInt();
            // 將物品數量由十進制轉成二進制存儲(這裡會將所有物品轉換後存入 GoodsArr)
            for(int k = 1; k <= s[i]; k *= 2) {
                s[i] -= k;
                GoodsArr.add(new Goods(k * v[i], k * w[i]));
            }
            // 如果還有剩餘的數字(代表 s[i] 不滿足 2^N - 1),放入最高位
            if(s[i] != 0)
                GoodsArr.add(new Goods(s[i] * v[i], s[i] * w[i]));
        }
        
        // 原本 0/1 背包是遍歷第 i 件物品,此時已將所有物品都轉化為二進制形式存放(等於遍歷了每種物品的所有可能),所以外層迴圈直接遍歷轉換過的二進制即可

        // i 遍歷的是 GoodsArr 長度,為 N * log(K) 次
        for(int i = 0; i < GoodsArr.size(); ++i) {
            for(int j = V; j >= GoodsArr.get(i).v; --j) {
                dp[j] = Math.max(dp[j], dp[j - GoodsArr.get(i).v] + GoodsArr.get(i).w);
            }
        }
        
        System.out.println(dp[V]);
    }
}

混合背包

題目:https://www.acwing.com/problem/content/7/

0/1 背包、完全背包與多重背包的混合體

// 時間複雜度:O(N * log(K) * V),K 為 s[i] 可能範圍的最大值
// 空間複雜度:O(Max(V, N * log(K)))
import java.util.*;

public class Main {
    
    public static class Goods {
        int v, w;
        int kind;
        public Goods(int kind, int v, int w) {
            this.kind = kind;
            this.v = v;
            this.w = w;
        }
    }
    
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[] s = new int[N + 1];
        int[] dp = new int[V + 1];
        
        ArrayList<Goods> GoodsArr = new ArrayList<>();
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
            s[i] = scanner.nextInt();
            // 如果是 0/1 背包
            if(s[i] == -1)
                GoodsArr.add(new Goods(-1, v[i], w[i]));
            // 如果是完全背包
            else if(s[i] == 0)
                GoodsArr.add(new Goods(0, v[i], w[i]));
            // 如果是多重背包,做特別處理
            else if(s[i] > 0) {
                // 多重背包轉成 0/1 背包的二進制優化
                // 轉換後以 0/1 背包存儲
                for(int k = 1; k <= s[i]; k *= 2) {
                    s[i] -= k;
                    GoodsArr.add(new Goods(-1, k * v[i], k * w[i]));
                }
                if(s[i] != 0)
                    GoodsArr.add(new Goods(-1, s[i] * v[i], s[i] * w[i]));
            }
        }
        
        for(int i = 0; i < GoodsArr.size(); ++i) {
            int cur_v = GoodsArr.get(i).v;
            int cur_w = GoodsArr.get(i).w;
            // 0/1 背包(包含從多重背包轉進來的)
            if(GoodsArr.get(i).kind < 0) {
                for(int j = V; j >= cur_v; --j) {
                    dp[j] = Math.max(dp[j], dp[j - cur_v] + cur_w);
                }
            }
            // 完全背包
            else {
                for(int j = cur_v; j <= V; ++j) {
                    dp[j] = Math.max(dp[j], dp[j - cur_v] + cur_w);
                }
            }
        }
        
        System.out.println(dp[V]);
    }
}

二維費用的背包問題

題目:https://www.acwing.com/problem/content/8/

在这里插入图片描述

限制從一個維度擴展到兩個維度,所以 dp 也相應擴展一個維度,若是 0/1 背包就用 0/1 背包的思維去解即可

// 時間複雜度:O(N * V * M)
// 空間複雜度:O(V * M)

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        int M = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] m = new int[N + 1];
        int[] w = new int[N + 1];
        int[][] dp = new int[V + 1][M + 1];
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            m[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        for(int i = 1; i <= N; ++i) {
            int cur_v = v[i];
            int cur_m = m[i];
            for(int j = V; j >= cur_v; --j) {
                for(int k = M; k >= cur_m; --k) {
                    dp[j][k] = Math.max(dp[j][k], dp[j - cur_v][k - cur_m] + w[i]);
                }
            }
        }
        System.out.println(dp[V][M]);
    }
}

分組背包

題目:https://www.acwing.com/problem/content/9/

在这里插入图片描述
在这里插入图片描述

一組之中至多只能選特定的一個,也可以不選,但一組不能選超過一個物品

多重背包是分組背包的一個特殊情況,將多重背包物品的個數看成是分組背包的組數

多重背包對於該物品的選擇數看做是分組背包一組內的選擇(一組代表一個物品),[這一組中從都不選,選一個,到該物品選擇 s[i] 個],一組裡只能挑出一個就恰好是多重背包中選擇該物品的個數

再形象點說,把多重背包的物品的個數看成是分組背包組內的選擇,在這個分組背包中,這一組對該物品有 [都不打包,打包一件,打包二件…打包 s[i] 件],然後分組背包的意思是說在一組中我只能選一個,那不就正好是多重背包中要求的該物品選擇的個數嗎?

本質上是一種 0/1 背包,選與不選

多重背包是分組背包的一種特例,但多重背包有可以優化的方式,分組背包範圍則更廣泛,目前還沒有優化的方式

for(int i = 1; i <= N; ++i) {
    for(int j = V; j >= v[i]; --j) {
        dp[j] = max(dp[j], dp[j - v[1]] + w[1], dp[j - v[2]] + w[2] + ... + dp[j - v[s]] + w[s]);
    }
}

降維後的狀態轉移式(此處省略了遍歷物品,但記得代碼要寫遍歷物品)

d p [ j ] = 0 如果  j = 0 d p [ j ] = max ⁡ ( d p [ j ] , d p [ j − v [ k ] ] + w [ k ] ) 如果  j ≥ v [ k ] ,其中  1 ≤ k ≤ s \begin{align*} dp[j] &= 0 \quad && \text{如果 } j = 0 \\ dp[j] &= \max(dp[j], dp[j-v[k]] + w[k]) \quad && \text{如果 } j \geq v[k] \text{,其中 } 1 \leq k \leq s \end{align*} dp[j]dp[j]=0=max(dp[j],dp[jv[k]]+w[k])如果 j=0如果 jv[k],其中 1ks

// 時間複雜度:O(N * V * S)
// 空間複雜度:O(V)

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] dp = new int[V + 1];
        
        for(int i = 1; i <= N; ++i) {
            
            int S = scanner.nextInt();
            int[] v = new int[S + 1];
            int[] w = new int[S + 1];
            
            for(int k = 1; k <= S; ++k) {
                v[k] = scanner.nextInt();
                w[k] = scanner.nextInt();
            }
            
            for(int j = V; j >= 0; --j) {
                for(int k = 1; k <= S; ++k) {
                    // 要記得確保 j - v[k] 不越界
                    if(j - v[k] >= 0)
                        dp[j] = Math.max(dp[j], dp[j - v[k]] + w[k]);
                }
            }
        }
        System.out.println(dp[V]);
    }
}

有依賴的背包問題

困難題(為面試刷題的可以跳過)

此題是在選擇物品前需要先選擇其依賴的物品

題目:https://www.acwing.com/problem/content/description/10/

在这里插入图片描述
在这里插入图片描述

// 時間複雜度:O(N^2 * V)
// 空間複雜度:O(N * V)
import java.util.*;

public class Main {
    public static void dfs(int x, ArrayList<Integer>[] dep_node, int[][] dp, int[] v, int[] w, int N, int V) {
        // 所有選到 x 的都需放入 w[x],確保有空間放入,所以只放入容量大於 v[x] 的可能
        // 因為不放入就不能遍歷其的依賴節點(題意是 x 節點走過才能走依賴 x 節點的節點)
        for(int j = v[x]; j <= V; ++j)
            dp[x][j] = w[x];
        
        // 遍歷所有依賴節點
        for(int i = 0; i < dep_node[x].size(); ++i) {
            // 找出依賴於 x 節點的下一個值,要遍歷所有依賴 x 節點的節點
            int y = dep_node[x].get(i);
            
            // 樹狀 dp,為了遍歷所有依賴 x 節點的可能
            dfs(y, dep_node, dp, v, w, N, V);
            
            // 本質上是一個 0/1 背包,每種物品只有選與不選
            // 要保證 x 這個節點能放入 w[i]
            for(int j = V; j >= v[x]; --j) {
                // 遍歷下一個依賴節點最多能放入的容量 [ 1, j - v[x] ]
                // k = 0 就是原先上一輪的 dp[x][j]
                for(int k = 1; k <= j - v[x]; ++k) {
                    // 為此節點(x)留下 v[x] 的空間,這樣下一個依賴節點(y)最多能放入的容量為 j - v[x]
                    dp[x][j] = Math.max(dp[x][j], dp[x][j - k] + dp[y][k]);
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        
        // dp[x][i] 表示以 x 為子樹的物品,在容量不超過 i 時的最大價值
        int[][] dp = new int[N + 1][V + 1];
        // dep_node[x][i] 表示所有依賴 x 的物品節點,在容量不超過 i 時的最大價值
        ArrayList<Integer>[] dep_node = new ArrayList[N + 1];
        
        for(int i = 1; i <= N; ++i)
            dep_node[i] = new ArrayList<>();
        
        int root = -1;
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
            int p = scanner.nextInt();

            if(p == -1) {
                root = i;
            }
            else
                dep_node[p].add(i);
        }

        // 先塞入 root,找符合條件的最大價值
        dfs(root, dep_node, dp, v, w, N, V);
        
        // 印出由 root 節點開始遍歷的最大容量就是答案,為什麼 root 物品在最大容量中的會是最大的呢,因為 dfs 中遍歷求最大值是後序操作的,也就意謂著當你先塞入 root,但 root 實際上最後才會被執行。
        // 遞歸它就是個棧,先壓 root,再壓與 root 依賴的節點(y),再壓與該節點(y)依賴的節點…,先進後出,結合棧與後序位置才遍歷的特性,就可以知道 root 雖然先進入,但是最後才執行遍歷操作。
        // 最後才遍歷的意義就是底下節點都遍歷好了,回到我們最開始就說過的邏輯,那我們打印最後一輪中容量最大的就會是答案
        System.out.println(dp[root][V]);
    }
}

背包問題求方案數

題目:https://www.acwing.com/problem/content/11/

在这里插入图片描述

此題要求 0/1 背包的最大值方案總數

0/1 背包的狀態轉移方程可以表示為:

dp[i][j] 是考慮前 i 個物品,當前容量不超過 j最大價值

d p [ i ] [ j ] = { 0  如果  i = 0  或  j = 0 d p [ i − 1 ] [ j ]  如果  i > 0  且  j < v i max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v i ] + w i )  如果  i > 0  且  j ≥ v i dp[i][j] = \begin{cases} 0 & \text{ 如果 } i = 0 \text{ 或 } j = 0 \\ dp[i-1][j] & \text{ 如果 } i > 0 \text{ 且 } j < v_i \\ \max(dp[i-1][j], dp[i-1][j-v_i] + w_i) & \text{ 如果 } i > 0 \text{ 且 } j \geq v_i \end{cases} dp[i][j]= 0dp[i1][j]max(dp[i1][j],dp[i1][jvi]+wi) 如果 i=0  j=0 如果 i>0  j<vi 如果 i>0  jvi

路徑跟蹤

pathNum[i][j] 是考慮前 i 個物品,當前使用容量恰好為 j,且價值最大的方案數

p a t h N u m [ i ] [ j ] = { 1  如果  i = 0  且  j = 0 p a t h N u m [ i − 1 ] [ j ] + p a t h N u m [ i − 1 ] [ j − v ]  如果  d p [ i ] [ j ] = d p [ i − 1 ] [ j ]  且  d p [ i ] [ j ] = d p [ i − 1 ] [ j − v i ] + w i p a t h N u m [ i − 1 ] [ j ]  如果  d p [ i ] [ j ] = d p [ i − 1 ] [ j ]  且  d p [ i ] [ j ] ≠ d p [ i − 1 ] [ j − v i ] + w i p a t h N u m [ i − 1 ] [ j − v ]  如果  d p [ i ] [ j ] ≠ d p [ i − 1 ] [ j ]  且  d p [ i ] [ j ] = d p [ i − 1 ] [ j − v i ] + w i pathNum[i][j] = \begin{cases} 1 & \text{ 如果 } i = 0 \text{ 且 } j = 0 \\ pathNum[i - 1][j] + pathNum[i - 1][j - v] & \text{ 如果 } dp[i][j] = dp[i - 1][j] \text{ 且 } dp[i][j] = dp[i - 1][j - v_i] + w_i \\ pathNum[i-1][j] & \text{ 如果 } dp[i][j] = dp[i - 1][j] \text{ 且 } dp[i][j] \neq dp[i - 1][j - v_i] + w_i \\ pathNum[i - 1][j - v] & \text{ 如果 } dp[i][j] \neq dp[i - 1][j] \text{ 且 } dp[i][j] = dp[i - 1][j - v_i] + w_i \\ \end{cases} pathNum[i][j]= 1pathNum[i1][j]+pathNum[i1][jv]pathNum[i1][j]pathNum[i1][jv] 如果 i=0  j=0 如果 dp[i][j]=dp[i1][j]  dp[i][j]=dp[i1][jvi]+wi 如果 dp[i][j]=dp[i1][j]  dp[i][j]=dp[i1][jvi]+wi 如果 dp[i][j]=dp[i1][j]  dp[i][j]=dp[i1][jvi]+wi

// 二維 dp
// 時間複雜度:O(N * V)
// 時間複雜度:O(N * V)
import java.util.*;

public class Main {
    final static int mod = 1000000007;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[][] dp = new int[N + 1][V + 1];
        int[][] pathNum = new int[N + 1][V + 1];
        
        pathNum[0][0] = 1;
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        for(int i = 1; i <= N; ++i) {
            for(int j = V; j >= 0; --j) {
                dp[i][j] = dp[i - 1][j];
                if(j >= v[i])
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);

                    
                if(j >= v[i] && dp[i - 1][j] == dp[i - 1][j - v[i]] + w[i])
                    pathNum[i][j] = (pathNum[i - 1][j] + pathNum[i - 1][j - v[i]]) % mod;
                else if(dp[i][j] == dp[i - 1][j])
                    pathNum[i][j] = pathNum[i - 1][j] % mod;
                else
                    pathNum[i][j] = pathNum[i - 1][j - v[i]] % mod;
                
            }
        }
        
        int res = 0;
        for(int j = 0; j <= V; ++j) {
            if(dp[N][V] == dp[N][j])
                res += pathNum[N][j] % mod;
        }
        System.out.println(res);
    }
}
// 降維 dp
// 時間複雜度:O(N * V)
// 時間複雜度:O(V)
import java.util.*;

public class Main {
    final static int mod = 1000000007;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[] dp = new int[V + 1];
        int[] pathNum = new int[V + 1];
        
        pathNum[0] = 1;
        
        for(int i = 1; i <= N; ++i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        for(int i = 1; i <= N; ++i) {
            for(int j = V; j >= v[i]; --j) {
                    
                int tmp = Math.max(dp[j], dp[j - v[i]] + w[i]);

                if(dp[j] == dp[j - v[i]] + w[i])
                    pathNum[j] = (pathNum[j] + pathNum[j - v[i]]) % mod;
                else if(tmp == dp[j])
                    pathNum[j] = pathNum[j] % mod;
                else
                    pathNum[j] = pathNum[j - v[i]] % mod;
                dp[j] = tmp;
            }
        }
        
        int res = 0;
        for(int j = 0; j <= V; ++j) {
            if(dp[V] == dp[j])
                res += pathNum[j] % mod;
        }
        System.out.println(res);
    }
}

背包問題求具體方案

題目:https://www.acwing.com/problem/content/description/12/

在这里插入图片描述

實際上這題是要找出最大值的具體選擇,並且依編號升序排列

此題需要求具體方案的路徑,所以不能降維

// 二維 dp
// 時間複雜度:O(N * V)
// 空間複雜度:O(N * V)
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N = scanner.nextInt();
        int V = scanner.nextInt();
        int[] v = new int[N + 1];
        int[] w = new int[N + 1];
        int[][] dp = new int[N + 1][V + 1];
        
        // 答案要按編號順序排列,所以先放入最後一個物品,再依序放入直至第一個,也就是倒序放入物品
        // 原本的 1 變為 N,原本的 N 變為 1
        for(int i = N; i >= 1; --i) {
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        
        // 雖然 i 遍歷是正序的,先遍歷 1,再繼續遍歷   2  , … 直至遍歷至   N
        // 實際遍歷是倒序的,先放入編號 N,再嘗試放入 N - 1,… 直至嘗試放入 1
        for(int i = 1; i <= N; ++i) {
            // 0/1 背包
            for(int j = 0; j <= V; ++j) {
                dp[i][j] = dp[i - 1][j];
                if(j - v[i] >= 0)
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
            }
        }
        
        // 記錄當前的背包容量
        int cur_v = V;
        
        // 從結果(dp[N][M] 是最大價值)反推其路徑,雖說是反推但其實我們原本的順序是顛倒的,所以 i 雖然是倒序,但編號是正序遍歷
        for(int i = N; i >= 1; --i) {
            // 滿足條件,代表 dp[i][cur_v] 是其推導過來,所以我們要往回走
            if(cur_v >= v[i] && dp[i][cur_v] == dp[i - 1][cur_v - v[i]] + w[i]) {
                cur_v -= v[i];
                // 打印出來時,注意 i 是倒的,但編號是正的,把 i 轉為編號(當 i 是 N 時,編號為 1)
                System.out.printf(N - i + 1 + " ");
            }
        }
    }
}

恰好等於背包體積求最大最小值(番外篇)

當背包要求恰好裝滿時的,就不能再初始化為 0,而是應該初始化為反向的極值,也就是如果要你求最大值,那就應該設為負無窮;求最小值,就設成正無窮。但你必須設置 dp[0] 為 0

為什麼這樣就會是恰好裝滿呢,當我們不需恰好裝滿時將所有體積的 dp 路徑都初始為 0,這就會造成不恰好裝滿的情況(例如 dp[1] = 0,即當體積為 1 的時候,價值為 0,就是沒裝滿的情況,往後迭代如果用到體積 1 時就可能讓沒裝滿的情況一路往後延續),我們只要將初始值設為都是恰好裝滿,不恰好裝滿的體積設一個相反極值讓它被之後我們 dp[0] = 0 推導出來的恰好裝滿給輕易取代掉即可


拿上面的完全背包一維 dp 舉例,0/1 背包也是同理修改初始值即可

// 降維 dp
// 時間複雜度: O(N * V)
// 空間複雜度: O(V)
import java.util.Scanner;
import.java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int N, V;
        
        N = scanner.nextInt();
        V = scanner.nextInt();
        
        int[] volume = new int[N + 1];
        int[] worth = new int[N + 1];
        int[] dp = new int[V + 1];
        
        for(int i = 1; i <= N; ++i) {
            volume[i] = scanner.nextInt();
            worth[i] = scanner.nextInt();
        }

        // 求最大值,所以設為負無窮
        Arrays.fill(dp, Integer.MIN_VALUE)
        
        // 很重要,當體積為 0 時的價值為 0 是合法的(也就是恰好裝滿背包,體積 0 => 沒塞東西 => 價值 0)。藉由設置此才能一路往後推,讓其它經過的 dp 路徑合法
        dp[0] = 0;

        scanner.close();

        for(int i = 1; i <= N; ++i) {        
            for(int j = volume[i]; j <= V; ++j) {
                // 提前判定,如果求最小值, dp 初始為最大值時要防止溢位才需設置此。但這裡無論求最大還最小統一都設置 if 條件,才不會混亂
                if(dp[j - volume[i]] == Integer.MIN_VALUE)
                    dp[j] = Math.max(dp[j], dp[j - volume[i]] + worth[i]);
            }
        }
        
        if(dp[V] != Integer.Min_VALUE)
            System.out.println(dp[V]);
        else
            System.out.println("背包無法恰好裝滿");
    }
}

求最小值的題目

HDU 1114 Piggy-Bank

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int T = scanner.nextInt();
        
        for(int k = 0; k < T; ++k) {
            
            int E = scanner.nextInt();
            int F = scanner.nextInt();
            int V = F - E;
            
            int N = scanner.nextInt();

            int[] weight = new int[N + 1];
            int[] value = new int[N + 1];
            int[] dp = new int[V + 1];
            
            for(int i = 1; i <= N; ++i) {
                value[i] = scanner.nextInt();
                weight[i] = scanner.nextInt();
            }
            
            Arrays.fill(dp, Integer.MAX_VALUE);
            dp[0] = 0;
            
            for(int i = 1; i <= N; ++i) {
                for(int j = weight[i]; j <= V; ++j) {
                    // 排除可能的溢位錯誤
                    if(dp[j - weight[i]] != Integer.MAX_VALUE)
                        dp[j] = Math.min(dp[j], dp[j - weight[i]] + value[i]);
                }
            }
            
            if(dp[V] == Integer.MAX_VALUE)
                System.out.println("This is impossible.");
            else
                System.out.println("The minimum amount of money in the piggy-bank is " + dp[V] + ".");
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值