京东笔试的时候有一道求幂的题目,看到讨论区有大佬用背包算法求解,回来复习复习背包问题。
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剪枝,也可以向背包的思路上思考。