动态规划 背包

目标和

LeetCode 494
对每一个数字都可以进行 + 操作 或者 - 操作,实际上最后就是一个树的搜索问题。
使用深度优先进行遍历,需要记录当前访问的是第几个元素,当前的结果。
递归结束的条件:
1)当前位置超过数组的边界
2)当前和等于目标和,记录结果

    class Solution {
        List<String> res = new ArrayList<>();

        public int findTargetSumWays(int[] nums, int S) {
            if (nums == null || nums.length == 0) {
                return 0;
            }
            int num =  dfs(nums, S, 0, 0, new StringBuilder());
            for(String s :res){
                System.out.println(s);
            }

            return num;
        }
		/**
		*使用回溯,记录了每次操作的路径
		*/
        private int dfs(int[] nums, int target, int index, int curSum, StringBuilder sb) {
            if (index == nums.length) {
                if(curSum == target){

                    res.add(sb.toString());
                    return 1;
                }
                return  0;
            }
            String tmp = "+" + nums[index];
            sb.append(tmp);
            int left = dfs(nums, target, index + 1, curSum + nums[index], sb);
            sb.delete(sb.length() - tmp.length(), sb.length());
            tmp = "-" + nums[index];
            sb.append(tmp);
            int right = dfs(nums, target, index + 1, curSum - nums[index], sb);
            sb.delete(sb.length() - tmp.length(), sb.length());
            return left + right;
        }
    }

输出结果:
在这里插入图片描述

分割子集

LeetCode 416
0/1背包问题的变种
实际上就是要在数组中选出若干元素,使得它们的和等于数组所有元素和的一半。

分析

如果数组元素总和为奇数,不能进行划分。
为偶数则求是否存在元素之和为总和一半

dp[i][j] 表示 在前i件物品中进行选择,每个物品最多选择一次,是否能够凑出j
初始化:
有两种形式:

  1. int[][] dp = new int[len][target+1];
    那么对第i件物品进行选择 下标就是i
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]]

初始化的值,应该用第1件物品进行初始化,dp[0][nums[0]] = true
对应的返回值 dp[len - 1][target];

  1. int[][] dp = new int[len+1][target+1];
    那么对第i件物品进行选择 下标就是i-1
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]]

初始化的值,应该用所有物品都不选进行初始化,dp[0][0] = true
对应的返回值是dp[len][target]

    class Solution {
        public boolean canPartition1(int[] nums) {
            int sum = 0;
            int len = nums.length;
            for (int i = 0; i < nums.length; i++) {
                sum += nums[i];
            }
            if (sum % 2 != 0) {
                return false;
            }
            int target = sum / 2;
            boolean dp[][] = new boolean[len][target + 1];
            //dp[i][j] 第 i件物品不选  = dp[i-1][j]
            //第i件物品选 =dp[i][j-nums[i-1][]
            if (nums[0] <= target) {
                dp[0][nums[0]] = true;
            }

            for (int i = 1; i < len; i++) {
                for (int j = 0; j <= target; j++) {
                    dp[i][j] = dp[i - 1][j];
                    boolean yes = j >= nums[i] ? dp[i - 1][j - nums[i]] : false;
                    dp[i][j] |= yes;
                }
            }
            return dp[len - 1][target];
        }

        public boolean canPartition2(int[] nums) {
            int sum = 0;
            int len = nums.length;
            for (int i = 0; i < nums.length; i++) {
                sum += nums[i];
            }
            if (sum % 2 != 0) {
                return false;
            }
            int target = sum / 2;
            boolean dp[] = new boolean[target + 1];


            //dp[i][j] 第 i件物品不选  = dp[i-1][j]
            //第i件物品选 =dp[i][j-nums[i-1][]
            dp[nums[0]] = true;
            for (int i = 1; i < len; i++) {
                for (int j = target; j >= 0; j--) {
                    boolean yes = j >= nums[i] ? dp[j - nums[i]] : false;
                    dp[j] |= yes;
                }
            }
            return dp[target];
        }


        public boolean canPartition(int[] nums) {
            int sum = 0;
            int len = nums.length;
            for (int i = 0; i < nums.length; i++) {
                sum += nums[i];
            }
            if (sum % 2 != 0) {
                return false;
            }
            int target = sum / 2;
            boolean dp[][] = new boolean[len + 1][target + 1];
            //dp[i][j] 第 i件物品不选  = dp[i-1][j]
            //第i件物品选 =dp[i][j-nums[i-1][]

            dp[0][0] = true;
            // 这里 i能够取值取到len
            for (int i = 1; i <= len; i++) {
                for (int j = 0; j <= target; j++) {
                    dp[i][j] = dp[i - 1][j];
                    boolean yes = j >= nums[i - 1] ? dp[i - 1][j - nums[i - 1]] : false;
                    dp[i][j] |= yes;
                }
            }
            return dp[len][target];
        }
    }

