四種揹包問題總結

1、01揹包

問題描述:有n種物品,每種物品有價值v和重量w,給定一個容量爲c的揹包,每種物品只能選一個,求揹包能裝物品的最大價值。

解法:用dp(i)( c)來表示當前揹包剩餘容量爲c時,前i個物品的最大價值。

之所以用這兩個狀態,是因爲揹包不同容量時候,選擇也不一樣。

狀態轉移情況是選擇或者不選擇第i個物品,狀態方程爲:

(注意這個選擇或者不選擇第i個物品,它表示組合數,與順序無關,儘管我們是按照順序列舉i的,但是我們列舉第i個的時候就默認包含了所有的0-i,順序列舉只是爲了拆分子問題。這和爬樓梯問題有本質區別,爬樓梯是有順序的,是排列數,是:dp[n]=dp[n-1]+dp[n-2])

dp(i)( c) = max( dp(i-1)( c), dp(i-1)(c-wi)+vi );

public int f(int[] v,int w[],int n){
    int[][] dp = new int[n+1][C+1];//此時初始化爲0,不需要單獨寫
	for(int i = 1;i<=n;i++){
    	for(int c=1;c<=C;c++){
       		if(w[i]>c){
            	dp[i][c]=dp[i-1][c];
        	}
        	else{
            	d[i][c] = Math.max(dp[i-1][c],dp[i-1][c-w[i]]+v[i]);
        	}
    	}
    return dp[n][c];
	}
}

有代碼可見,每一行的狀態只和上一行有關係,因此可以用空間壓縮:

public int f(int[] v,int w[],int n){
    int[] dp = new int[C+1];
	for(int i = 1;i<=n;i++){
    	for(int c=C;c>=w[i];c--){//倒序是爲了防止覆蓋
            d[c] = Math.max(dp[c],dp[c-w[i]]+v[i]);
    	}
    return dp[c];
	}
}

若題目要求恰好裝滿的最大價值,則沒恰好裝滿的情況都是無解的情況,此時只需要改變初始值就可以,這樣,不能恰好裝滿的都會被無窮小填充

見leetcode 分割等和子集(這也是親身經歷慘痛的騰訊面試題)

public int f(){
    int[][] dp = new int[n+1][C+1];
    for(int i =1;i<=C;i++){
        dp[0][i]=Integer.MIN_VALUE;
    }
    dp[0][0]=0;
	for(int i = 2;i<=n;i++){
    	for(int c=1;c<=C;c++){
       		if(w[i]>c){
            	dp[i][c]=dp[i-1][c];
        	}
        	else{
            	d[i][c] = Math.max(dp[i-1][c],dp[i-1][c-w[i]]+v[i]);//此處控制無解情況
                //若不選第i個,則由i-1個控制,若選擇,則由dp[i-1][c-w[i]]控制
        	}
    	}
    return dp[n][c];
	}
}

爬樓梯問題:

有n個臺階,每次可以上w=[1,2,3,4]個臺階,問從0爬到n有多少種方法。

解法:每次列舉w,狀態方程dp[n]=dp[n-w[0]]+dp[n-w[1]]+…+dp[n-w[m]]

此題看似和揹包問題比較相似,但是有很大區別,比如第一步走1個臺階,第二步走兩個臺階,和第一步走兩個第二步走一個不是同一情況。但是對於揹包,則沒有先後之分,都一樣。

2、完全揹包

問題描述:有n種物品,每種物品有價值v和重量w,給定一個容量爲c的揹包,每種物品可以選任意個,求揹包能裝物品的最大價值。

這個問題和01揹包的區別是,我們進行狀態轉移時候,不僅考慮選或者不選,而是選0件,選1件,一直到選C/w[i]件。

可以寫出狀態方程:

dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)
//dp[i][j]表示前i個物品組成最大容量爲j的揹包的最大價值

此時解題的代碼可以寫爲:

public int f(){
    int[][] dp = new int[n+1][C+1];
	for(int i = 1;i<=n;i++){
    	for(int j=1;j<=C;j++){
       		for(int k=0;k*w[i]<j;k++){
                dp[i][j]=Math.max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
            }
    	}
    return dp[n][c];
	}
}

上述的複雜度是C^2 *N,考慮優化狀態方程:

dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)(1)
    
把dp[i][j-w[i]]帶入上述方程:
dp[i][j-w[i]]=Math.max(dp[i-1][j-w[i]],dp[i-1][j-2w[i]]+v[i]...)(2)
    
dp[i][j-w[i]]+v[i]=v[i]+Math.max(dp[i-1][j-w[i]],dp[i-1][j-2w[i]]+2v[i]...)(3)
=Math.max(dp[i-1][j-w[i]]+v[i],dp[i-1][j-2w[i]]+2v[i]...)(4)
    
公式(1)中的後面的項和公式(4)後面的項一樣,可以合併:

dp[i][j]=Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i])

由此我們得到優化的轉移方程,複雜度是C*n。

dp[i][j]=Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i])

上述方程也可以有另一種解釋:對於第i個物品,我們面臨兩種互補的選擇:要麼不選i,要麼至少選一個i,兩種情況不僅互斥,他們的並集就是全集,所以稱之爲互補。至於爲什麼至少選一個i是這種形式:

dp[i][j-w[i]]+v[i]

因爲我們要消除選擇一個i帶來的影響,而不管接下來會怎麼樣,就是至少選擇一個。就像正則表達式匹配一樣。正則表達式匹配

3、多重揹包

問題描述:有n種物品,每種物品有價值v和重量w,給定一個容量爲c的揹包,每種物品可以選p個,求揹包能裝物品的最大價值。

此問題明顯是完全揹包的變形,可以和完全揹包幾乎一樣,只是多了一個最多p個的判斷:

public int f(){
    int[][] dp = new int[n+1][C+1];
	for(int i = 1;i<=n;i++){
    	for(int j=1;j<=C;j++){
       		for(int k=0;k*w[i]<j && k<=p[i];k++){
                dp[i][j]=Math.max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
            }
    	}
    return dp[n][c];
	}
}

4、分組揹包

問題描述: 物品被劃分爲若干組,每組中的物品互相沖突,最多選一件。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。

問題變成了每組選或者不選

狀態方程爲:

f[k][j]=max(f[k−1][j],f[k−1][j−c[i]]+w[i]∣物品i屬於組k)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章