*三种一维数组总结:**
01背包:
for (int i = 0; i < n; i++) {
for (int j = W; j >=w[i]; j--) //W背包总容量
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
完全背包:
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= W; j++)
f[j] = Math.max(f[j], f[j - w[i]] + v[i]);
return f[W]; // 最优解
多重背包
for(int i = 1; i <= N; i++){
int M = num[i-1], w = weights[i-1], v = values[i-1];
for(int j = W; j >= w; j--){ //01背包问题
for(int k = 0; k <= M && k * w < =j; k++){
dp[j] = Math.max(dp[j], dp[j-k*w]+k*v);
}
}
}
**有序的完全背包**
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-iv
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
看到这题时第一感觉是和零钱兑换那道题很像,但是比较蛋疼的是它求的是排列,我感觉要用DP做,但是死活想不明白这个排列问题怎么解决。比如nums=[1,2,3] target = 8,要计算能构成8的所有排列有几个,那就可以计算构成5的排列有几个,但是到这我想的是,这个3应该有几种方法插入到构成5的排列里呢?就卡在这了。然后看了别人的代码,一时又难以理解,想了半天想明白了。
在计算构成8的排列有几个时即dp[8],我们只关注分别以1,2,3为“屁股”的所有排列的个数,这仨排列个数之和就是构成8的所有排列了。为什么这么说呢?因为所有排列一定一定是以1,2,3其中一个为结尾的(废话),那么对于8而言,以3为结尾的排列个数就是dp[5],以2为结尾的排列个数就是dp[6],同理以
1为结尾的排列个数就是dp[7],把它仨加起来,就是dp[8]了。
这道题要注意一下初始化和overflow问题,初始化是应该dp[0]=1,详见代码注释。
零钱兑换问题中dp[i]为dp[i-nums[j]]的最小值,而这道题中dp[i]为dp[i-nums[j]]之和,这是它们的区别。
这道题真实教做人了,求解DP问题的关键在于你想赋予DP数组以怎样的含义,这个含义如果赋予的合
理,那DP问题就是面无表情敲代码了,比如这道题中,赋予的含义应该是“以nums[j]为结尾的排列个数”,而非“nums[j]参与运算的排列的个数”,以后者为含义的话,那可就麻烦太多了。不过还是需要注意一下初始状态的,这也算是个不小的难点。
代码
class Solution {
int totalNum=0;
public int combinationSum4(int[] nums, int target) {
int [] dp=new int [target+1];
dp[0]=1; 若num=j,以num为结尾,和为j的数目+1,
for(int j=1;j<=target;j++){
for(int num:nums){
if(j>=num){
dp[j]=dp[j]+dp[j-num]; //包含num和为j的+不包含num和为j-num的
}
}
}
return dp[target];
// dfs(nums,target,0,0);
// return totalNum;
}
// public void dfs (int []nums,int target,int start,int sum){
// if(sum>target){
// return ;
// }if(sum==target){
// totalNum++;
// }
// for(int i=0;i<nums.length;i++){ //有序性i从0开始,无序性从start开始
// sum+=nums[i];
// dfs(nums,target,i,sum); //可重复i,不可重复 i+1
// sum=sum-nums[i];
// }
// }
}
一维数组求最终所有的组合,leetcode39,每次循环数组前i个数的和为j的结果用map存起来,后面再碰到和为j的,并进集合
Map< Integer, List< List< Integer>>> map =new HashMap< Integer, List< List< Integer>>>();
List< List< Integer>> listfor0=new ArrayList< List< Integer>>();
listfor0.add(new ArrayList());
map.put(0,listfor0);
for(int candidate:candidates){
for(int j=candidate;j<=target;j++){
// List< List< Integer>> tempList=new ArrayList< List< Integer>>();
if(map.get(j-candidate)!=null){
List< List< Integer>> beforeList =map.get(j-candidate);
List< List< Integer>> newList =new ArrayList< List< Integer>>();
for(int k=0;k< beforeList.size();k++){
List< Integer> subList= new ArrayList< Integer>();
subList.addAll(beforeList.get(k));
subList.add(candidate);
newList.add(subList);
}
if(map.get(j)!=null){
List< List< Integer>> beforeList2 =map.get(j);
beforeList2.addAll(newList);
}else{
map.put(j,newList);
}
}
}
}
https://leetcode-cn.com/problems/combination-sum/submissions/
*三种二维数组总结:**
01背包
for(int i=1;i<=num;i++){
for(int j=0;j<=target;j++){
if(j>=weight[i-1]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i-1]]+val[i-1]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
完全背包:
for(int i=1;i<=num;i++){
for(int j=0;j<=target;j++){
if(j>=weight[i-1]){
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-weight[i-1]]+val[i-1]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
多重背包
for(int i=1;i<=num;i++){
for(int j=0;j<=target;j++){
if(j>=weight[i-1]){
//考虑物品的件数限制
int maxV = Math.min(num[i-1],j/weight[i-1]);
for(int k=0;i<=maxV;k++){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-kweight[i-1]]+kval[i-1])
}
}else{
dp[i][j]=dp[i-1][j];
}
}
}
希望用一种规律搞定背包问题
解题思路
常见的背包问题有1、组合问题。2、True、False问题。3、最大最小问题。
以下题目整理来自大神CyC,github地址:
github
我在大神整理的基础上,又做了细分的整理。分为三类。
1、组合问题:
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
class Solution {
int totalNum=0;
public int combinationSum4(int[] nums, int target) {
int [] dp=new int [target+1];
dp[0]=1;
for(int j=1;j<=target;j++){
for(int num:nums){
if(j>=num){
dp[j]=dp[j]+dp[j-num];
}
}
}
return dp[target];
// dfs(nums,target,0,0);
// return totalNum;
}
// public void dfs (int []nums,int target,int start,int sum){
// if(sum>target){
// return ;
// }if(sum==target){
// totalNum++;
// }
// for(int i=0;i< nums.length;i++){ //有序性i从0开始,无序性从start开始
// sum+=nums[i];
// dfs(nums,target,i,sum); //可重复i,不可重复 i+1
// sum=sum-nums[i];
// }
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1"
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
class Solution {
int totalNum;
public int findTargetSumWays(int[] nums, int S) {
int sum=0;
for(int i=0;i< nums.length;i++){
sum+=nums[i];
}
//如果S大于sum,无法得到
if(Math.abs(S) > Math.abs(sum)) return 0;
int target=(sum+S)/2;
if((sum+S)%2==1){
return 0;
}
// int result[]=new int[target+1];
// result[0]=1;
// for(int i=0;i< nums.length;i++){
// for(int j=target;j>=nums[i];j--){
// result[j]=result[j]+result[j-nums[i]]; //result[i]是不包含当前数组,上一轮循环中的,result[j-nums[i])为包含当前数组
// }
// }
// return result[target];
dfs(nums,0,0,target);
return totalNum;
}
public void dfs(int[]nums,int start,int sum,int target){
if(sum==target){
totalNum++;
}if(sum>target){
return;
}
for(int i=start;i< nums.length;i++){
sum+=nums[i];
dfs(nums,i+1,sum,target);
sum-=nums[i];
}
}
518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
class Solution {
public int total=0;
public int change(int amount, int[] coins) {
int [] dp=new int[amount+1];
dp[0]=1;
for(int i=0;i< coins.length;i++){
for(int j=0;j<=amount;j++){
if(j>=coins[i]){
dp[j]=dp[j]+dp[j-coins[i]];
}
}
}
return dp[amount];
// dfs(amount,coins,0,0);
// return total;
}
// public void dfs(int amount,int[] coins, int sum,int start){
// if(sum>amount){
// return;
// }
// if(sum==amount){
// total++;
// return;
// }
// for(int i=start;i< coins.length;i++){
// sum+=coins[i];
// dfs(amount,coins,sum,i);
// sum-=coins[i];
// }
// }
}
leetcode39组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
class Solution {
static List< List> resultList=new ArrayList< List>();
public static List< List< Integer>> combinationSum(int[] candidates, int target) {
if(candidates==null){
return new ArrayList< List>();
}
Arrays.sort(candidates);
if(candidates[0]> target){
return new ArrayList< List< Integer>>();
} // dfs(candidates,new ArrayList< Integer>(),target,0,0);
Map< Integer, List< List< Integer>>> map =new HashMap< Integer, List< List>>();
List< List> listfor0=new ArrayList< List< Integer>>();
listfor0.add(new ArrayList());
map.put(0,listfor0);
for(int candidate:candidates){
for(int j=candidate;j<=target;j++){
// List< List> tempList=new ArrayList< List>();
if(map.get(j-candidate)!=null){
List< List> beforeList =map.get(j-candidate);
List< List> newList =new ArrayList< List>();
for(int k=0;k< beforeList.size();k++){
List< Integer> subList= new ArrayList< Integer>();
subList.addAll(beforeList.get(k));
subList.add(candidate);
newList.add(subList);
}
if(map.get(j)!=null){
List< List> beforeList2 =map.get(j);
beforeList2.addAll(newList);
}else{
map.put(j,newList);
}
}
}
}
return map.get(target)==null?new ArrayList< List>():map.get(target)
// return resultList;
// public static void dfs(int[] candidates,List< Integer>subList, int target,int start,int sum){
// if(sum>target){
// return ;
// }
// if (sum==target){
// resultList.add(new ArrayList<>(subList)) ;
// return;
// }
// for(int i=start;i<candidates.length;i++){
// int numI=candidates[i];
// subList.add(numI);
// sum=numI+sum;
// dfs(candidates,subList,target,i,sum);
// sum=sum-numI;
// subList.remove(subList.size()-1);
// }
// }
}
leetcode40组合总和||
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。 class Solution { List< List< Integer>> resultList=new ArrayList< List< Integer >> (); public List< List> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates); Map< Integer ,List< List >> map=new HashMap< Integer ,List< List< Integer>>>(); List< List> listFor0=new ArrayList< List>(); List < Integer> listFor01=new ArrayList< Integer>(); listFor0.add(listFor01); map.put(0,listFor0); for(int i=0;i< candidates.length;i++){ if((i< candidates.length-1)&&(candidates[i]==candidates[i+1]))continue; for(int j=target;j>=candidates[i];j--){ if(map.get(j-candidates[i])!=null){ List< List> listBefore=map.get(j-candidates[i]); List< List> listNew=new ArrayList< List> (); for(List< Integer> list: listBefore){ List< Integer> list1=new ArrayList< Integer>(list); list1.add(candidates[i]); listNew.add(list1); } if(map.get(j)==null){ map.put(j,listNew); }else{ List< List> listBefore2=map.get(j); listBefore2.addAll(listNew); } } } } return map.get(target); // dfs(candidates,target,0,0,new ArrayList ()); // return resultList; } // public void dfs(int[] candidates, int target,int start,int sum,List subList){ // if(sum>target){ // return; // } if(sum==target){ // resultList.add(new ArrayList(subList)); // } // for(int i=start;i< candidates.length;i++){ // if(i>start&&candidates[i]==candidates[i-1]) continue; //i>start,让横向不重复,纵向可以重复 // sum+=candidates[i]; // subList.add(candidates[i]); // dfs(candidates,target,i+1,sum,subList); // sum-=candidates[i]; // subList.remove(subList.size()-1); // } // } }
2、True、False问题:
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
class Solution {
public boolean result;
public boolean wordBreak(String s, List wordDict) {
boolean [] dp =new boolean[s.length()+1];
dp[0]=true;
for(int j=1;j<=s.length();j++){
for(String subString :wordDict){
int length=subString.length();
if(j>=length&&subString.equals(s.substring(j-length,j))){ //以subString结尾的字符串能否匹配
dp[j]=dp[j]||dp[j-length];
}
}
}
return dp[s.length()];
// dfs(s,wordDict,"");
// return result;
}
// public void dfs(String s, List wordDict,String sumString){
// if(sumString.length()>s.length()){
// return;
// }
// if(sumString.length()==s.length()&&sumString.equals(s)){
// result=true;
// return;
// }
// for(int i=0;i< wordDict.size();i++){
// sumString+=wordDict.get(i);
// dfs(s,wordDict,sumString);
// sumString=sumString.substring(0,sumString.length()-wordDict.get(i).length());
// }
// }
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
boolean result=false;
public boolean canPartition(int[] nums) {
int N=nums.length;
int Sum=0,i,j;
for(i=0;i< N;i++)
{
Sum+=nums[i];
}
if(Sum%2!=0)
return false;
else {
int target=Sum/2;
boolean [] dp=new boolean[target+1];
dp[0]=true;
for(int num:nums){
for( j=target;j>=num;j--){
dp[j]=dp[j]||dp[j-num];
}
}
return dp[target];
// dfs(nums,0,0,target);
// return result;
}
}
// public void dfs(int []nums,int start,int sum,int target){
// if(sum==target){
// result=true;
// return;
// } if(sum>target){
// return;
// }
// for(int i=start;i< nums.length;i++){
// sum+=nums[i];
// dfs(nums,i+1,sum,target);
// sum-=nums[i];
// }
// }
}
3、最大最小问题:
474. 一和零
public int findMaxForm(String[] strs, int m, int n) {
if (strs == null || strs.length <= 0) {
return 0;
}
// dp[i][j] 表示 当前数组中,最多有 i个0、j个1 的组合数
int[][] dp = new int[m + 1][n + 1];
int oneNum;
int zeroNum;
for (String str : strs) {
oneNum = 0;
zeroNum = 0;
/*
记录 当前字符串 中的 0/1个数
*/
char[] chars = str.toCharArray();
for (char aChar : chars) {
if (aChar == '0') {
zeroNum++;
} else {
oneNum++;
}
}
/*
dp[i][j] 的结果有两种情况:
1、当前状态(dp[i][j])
2、上一个状态(dp[i - zeroNum][j - oneNum])的个数 + 1
*/
for (int i = m; i >= zeroNum; i--) {
for (int j = n; j >= oneNum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
for (int i = m; i >= 0; i--) {
for (int j = n; j >= 0; j--) {
System.out.print(dp[i][j]);
}
System.out.println();
}
}
return dp[m][n];
}
定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
public int coinChange(int[] coins, int amount) {
int [] dp=new int [amount+1];
for(int j=0;j<=amount;j++){
dp[j]=Integer.MAX_VALUE-1;
}
dp[0]=0; 和为coin,切i=coin,包含coin时 dp[i-coin]+1 为1个
for(int coin:coins){
for(int i=coin;i<=amount;i++){
dp[i]=Math.min(dp[i],dp[i-coin]+1);
}
}
return dp[amount]
组合问题公式
dp[i] += dp[i-num]
True、False问题公式
dp[i] = dp[i] or dp[i-num]
最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
以上三组公式是解决对应问题的核心公式。
当然拿到问题后,需要做到以下几个步骤:
1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。
接下来讲一下背包问题的判定
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。
背包问题技巧:
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
for num in nums:
for i in range(target, nums-1, -1):
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
3.如果组合问题需考虑元素之间的顺序,需将target放在外循环,将nums放在内循环。
for i in range(1, target+1):
for num in nums:
代码
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
if not nums:
return 0
dp = [0] * (target+1)
dp[0] = 1
for i in range(1,target+1):
for num in nums:
if i >= num:
dp[i] += dp[i-num]
return dp[target]
以上是我对背包问题的总结,希望对你有帮助。要是觉得不错,点个赞吧。
一 01背包二维数组详解:**
适用动态规划的问题必须满足最优化原理、无后效性和重叠性。
a.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
b.无后效性 将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
c.子问题的重叠性 动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的算法,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
1.问题描述
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
为方便讲解和理解,下面讲述的例子均先用具体的数字代入,即:eg:number=4,capacity=8
i(物品编号) | 1 | 2 | 3 | 4 |
w(体积) | 2 | 3 | 4 | 5 |
v(价值) | 3 | 4 | 5 | 6 |
2.总体思路
根据动态规划解题步骤(问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成)找出01背包问题的最优解以及解组成,然后编写代码实现。
3.动态规划的原理
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
4.背包问题的解决过程
在解决问题之前,为描述方便,首先定义一些变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合 对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
1、建立模型,即求max(V1X1+V2X2+…+VnXn);
2、寻找约束条件,W1X1+W2X2+…+WnXn<capacity;
3、寻找递推关系式,面对当前商品有两种可能性:
- 包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
- 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i),但价值增加了v(i);
由此可以得出递推关系式:
(1)j<w(i), V(i,j)=V(i-1,j)
(2)j>=w(i), V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)} 其中V(i-1,j)表示装的下,却不装
(1)式表明:如果第i个物品的重量大于背包的容量,则装人前i个物品得到的最大价值和装入前i-1个物品得到的最大价是相同的,即物品i不能装入背包;
第(2)个式子表明:如果第i个物品的重量小于背包的容量,则会有一下两种情况:(a)如果把第i个物品装入背包,则背包物品的价值等于第i-1个物品装入容量位j-wi 的背包中的价值加上第i个物品的价值vi; (b)如果第i个物品没有装入背包,则背包中物品价值就等于把前i-1个物品装入容量为j的背包中所取得的价值。显然,取二者中价值最大的,作为把前i个物品装入容量为j的背包中的最优解。
这里需要解释一下,为什么背包容量足够的情况下,还需要 V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}?
V(i-1,j-w(i))+v(i) 表示装了第i个物品后背包中的最大价值,所以当前背包容量 j 中,必定有w(i)个容量给了第i个背包。
因此只剩余j-w(i)个容量用来装,除了第i件物品的其他所有物品。
V(i-1,j-w(i))是前i-1个物品装入容量为j-w(i)的背包中最大价值。
注意,这里有一个问题。前i-1个物品装入容量为j-w(i)的背包中最大价值+物品i的价值。可能不如将,前i-1个物品装入容量为j的背包中得到的价值大。也就是说,可能出现 V(i-1,j) > (V(i-1,j-w(i))+v(i))
比如说,将第i个物品放入背包,可能会导致前面更有价值的物品放不进背包。因此,还不如不把第i个物品放进去,把空间让出来,从而能让前i-1个物品中更有价值的物品能够放入背包。从而让V(i,j)取得最大的值。
所以我们需要 max{V(i-1,j),V(i-1,j-w(i))+v(i)},来作为把前i个物品装入容量为j的背包中的最优解。
4、填表,首先初始化边界条件,V(0,j)=V(i,0)=0;
然后一行一行的填表:
- 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
- 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
- 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10……
所以填完表如下图:
5、表格填完,最优解即是V(number,capacity)=V(4,8)=10。(也就是说,在有4个可选物品,背包容量为8的情况下,能装入的最大价值为10)。
5.背包问题最优解回溯(求解 这个最优解由哪些商品组成?)
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
- V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
- V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
- 一直遍历到i=0结束为止,所有解的组成都会找到。
就拿上面的例子来说吧:
- 最优解为V(4,8)=10,而V(4,8)!=V(3,8)却有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被选中,并且回到V(3,8-w(4))=V(3,3);
- 有V(3,3)=V(2,3)=4,所以第3件商品没被选择,回到V(2,3);
- 而V(2,3)!=V(1,3)却有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被选中,并且回到V(1,3-w(2))=V(1,0);
- 有V(1,0)=V(0,0)=0,所以第1件商品没被选择。
动态规划法求解0/1背包问题:
1)基本思想:
令表示在前个物品中能够装入容量为的背包中的物品的最大值,则可以得到如下动态函数:
代码实现
为了和之前的动态规划图可以进行对比,尽管只有4个商品,但是我们创建的数组长度为5。。
import java.util.*;public class DynamicProgramming {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
/* 1.读取数据 */
int number = sc.nextInt(); // 物品的数量
// 注意:我们声明数组的长度为"n+1",并另score[0]和time[0]等于0。
// 从而使得 数组的下标,对应于题目的序号。即score[1]对应于第一题的分数,time[1]对应于第一题的时间
int[] weight = new int[number + 1]; // {0,2,3,4,5} 每个物品对应的重量
int[] value = new int[number + 1]; // {0,3,4,5,6} 每个物品对应的价值
weight[0] = 0;
for (int i = 1; i < number + 1; i++) {
weight[i] = sc.nextInt();
}
value[0] = 0;
for (int i = 1; i < number + 1; i++) {
value[i] = sc.nextInt();
}
int capacity = sc.nextInt(); // 背包容量
/* 2.求解01背包问题 */
int[][] v = new int[number + 1][capacity + 1];// 声明动态规划表.其中v[i][j]对应于:当前有i个物品可选,并且当前背包的容量为j时,我们能得到的最大价值
// 填动态规划表。当前有i个物品可选,并且当前背包的容量为j。
for (int i = 0; i < number + 1; i++) {
for (int j = 0; j < capacity + 1; j++) {
if (i == 0) {
v[i][j] = 0; // 边界情况:若只有0道题目可以选做,那只能得到0分。所以令V(0,j)=0
} else if (j == 0) {
v[i][j] = 0; // 边界情况:若只有0分钟的考试时间,那也只能得0分。所以令V(i,0)=0
} else {
if (j < weight[i]) {
v[i][j] = v[i - 1][j];// 包的容量比当前该物品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
} else {
v[i][j] = Math.max(v[i - 1][j], v[i - 1][j - weight[i]] + value[i]);// 还有足够的容量可以装当前该物品,但装了当前物品也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
}
}
}
}
System.out.println();
System.out.println("动态规划表如下:");
for (int i = 0; i < number + 1; i++) {
for (int j = 0; j < capacity + 1; j++) {
System.out.print(v[i][j] + "\t");
}
System.out.println();
}
System.out.println("背包内最大的物品价值总和为:" + v[number][capacity]);// 有number个物品可选,且背包的容量为capacity的情况下,能装入背包的最大价值
/* 3.价值最大时,包内装入了哪些物品? */
int[] item = new int[number + 1];// 下标i对应的物品若被选中,设置值为1
Arrays.fill(item, 0);// 将数组item的所有元素初始化为0
// 从最优解,倒推回去找
int j = capacity;
for (int i = number; i > 0; i--) {
if (v[i][j] > v[i - 1][j]) {// 在最优解中,v[i][j]>v[i-1][j]说明选择了第i个商品
item[i] = 1;
j = j - weight[i];
}
}
System.out.print("包内物品的编号为:");
for (int i = 0; i < number + 1; i++) {
if (item[i] == 1) {
System.out.print(i + " ");
}
}
System.out.println("----------------------------");
}
}
}
测试结果:
4
2 3 4 5
3 4 5 6
8动态规划表如下:
0 0 0 0 0 0 0 0 0
0 0 3 3 3 3 3 3 3
0 0 3 4 4 7 7 7 7
0 0 3 4 5 7 8 9 9
0 0 3 4 5 7 8 9 10
背包内最大的物品价值总和为:10
包内物品的编号为:2 4
----------------------------
5
2 2 3 5 1
5 4 3 5 2
10动态规划表如下:
0 0 0 0 0 0 0 0 0 0 0
0 0 5 5 5 5 5 5 5 5 5
0 0 5 5 9 9 9 9 9 9 9
0 0 5 5 9 9 9 12 12 12 12
0 0 5 5 9 9 9 12 12 14 14
0 2 5 7 9 11 11 12 14 14 16
背包内最大的物品价值总和为:16
包内物品的编号为:1 2 4 5
----------------------------
参考文献
https://blog.csdn.net/qq_38410730/article/details/81667885
https://www.cnblogs.com/xym4869/p/8513801.html
https://www.cnblogs.com/variance/p/6909560.html
看了好几天的背包问题。。。终于有了一点浅显的理解
一开始学完01背包的二维写法,再看一维写法是一脸懵逼的,自己推导了几遍过程,终于是理解了!!!分享一下蒟蒻的心得,背包问题可以去看一下胡凡的算法笔记。
问题如下:有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。–来自《挑战程序设计竞赛》
输入:n=4 (w,v)={(2,3),(1,2),(3,4),(2,2)}
输出:7
最大负重为5
如果学过01背包的二维数组写法,那么应该会发现若是递推顺序是正序,那么我们需要用到的是左上方和正上方的数据,如下图
当计算dp[i+1][]时,dp[i-1][]的部分就用不到了。
那么用一维数组又是怎么做的?我们是把上面讲过的左上方和正上方的部分结合起来,从后往前推导
从第一个物品开始,在前面更新的数组基础上继续去更新DP数组的内容,直到没有物品为止。
核心代码如下:
for (int i = 0; i < 4; i++) {
for (int j = bag_w; j >=w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
- 1
- 2
- 3
bag_w为重量上限
推导过程如下:
dp[]右边是一维数组下标
举个例子,在枚举完i=0的情况时,i=1时使用的是上一次更新完的数组,
dp[j]=max(dp[j],dp[j-w[i]]);等号右边的dp[j]是上一次更新的,即使一维数组里面的dp[j],dp[j-w[i]]相当于二维数组的dp[i-1][j],dp[i-1][j-w[i]],这样,我们就把二维数组进行了降维。
比如说数塔问题,就是一行一行地去更新,也就是在一维数组里面不断滚动
可以试着去推导,如果是正序的话,同一个物品可能会多次使用,
例如,i=0时,dp[2]=3,dp[3]=3,dp[4]=6,注意了,这里dp[4]=max(dp[4],dp[4-w[0]+v[0])=6;可以看到,在dp[2]的基础上,又放入了物品1,这就变成了完全背包而不是01背包了。
切记,一维必须是逆序的,而二维可以正序也可以逆序。
二:完全背包问题
完全背包问题
/**
* 完全背包
* 思路分析:
* 注意这里当考虑放入一个物品 i 时应当考虑还可能继续放入 i,
* 因此这里是dp[i][j-weight[i]]+value[i], 而不是dp[i-1][j-weight[i]]+value[i]。
* 放第i件物品。这里的处理和01背包有所不同,因为01背包的每个物品只能选择一个
* *因此选择放第i件物品就意味着必须转移到dp[i-1][v-w[i]]这个状态;但是完全背包
*问题不同,完全背包如果选择放第i件物品之后并不是转移到dp[i-1][v-w[i]]这个状态,
*而是转移到dp[i][v-w[i]],这是因为每种物品可以放任意件(注意有容量的限制,因此
*还是有限的),放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保
*持大于等于0为止。
* @param V
* @param N
* @param weight
* @param value
* @return
*/
public static String completePack(int V,int N,int[] weight,int[] value){
//初始化动态规划数组
int[][] dp = new int[N+1][V+1];
//为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
for(int i=1;i<N+1;i++){
for(int j=1;j<V+1;j++){
//如果第i件物品的重量大于背包容量j,则不装入背包
//由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
if(weight[i-1] > j)
dp[i][j] = dp[i-1][j];
else
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-weight[i-1]]+value[i-1]);
}
}
//则容量为V的背包能够装入物品的最大值为
int maxValue = dp[N][V];
int j=V;
String numStr="";
for(int i=N;i>0;i–){
//若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的
while(dp[i][j]>dp[i-1][j]){ //此处while 区别与01背包中的if while循环次数,就是i的个数
numStr = i+" "+numStr;
j=j-weight[i-1];
}
if(j==0)
break;
}
return numStr;
}
完全背包一维数组解法:
设f[j]表示重量不超过j公斤的最大价值 可得出状态转移方程 :
f[j]=max{f[j],f[j−w[i]]+v[i]}
区别:0,1背包是从大到小遍历,完全背包是从小到大遍历
代码如下:
// 完全背包:一维法-倒序
public static int bag3(int W, int[] w, int[] v) {
int n = w.length - 1;// 第一个值,不算
int[] f = new int[W + 1];
for (int i = 1; i <= n; i++)
for (int j = W; j >= w[i]; j--)
for (int k = 0; j - k*w[i] >=0; k++)
f[j] = Math.max(f[j], f[j - k*w[i]] +k* v[i]);
return f[W]; // 最优解
}
想必大家看出了和01背包的区别,这里的内循环是顺序的,而01背包是逆序的。
现在关键的是考虑:为何完全背包可以这么写?
在次我们先来回忆下,01背包逆序的原因?是为了是max中的两项是前一状态值,这就对了。 那么这里,我们
顺序写,这里的max中的两项当然就是当前状态的值了,为何? 因为每种背包都是无限的。当我们把i从1到N循
环时,f[v]表示容量为v在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我
们要考虑的当然是当前状态。
// 完全背包:一维法-正序
public static int bag3(int W, int[] w, int[] v) {
int n = w.length - 1;// 第一个值,不算
int[] f = new int[W + 1];
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= W; j++)
f[j] = Math.max(f[j], f[j - w[i]] + v[i]);
return f[W]; // 最优解
}
三:多重背包
题目
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则有状态转移方程:
f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k<=n[i]}
java代码实现如下:
/**
* 第三类背包:多重背包
*
* @param args
* @param V 背包容量
* @param N 物品种类
*/
public static int manyPack(int V,int N,int[] weight,int[] value,int[] num){
//初始化动态规划数组
int[][] dp = new int[N+1][V+1];
//为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
for(int i=1;i<N+1;i++){
for(int j=1;j<V+1;j++){
//如果第i件物品的重量大于背包容量j,则不装入背包
//由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
if(weight[i-1] > j)
dp[i][j] = dp[i-1][j];
else{
//考虑物品的件数限制
int maxV = Math.min(num[i-1],j/weight[i-1]);
for(int k=0;k<maxV+1;k++){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-k*weight[i-1]]+k*value[i-1]);
}
}
}
}
/*//则容量为V的背包能够装入物品的最大值为
int maxValue = dp[N][V];
int j=V;
String numStr="";
for(int i=N;i>0;i--){
//若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的
while(dp[i][j]>dp[i-1][j]){
numStr = i+" "+numStr;
j=j-weight[i-1];
}
if(j==0)
break;
}*/
return dp[N][V];
}
**多重背包一维数组代码:**
//N为物品数目
//W为背包容量
//num[]为每个物品的数目
//weights[]为每个物品的体积
//values[]为每个物品的价值
public int multiKnapack(int N, int W, int[] num, int[] weights, int[] values){
int[] dp = new int[W+1];
for(int i = 1; i <= N; i++){
int M = num[i-1], w = weights[i-1], v = values[i-1];
for(int j = W; j >= w; j--){
for(int k = 0; k <= M && k * w < =j; k++){
dp[j] = Math.max(dp[j], dp[j-k*w]+k*v);
}
}
}
return dp[W];
}