1和0

Leetcode 474

每个字符串都会贡献 a个0,b个1,求最多贡献m个0,n个1的最多的字符串个数
背包问题转换,背包容量 从一维的体积,变成两维度的约束,0,1的个数。

    class Solution {
        /***
         * f[k][i][j] 代表考虑前 k 件物品,在数字 1 容量不超过 i,数字 0 容量不超过 j 的条件下的「最大价值」(每个字符串的价值均为 1)。
         *
         * f[k][i][j] = max(f[k - 1][i][j], f[k - 1][i - cnt[k][0]][j - cnt[k][1]] + 1)
         * f[k][i][j]=max(f[k−1][i][j],f[k−1][i−cnt[k][0]][j−cnt[k][1]]+1)
         * 其中 cntcnt 数组记录的是字符串中出现的 0101 数量
         *
         * @param strs
         * @param m
         * @param n
         * @return
         */
        public int findMaxForm(String[] strs, int m, int n) {
            if (strs == null || strs.length == 0) {
                return 0;
            }
            int len = strs.length;
            int[][] arr = new int[len][2];
            for (int i = 0; i < len; i++) {
                String temp = strs[i];
                int zero = 0;
                int one = 0;
                for (char c : temp.toCharArray()) {
                    if (c == '0') {
                        zero++;
                    } else {
                        one++;
                    }
                }
                arr[i] = new int[]{zero, one};
            }

            int[][][] dp = new int[len][m + 1][n + 1];

            //初始化第0件物品 i代表 0的个数 j代表1的个数 (i,j)相当于背包容量
            for (int i = 0; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    dp[0][i][j] = (i >= arr[0][0] && j >= arr[0][1]) ? 1 : 0;
                }
            }

            for (int k = 1; k < len; k++) {
                int zero = arr[k][0], one = arr[k][1];
                for (int i = 0; i <= m; i++) {
                    for (int j = 0; j <= n; j++) {
                        //不选择第k件物品
                        int a = dp[k - 1][i][j];
                        //选择第k件物品 当前物品容量足以容纳第k件物品 就是在选择k-1的基础上+1 否则就是0
                        int b = (i >= zero && j >= one) ? dp[k - 1][i - zero][j - one] + 1 : 0;
                        dp[k][i][j] = Math.max(a, b);
                    }
                }
            }
            return dp[len - 1][m][n];
        }
    }

完全背包

组合问题

Leetcode 377
在数组中选择一些数,可重复选择,构成target的排列数量
完全背包,但是要考虑物品的选择顺序。

先从dfs搜索开始:

 // dp[i] 表示 从nums中任意进行选择,凑出i的方法数目
 // 等同于 dp[i-nums[0]] + dp[i-nums[1]]+ dp[i-nums[2]] +.... + dp[i-nums[n-1]]
// 第一次可以从nums中任意选择一个数
        public int dfs(int[] nums, int target, int curSum) {
            if (curSum >= target) {
                if (target == curSum) {
                    return 1;
                }
                return 0;
            }
            int res = 0;
            for (int num : nums) {
                res += dfs(nums, target, curSum + num);
            }
            return res;
        }

上述操作需要穷举整个搜索树,但是有些数据是重复计算的,使用记忆化搜索来优化

		int[] f;
        //记忆化搜索
        public int combinationSum4_1(int[] nums, int target) {
            f = new int[target + 1];
            //初始值全部填-1 代表该值没有计算过,
            Arrays.fill(f, -1);
            f[0] = 1;
            return dfs2(nums, target, 0);
        }
        public int dfs2(int[] nums, int target, int curSum) {
            if (curSum >= target) {
                if (f[curSum] != -1) {
                    return f[curSum];
                }
                return 0;
            }
            int res = 0;
            for (int num : nums) {
                res += dfs(nums, target, curSum + num);
            }
            f[curSum] = res;
            return res;
        }
    }

动态规划优化:

public int combinationSum4(int[] nums, int target) {
            if (nums == null || nums.length == 0) {
                return 0;
            }
            int len = nums.length;
            int[] dp = new int[target + 1];
            //target=0,什么都不选是一种排列
            dp[0] = 1;
            //先遍历背包,再遍历物品
            for (int j = 0; j <= target; j++) {
            //当背包值固定时,此时可以选择的物品为i i:1~len,每次选择nums[i],剩下的j-nums[i]还在 1~len中去选
                for (int i = 1; i <= len; i++) {
                    if (j >= nums[i - 1]) {
                        dp[j] += dp[j - nums[i - 1]];
                    }
                }
            }
            return dp[target];
        }

