hihoCode----揹包問題
0-1揹包問題
且說上一週的故事裡,小Hi和小Ho費勁心思終於拿到了茫茫多的獎券!而現在,終於到了小Ho領取獎勵的時刻了!
小Ho現在手上有M張獎券,而獎品區有N件獎品,分別標號為1到N,其中第i件獎品需要need(i)張獎券進行兌換,同時也只能兌換一次,為了使得辛苦得到的獎券不白白浪費,小Ho給每件獎品都評了分,其中第i件獎品的評分值為value(i),表示他對這件獎品的喜好值。現在他想知道,憑藉他手上的這些獎券,可以換到哪些獎品,使得這些獎品的喜好值之和能夠最大。
解題思路:列舉2^N種可能的選取方案,先計算他們需要的獎券之和sum,在sum不超過M的情況下,計算他們的喜好值之和value,並統計一個最優的方案,也就是value的最大值。時間複雜度O(2^N),暴力方法不可取。考慮動態規劃。
首先,我們要想辦法把我們現在遇到的問題給抽象化。以best(i, j)表示已經決定了前i件物品是否選取,當前已經選取的物品的所需獎券數總和不超過j時,能夠獲取的最高的喜好值的和。對於任意i>1, j,我們都可以知道best(i, j)=max{best(i-1, j-need(i)) + value(i), best(i - 1, j)}。
檢驗一下這個問題的定義方法是否擁有動態規劃所需要的兩種性質:重複子問題和無後效性。
首先看重複子問題——這是動態規劃之所以比搜尋高效的原因,如果最後四件獎品分別為所需獎券為1,喜好值為1、所需獎券為2,喜好值為2、所需獎券為3,喜好值為3、所需獎券為4,喜好值為4的四個獎品,那麼無論是選擇1、4還是2、3,都會要求解best(N-4, M-5)這樣一個子問題,而這個子問題只需要求解一次就能夠進行計算,所以重複子問題這一性質是滿足的。
其次再看無後效性……同樣的,如果分別有所需獎券為1,喜好值為1、所需獎券為2,喜好值為2、所需獎券為3,喜好值為3、所需獎券為4,喜好值為4的四個獎品,那麼無論是選取第1個和第4個,還是選取第2個和第3個,他們的所需獎券數都為5,喜好值之和都為5。所以我只需要知道best(4, 5)=5就夠了,它為什麼等於5對我而言沒有區別,不會對之後的決策產生影響。這就是無後效性,所以想來也是滿足的。
那麼接下來要考慮的是如何使用best(i, j)=max{best(i-1, j-need(i)) + value(i), best(i - 1, j)}來求解每一個best(i, j)了。我們定義一個問題A依賴於另一個問題B當且僅當求解A的過程中需要事先知道B的值,那麼我們很容易的發現best(i, j)是依賴於best(i-1, j-need(i))和best(i-1, j)兩個問題的,也就是說這兩個問題要先於best(i, j)進行求解。所以我們只要按照i從小到大的順序,以這樣的方式進行計算,就可以了。
時間複雜度已經優化到O(NM)。接下來優化空間複雜度。
按照上面的思路,需要開一個N * M大小的二維陣列best,來記錄求解出的best值。但是我們在計算i的時候只需要i-1的資料,所以空間可以優化到2*M。進一步,只需要開一個M大小的一維陣列就可以了。如果我按照j從M到1的順序,也就是跟之前相反的順序來進行計算的話。另外根據我們的狀態轉移方程,可以顯然得出如果狀態(iA, jA)依賴於狀態(iB, jB),那麼肯定有iA = iB+1, jA>=jB。所以不難得出一個結論:我在計算best(i, j)的時候,因為best(i, j+1..M)這些狀態已經被計算過了,所以意味著best(i - 1, k),k=j..M這些值都沒有用了——所有依賴於他們的值都已經計算完了。於是它們原有的儲存空間都可以用來儲存別的東西,所以我不仿直接就將best(i, j)的值存在best(i-1, j)原有的位置上。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
while(cin.hasNext()) {
int N = cin.nextInt(); // N個獎品
int M = cin.nextInt(); // M張劵
int[] need = new int[N+1];
int [] value = new int[N+1];
for(int i=1; i<=N; i++) {
need[i] = cin.nextInt();
value[i] = cin.nextInt();
}
int[] f = new int[M+1];
for(int i=1; i<=N; i++) {
for(int j=M; j>=0; j--) {
if (j >= need[i]) {
f[j] = Math.max(f[j], f[j-need[i]] + value[i]);
}
}
}
System.out.println(f[M]);
}
}
}
完全揹包問題
且說之前的故事裡,小Hi和小Ho費勁心思終於拿到了茫茫多的獎券!而現在,終於到了小Ho領取獎勵的時刻了!
等等,這段故事為何似曾相識?這就要從平行宇宙理論說起了………總而言之,在另一個宇宙中,小Ho面臨的問題發生了細微的變化!
小Ho現在手上有M張獎券,而獎品區有N種獎品,分別標號為1到N,其中第i種獎品需要need(i)張獎券進行兌換,並且可以兌換無數次,為了使得辛苦得到的獎券不白白浪費,小Ho給每件獎品都評了分,其中第i件獎品的評分值為value(i),表示他對這件獎品的喜好值。現在他想知道,憑藉他手上的這些獎券,可以換到哪些獎品,使得這些獎品的喜好值之和能夠最大。
思路:按照01揹包的想法,我可以使用best(i, j)表示已經決定了前i件物品每件物品選擇多少件,當前已經選取的物品的所需獎券數總和不超過j時,能夠獲取的最高的喜好值的和,那麼最終的答案便是best(N, M)。對於一個問題best(i, j),考慮最後一步——即第i件物品選擇多少件,不妨就假設選擇k件吧,那麼k的取值範圍肯定是在0~(j / need(i))這個範圍內。這個時候我們可以知道best(i - 1, j - need(i) * k) + value(i) * k將會是一種可能的方案。best(i, j) = max(best(i - 1, j - need(i) * k) + value(i) * k),0 <= k <= j / need(i)。我們可以進一步將子結構最優化。將子問題細化到選擇第i件物品的每一件,比如我們在選擇第i件物品的k件的時候,是可以利用第i件物品的k-1件的資訊的。於是,best(i, j) = max(best(i-1, j), best(i, j - need(i)) + value(i))。第一種情況是一件第i件都不選,第二種情況是在i件已經選了k-1的基礎上再選一件i。
和0-1揹包問題類似,為了優化空間複雜度,我們將j按照從0到M的順序遍歷即可。可以發現,程式碼與0-1揹包問題的唯一區別只有一行。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in);
while(cin.hasNext()) {
int N = cin.nextInt();
int M = cin.nextInt();
int[] need = new int[N+1];
int[] value = new int[N+1];
for(int i=1; i<=N; i++) {
need[i] = cin.nextInt();
value[i] = cin.nextInt();
}
int[] f = new int[M+1];
for(int i=1; i<=N; i++) {
for(int j=0; j<=M; j++) {
if(need[i] <= j) {
f[j] = Math.max(f[j], f[j-need[i]]+value[i]);
}
}
}
System.out.println(f[M]);
}
}
}