剑指Offer题目笔记30(动态规划背包问题)

本文解析了四个编程面试题,涉及动态规划在背包问题中的应用,如0-1背包、无界背包(完全背包)以及计数特定和的排列,展示了如何通过递归和迭代方式解决此类问题,并讨论了优化空间效率的方法。
摘要由CSDN通过智能技术生成

面试题101:

面试题101

问题:

​ 给定一个非空的正整数数组,判断能否将这些数字分成和相等的两部分。

解决方案:
  1. 如果有n个物品,每步判断一个物品是否要放入背包,也就是说解决这个问题需要n步,并且每步都面临放入或不放入两个选择,这看起来是一个能用回溯法解决的问题。但这个题目没有要求列出所有可能的放满背包的方法,而是只要求判断是否存在放满背包的方法,也就是判断方法的数量是否大于0。因此,这个问题更适合用动态规划解决。
  2. 用函数f(i,j)表示能否从前i个物品(物品标号分别为0,1,…,i-1)中选择若干物品放满容量为j的背包。如果总共有n个物品,背包的容量为t,那么f(n,t)就是问题的解。
  3. 当j等于0时,即背包的容量为0,不论有多少个物品,只要什么物品都不选择,就能使选中的物品的总重量为0,因此f(i,0)都为true。当i等于0时,即物品的数量为0,肯定无法用0个物品来放满容量大于0的背包,因此当j大于0时f(0,j)都为false。
  4. 当判断能否从前i个物品中选择若干物品放满容量为j的背包时,对标号为i-1的物品有两个选择。一个选择是将标号为i-1的物品放入背包中,如果能从前i-1个物品(物品标号分别为0,1,…,i-2)中选择若干物品放满容量为j-nums[i-1]的背包(即f(i-1,j-nums[i-1])为true),那么f(i,j)就为true。另一个选择是不将标号为i-1的物品放入背包中,如果从前i-1个物品中选择若干物品放满容量为j的背包(即f(i-1,j)为true),那么f(i,j)也为true。
源代码(递归):
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num:nums){
            sum += num;
        }

        if(sum % 2 == 1){
            return false;
        }

        return subsetSum(nums,sum/2);
    }

    private boolean subsetSum(int[] nums,int target){
        Boolean[][] dp = new Boolean[nums.length+1][target+1];
        return dfs(nums,dp,nums.length,target);
    }
    //i表示物品数量,j表示背包容量
    private boolean dfs(int[] nums,Boolean[][] dp,int i,int j){
        if(dp[i][j] == null){
            //当背包容量为0时,不选择任何物品就可以将背包填满,故dp[i][j] = true
            if(j == 0){
                dp[i][j] = true;
            //当物品数量为0时,没有物品了还怎么填满背包,故dp[i][j] = false
            }else if(i == 0){
                dp[i][j] = false;
            }else{
                //情况一:不取该物品
                dp[i][j] = dfs(nums,dp,i-1,j);
                //情况二:取该物品,但是背包容量必须大于等于该物品重量
                if(!dp[i][j] && j >= nums[i-1]){
                    dp[i][j] = dfs(nums,dp,i-1,j-nums[i-1]);
                }
            }
        }

        return dp[i][j];
    }
}
源代码(迭代):
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num:nums){
            sum += num;
        }

        if(sum % 2 == 1){
            return false;
        }

        return subsetSum(nums,sum/2);
    }

    private boolean subsetSum(int[] nums,int target){
        //i表示物品数量,j表示背包容量
        //因为这题所用数组与上题不太一样,Boolean数组的默认值是null,而boolean数组的默认值是false,因此不需要再对当物品数量为0时,没有物品了还怎么填满背包,故dp[i][j] = false的情况进行赋值
        boolean[][] dp = new boolean[nums.length+1][target+1];
       	//当背包容量为0时,不选择任何物品就可以将背包填满,故dp[i][j] = true
        for(int i = 0;i <= nums.length;i++){
            dp[i][0] = true;
        }

        for(int i = 1;i <= nums.length;i++){
            for(int j = 1;j <= target;j++){
                //情况一:不取该数字,背包容量不变,而且数字个数减一
                dp[i][j] = dp[i-1][j];
                //情况二:取该数字,背包容量必须大于等于该数字大小,背包容量减少该数字大小,而且数字个数减一
                if(!dp[i][j] && j >= nums[i-1]){
                    dp[i][j] = dp[i-1][j-nums[i-1]];
                }
            }
        }

        return dp[nums.length][target];
    }
}
优化空间效率思路:

​ 在计算f(i,j)的值时需要用到f(i-1,j)、f(i-1,j-nums[i-1])、因为f(i-1,j-nums[i-1])保存在f(i,j)之前,那么如果从左到右计算,那么会将f(i-1,j-nums[i-1])覆盖为f(i,j-nums[i-1]),所以只能从右到左计算。而且在f(i,j)计算完后就不需要用到了。

源代码:
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int num:nums){
            sum += num;
        }

        if(sum % 2 == 1){
            return false;
        }

        return subsetSum(nums,sum/2);
    }

    private boolean subsetSum(int[] nums,int target){
        //i表示物品数量,j表示背包容量
        boolean[] dp = new boolean[target+1];
        //当背包容量为0时,不选择任何物品就可以将背包填满,故dp[j] = true
        dp[0] = true;

        for(int i = 1;i <= nums.length;i++){
            for(int j = target;j > 0;j--){
                //因为压缩为一维数组了,dp[i][j]没赋值前就是dp[i-1][j],所以省去赋值操作
                if(!dp[j] && j >= nums[i-1]){
                    dp[j] = dp[j-nums[i-1]];
                }
            }
        }

        return dp[target];
    }
}