硬币找零问题

LeetCode322 找零的最少硬币数

每个物品可以选或者不选,每个硬币可以重复选择,是个完全背包问题。

public int coinChange1(int[] coins, int amount) {
            int n = coins.length;
            int[][] dp = new int[n + 1][amount + 1];
            for (int i = 0; i <= n; i++) {
                for (int j = 0; j <= amount; j++) {
                    //预先设置一个最大值
                    dp[i][j] = amount + 1;
                }
            }
            //前i件物品 凑出价值为0  最少硬币数就是0 ,一个都不选,这个和方法数不一样
            for (int i = 0; i <= n; i++) {
                dp[i][0] = 0;
            }
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= amount; j++) {
                    // 第i件物品不选
                    dp[i][j] = dp[i - 1][j];
                    if (j - coins[i - 1] >= 0) {
                        //dp[i] 第i件物品可以重复选
                        dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i - 1]] + 1);
                    }
                }
            }
            return dp[n][amount] == (amount + 1) ? -1 : dp[n][amount];
        }
        

        public int coinChange(int[] coins, int n) {

            int[] dp = new int[n + 1];
            Arrays.fill(dp, n + 1);
            //初始化
            dp[0] = 0;

            //先遍历背包,再遍历物品 完全背包
            for (int j = 1; j <= n; j++) {
                //固定背包重量,遍历物品
                // 固定背包为j时,每次都是可以从[1,len]中选择硬币,选择最小的值,为此次背包的最小数
                for (int i = 1; i <= coins.length; i++) {
                    int b = dp[j];
                    if (j >= coins[i - 1]) {
                        dp[j] = dp[j - coins[i - 1]] + 1;
                    }
                    dp[j] = Math.min(dp[j], b);
                }
            }
            return dp[n] == n + 1 ? -1 : dp[n];
        }

LeetCode 硬币的方法数

    class Solution {
        public int change(int amount, int[] coins) {
            int n = coins.length;
            int[][] dp = new int[n + 1][amount + 1];
            for (int i = 0; i <= n; i++) {
                // 前i件物品 凑出质量是0的
                dp[i][0] = 1;
            }
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= amount; j++) {
                    dp[i][j] = dp[i - 1][j];
                    if (j - coins[i - 1] >= 0) {
                        // 从 第1到第i件物品中,选出并质量为j的方法数
                        //等于 选择第i件物品 的方法数 加上  不选择第i件物品 的方法数
                        dp[i][j] = dp[i][j] + dp[i][j - coins[i - 1]];
                    }
                }
            }
            System.out.println(dp[n][amount]);
            return dp[n][amount];
        }


        /***
         * 使用空间压缩一维进行优化时
         * 先遍历背包,再遍历物品
         * 这样求出来的是排列数
         * @param amount
         * @param coins
         * @return
         */
        public int change2(int amount, int[] coins) {
            // dp[i] 表示从第1个到第n个数中选择,和为i的方法数
            int[] dp = new int[amount + 1];
            dp[0] = 1;
            for (int i = 1; i <= amount; i++) {
                for (int coin : coins) {
                    if (i >= coin) {
                        dp[i] += dp[i - coin];
                    }
                }
            }
            return dp[amount];
        }


        /***
         * 对于完全背包而言,如果是二维dp[i][j] 背包和物品的遍历顺序都可以
         * 使用空间压缩一维进行优化时
         * 先遍历物品,再遍历背包
         * 这样求出来的是组合数
         * @param amount
         * @param coins
         * @return
         */
        public int change3(int amount, int[] coins) {
            // dp[i] 表示从第1个到第n个数中选择,和为i的方法数
            int[] dp = new int[amount + 1];
            dp[0] = 1;
            for (int coin : coins) {
                for (int i = 1; i <= amount; i++) {
                    if (i >= coin) {
                        dp[i] += dp[i - coin];
                    }
                }
            }
            return dp[amount];
        }
    }

重点

change3方法中先遍历的是物品,再遍历的是背包,
固定前i个物品,然后遍历背包j,可以选择第i个物品,也可以不选择
dp[j] = dp[j] + dp[j-coin]
组合问题,不关心硬币的使用顺序,关注的是硬币是否被用到。
在前i个物品中进行选择,dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]] (i从0开始,表示第i件物品)前面i-1件凑出j 或者 前面i件凑出 j-coins[i-1] (还能再选择第i件)

对于change2方法,固定背包容量,物品可以随意选择,第一次选择的物品可以是任意位置的,具有顺序。

如果求组合数就是外层for循环遍历物品,内层for遍历背包;
如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值