四种揹包问题总结

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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章