面试题102:

面试题102

问题:

​ 给定一个非空的正整数数组和一个目标值S,如果为每个数字添加“+”或“-”运算符,请计算有多少种方法可以使这些整数的计算结果为S

解决方案:
  1. 假设数组有n个,每一个数字都有两种情况+或-,看起来适合使用回溯法,但是题目没有要求我们求出每一种方法的情况,而是让我们求出总共有多少种方法,故使用动态规划。
  2. 为输入的数组中的有些数字添加“+”,有些数字添加“-”。如果所有添加“+”的数字之和为p,所有添加“-”的数字之和为q,按照题目的要求,p-q=S。如果累加数字中的所有数字,就能得到整个数组的数字之和,记为sum,即p+q=sum。将这两个等式的左右两边分别相加,就可以得到2p=S+sum,即p=(S+sum)/2。
  3. 用函数f(i,j)表示在数组的前i个数字(即nums[0…i-1])中选出若干数字使和等于j的方法的数目。如果数组的长度为n,目标和为t,那么 f(n,t)就是整个问题的解。
  4. 当j0时,也就是在i个数字选出n个数,使它们的和等于j,只要一个不选就等于0了,故f(i,0) = 1,当i0,j > 0时,当数字个数为0,不管怎么选它们的和都不可能大于0,故f(0,j) = 0,当i >0 并且 j > nums[i]时,f(i,j) = f(i-1,j) + f(i-1,j-nums[i])。
源代码:
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for(int num:nums){
            sum += num;
        }

        if(target < 0){
            target = -target;
        }

        if((sum + target) % 2 == 1 || sum < target){
            return 0;
        }
        target = (sum+target)/2 ;
        return targetSum(nums,target);
    }

    private int targetSum(int[] nums,int target){
        int[] dp = new int[target+1];
        dp[0] = 1;
        
		//因为f(i,j)依赖于f(i-1,j)、f(i-1,j-nums[i-1]),如果从左到右顺序f(i-1,j-nums[i-1])在f(i,j)前面计算会给f(i,j-nums[i-1])覆盖,故从右到左进行计算,又因为f(i-1,j)和f(i,j)保存在一起,但是还没计算完f(i,j)的时候,dp[j]保存的是f(i-1,j)的值,故使用累加:dp[j] += dp[j-num]。
        for(int num:nums){
            for(int j = target;j >= num;--j){     
                dp[j] += dp[j-num]; 
            }
        }

        return dp[target];
    }
}

面试题103:

面试题103

问题:

​ 给定正整数数组coins表示硬币的面额和一个目标总额t,请计算凑出总额t至少需要的硬币数目。

解决方案:
  1. 如果将每种面额的硬币看成一种物品,而将目标总额看成背包的容量,那么这个问题等价于求将背包放满时物品的最少件数。值得注意的是,这里每种面额的硬币可以使用任意多次,因此这个问题不再是0-1背包问题,而是一个无界背包问题(也叫完全背包问题)。
  2. 用函数f(i,j)表示用前i种硬币(coins[0,…,i-1])凑出总额为j需要的硬币的最少数目。当使用0枚标号为i-1的硬币时,f(i,j)等于 f(i-1,j)(用前i-1种硬币凑出总额j需要的最少硬币数目,再加上1枚标号为i-1的硬币);当使用1枚标号为i-1的硬币时,f(i,j)等于f(i-1,j-coins[i-1])加1(用前i-1种硬币凑出总额j-coins[i-1]需要的最少硬币数目,再加上1枚标号为i-1的硬币);以此类推,当使用k枚标号为i-1的硬币时,f(i,j)等于f(i-1,j-k×coins[i-1])加k(用前i-1种硬币凑出总额j-k×coins[i-1]需要的最少硬币数目,再加上k枚标号为i-1的硬币)。
源代码:
class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
        Arrays.fill(dp,amount+1);
        dp[0] = 0;

        for(int num:coins){
            for(int j = amount;j >= num;j--){
                for(int k = 1;k*num <= j;k++){
                    dp[j] = Math.min(dp[j],dp[j - k*num] + k);
                }
            }
        }

        return dp[amount] > amount?-1:dp[amount];
    }
}

面试题104:

面试题104

问题:

​ 给定一个非空的正整数数组nums和一个目标值t,数组中的所有数组都是唯一的,请计算数字之和等于t的所有排列的数目。

解决方案:
  1. 如果将每个数字看成一种物品,而将目标值看成背包的容量,那么这个问题等价于求将背包放满时物品的最少件数。值得注意的是,这里每个数字可以在排列中出现任意次,因此这个问题不再是0-1背包问题,而是一个无界背包问题(也叫完全背包问题)。
  2. 用f(i)表示和为i的排列的数目。为了得到和为i的排列,有如下选择:在和为i-nums[0]的排列中添加标号为0的数字,此时f(i)等于f(i-nums[0]);在和为i-nums[1]的排列中添加标号为1的数字,此时f(i)等于f(i-nums[1])。以此类推,在和为i-nums[n-1]的排列中添加标号为n-1的数字(n为数组的长度),此时f(i)等于 f(i-nums[n-1])。因为目标是求出所有和为i的排列的数目,所以将上述所有情况全部累加起来。
源代码:
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target+1]; 
        dp[0] = 1;

        for(int i = 1;i <= target;i++){
            for(int num:nums){
                if(i >= num){
                    dp[i] += dp[i - num]; 
                }
            }
        }

        return dp[target];
    }
}
  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值