【背包+DP】背包问题+面试实例

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_25232555/article/details/78106107

京东笔试的时候有一道求幂的题目,看到讨论区有大佬用背包算法求解,回来复习复习背包问题。


N件物品,每件重量为weight[i],价值为price[i],问如何装进容易Vol的背包中得到背包价值最大。

此类问题分为0-1背包、完全背包、多重背包问题。

0-1背包:每件物品只有一个;

完全背包:每件物品有无限个;

多重背包:每件物品有K个;


0-1背包:

DP: dp[i][j] 表示j的背包中装进前i件物品所得到的最大价值。

递推关系:dp[i][j] = max {dp[i - 1][j], dp[i - 1][j - weight[i]] + price[i]};

解释:对于第i件物品,有两种选择:装or不装。

如果不装,则dp[i][j] 的值就是容量为j时,仅有前i - 1项物品时所能得到的最大价值;

如果装,则需要先保证有weight[i]的空余位置,即j-weight[i]。

两者取最大值,即为拥有前i项物品可以选择时的最优值。


可以注意到,因为在迭代的过程中,二维数组dp[i][j]的值取决于的两项只跟它的上一行(dp[i-1][*])有关,所以二维数组可以优化成一维数组来代替。


static int zeroOnePack(int [] weight, int [] price, int vol){
		
		int [] record = new int [vol + 1];
		
		for(int i = 0; i < weight.length; ++i){
			for(int k = vol; k >= weight[i]; --k){
				if(record[k] < record[k - weight[i]] + price[i])
					record[k] = record[k - weight[i]] + price[i];
			}
		}
		return record[vol];
	}

注意一点,内层循环需要从上限向下遍历。因为是0-1背包,每个物品只能装1次。

如果是从下限开始遍历,如果当前物品的性价比非常高,则会重复装包多次。

以此来看,所谓完全背包只不过是将循环的方向改变即可。


完全背包

static int fullPack(int [] weight, int [] price, int vol){
		
		int [] record = new int [vol + 1];
		
		for(int i = 0; i < weight.length; ++i){
			for(int k = weight[i]; k <= vol; ++k){
				if(record[k] < record[k - weight[i]] + price[i])
					record[k] = record[k - weight[i]] + price[i];
			}
		}
		return record[vol];
	}


===================================================================


刷牛客讨论区看到的一道面试代码题目,大佬解释的用二重背包求解:


作者:Cavs
链接:https://www.nowcoder.com/discuss/48597?type=0&order=0&pos=128&page=1
来源:牛客网

输入:一个正整数数组a,最多包含50个元素,每个元素最大1000,所有元素的和最大为1000。可能包含重复元素。
找出数组中两个不相交子集,使得两个子集中元素的和相同,两个子集不一定包含a中所有元素,即可以有元素不在两个子集中。求满足这样条件的最大子集的和,不存在返回0。
例如,a=[1,2,2,2,3,4,6],子集[1,2,3,4]和[2,2,6]为满足条件的两个子集,则返回10。
a=[1,4,5,6],子集[1,5]和[6]满足条件,返回6。


题解代码:

import java.util.Arrays;

public class equalSubSet {
	static int maxSum = 1000;
	static boolean [][] dp = new boolean [maxSum][maxSum];

	public static void main(String[] args) {
		dp[0][0] = true;
		int [] nums = {1,2,2,2,3,4,6};
		for(int i = 0; i < nums.length; ++i){
			equalSubset(nums[i]);
		}
		for(int i = maxSum - 1; i >= 0; --i){
			if(dp[i][i]){
				System.out.println(i);
				break;
			}
		}
	}
	
	static void equalSubset(int num){
		
		for(int i = maxSum - 1; i >= 0; --i){
			for(int k = maxSum - 1; k >= 0; --k){
				if(k >= num && dp[i][k - num])
					dp[i][k] = true;
				if(i >= num && dp[i - num][k])
					dp[i][k] = true;
			}
		}
	}
}

理解:dp[i][j]为true时,表示第一个集合里元素相加的和为i,第二个集合中的元素相加和为j的情况成立。

有两种情形:第一种是新来的num可以加到第一个集合里,第二种情形是新来的num可以加到第二个集合里。

加到第一个集合里变成dp[i][j]成立的话,需要dp[i-num][j]成立,反之亦然。

最后只要在dp的对角线上从大向小去找到第一个为true的值就可以了。

因为题目中给出了大小的限制,所以可以直接申请一个1000*1000的数组,大小尚可接受。

时间复杂度1000*1000*50。


以后类似的这种组合性质的题目除了考虑强递归dfs剪枝,也可以向背包的思路上思考。

展开阅读全文

没有更多推荐了,返回首页