題目來源:揹包九講
時間限制:1000ms 內存限制:64mbjava
有 \(N\) 件物品和一個容量是 \(V\) 的揹包。每件物品只能使用一次。
第 \(i\) 件物品的體積是 \(v_i\),價值是 \(w_i\)。
求解將哪些物品裝入揹包,可以使這些物品的整體積不超過揹包容量,且總價值最大。
輸出最大價值。算法
第一行兩個整數,\(N\),\(V\),用空格隔開,分別表示物品數量和揹包容積。
接下來有 \(N\) 行,每行兩個整數 \(v_i\),\(w_i\),用空格隔開,分別表示第 \(i\) 件物品的體積和價值。shell
輸出一個整數,表示最大價值。數組
0 < \(N\),\(V\) ≤ 1000
0 < \(v_i\),\(w_i\) ≤ 1000學習
4 5 1 2 2 4 3 4 4 5
8
嘗試各類可能的商品組合,並找出價值最高的組合。
使用 \(N\) 位二進制字串表示物品是否放入揹包,枚舉全部的可能,而後算出每種可能的價值,取其最大值輸出。
解法不足:速度很是慢。在只有3件商品的狀況下,你須要計算8個不一樣的集合;當有4件商品的時候,你須要計算16個不一樣的集合。每增長一件商品,須要計算的集合數都將翻倍。
對於每一件商品,都有選或不選兩種可能,即這種算法的運行時間是 \(O(2^n)\) 。
IO競賽(例如:藍橋杯)當中,若是實在想不起來動態規劃,可使用這個方法拿到一部分分數,可是在ACM競賽當中,就不能使用這種方法了。ui
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); } }
對於動態規劃算法,能夠去這篇文章裏學習:經典中的經典算法:動態規劃(詳細解釋,從入門到實踐,逐步講解)
每一個動態規劃都從一個網格開始。
在本題中,網格的各行表示商品,各列表明不一樣容量的揹包(從1到V)。
網格最初是空的。你將填充其中的每一個格子,網格填滿後,就找到了問題的答案!
好比本題樣例的網格以下圖:
spa
在每一個一格子你都要作出一個選擇:放不放進這一行對應的物品。
物品1所佔空間爲 \(1\) ,也就是說物品1能放進容量爲 \(1\) 的這個揹包,所以這個單元格包含物品1。因此往第一行第一列中裝入物品1,價值爲2。
這行的其餘單元格也同樣。別忘了,這是第一行,只有一個物品可供你選擇,換而言之,你僞裝如今尚未打算放進其餘物品。
填充完以後以下圖:
此時你極可能心存疑惑:原題說的是容量爲5的揹包,咱們爲什麼要考慮容量爲一、二、三、4的揹包呢?動態規劃從子問題着手,逐步解決大問題。這裏解決的子問題將幫助你解決大問題。
這行表示的是當前的最大價值,並非最終解。隨着算法往下執行,將逐步修改最大價值。.net
在第二行中,你有兩種物品選擇。先看第一個單元格,容量爲1,以前裝進去的最大的價值爲2。
這一個單元格該不應放入第二個物品呢?答案顯然是不行,由於容量爲1的口袋沒法裝下佔空間2的物品。
所以第一列的最大價值保持不變。
接下來看第二行第二列,這個格子所對應揹包的容量爲2,如今可以裝下第二個物品了,對比一下價值,比以前決定的物品1價值高,因此將原來的物品1換爲物品2。
再看後面的第三列,這個格子容量爲3,能夠同時裝下物品1和物品2,因此都放進去。
以後的列也進行同樣的操做,最後操做完第二行的結果爲:
3d
以一樣的方式處理物品3,物品3佔用空間3,價值爲4。
其中在第五列時,容量爲5,本來決定的爲(物品1+物品2),如今有物品3能夠選擇了,因此考慮將物品1換爲價值更高的物品3,因此此行第五列結果爲8
作完這一步就獲得了下面的網格:
code
能夠總結獲得一個公式:\( 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開始處理。
根據第五步的公式,對於編號爲 \(i\) 的物品:
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)); } }