动态规划

0.1背包 理论基础

二维表 行代表选取0-n 多少物品

列代表 最大容量

最后需要的是最后一个格子的值

普通版本

import java.util.*;

public class Main {

    public static void main(String[] args) { // 1. 读取输入
         Scanner sc = new Scanner(System.in);

        // 读取第一行
        int M = sc.nextInt(); // 材料种类
        int N = sc.nextInt(); // 行李空间大小

        // 读取第二行:每种研究材料的占用空间
        int[] weights = new int[M+1];
        for (int i = 1; i <= M; i++) {
            weights[i] = sc.nextInt();
        }

        // 读取第三行:每种研究材料的价值
        int[] values = new int[M+1];
        for (int i = 1; i <= M; i++) {
            values[i] = sc.nextInt();
        }

        // 2. 定义一维dp数组,dp[j]表示容量为j时可以获取的最大价值
        int[][] dp = new int[M+1][N + 1];

        for (int i=1;i<=M ;i++ ){
            for(int j=0;j<=N;j++){
                dp[i][j]=dp[i-1][j];
                if(j - weights[i] >= 0) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - weights[i]] + values[i]);
                }

            }
        }
        System.out.println(dp[M][N]);
    }}

每一个格子 依靠的其实是该格子的上一个格子 和上一行格子左边的我某个格子

注意:要判断当前容量能否放得下第i个物品

压缩版本

用一个一维数组,我们不用去保存每一行的值,dp一维数组我们可以动态的滚动,注意要从右往左更新,因为格子依靠的是上一行和其上一行左边格子的值,从右往左时,上一行及其左边的值还没有被覆盖。

如果从左往右,左边的值已经被覆盖。

注意,当j<weights[i]时,我们就可以跳出该次循环了,因为接下来j--,j一定还是小于当前物品容量的,我们跳出循环,默认dp[J]就等于其上一行的值。



import java.util.*;

public class Main {

    public static void main(String[] args) { // 1. 读取输入
        Scanner sc = new Scanner(System.in);

        // 读取第一行
        int M = sc.nextInt(); // 材料种类
        int N = sc.nextInt(); // 行李空间大小

        // 读取第二行:每种研究材料的占用空间
        int[] weights = new int[M+1];
        for (int i = 1; i <= M; i++) {
            weights[i] = sc.nextInt();
        }

        // 读取第三行:每种研究材料的价值
        int[] values = new int[M+1];
        for (int i = 1; i <= M; i++) {
            values[i] = sc.nextInt();
        }

        // 2. 定义一维dp数组,dp[j]表示容量为j时可以获取的最大价值
        int[] dp = new int[N + 1];

        for (int i=1;i<=M ;i++ ){
            for(int j=N;j>=weights[i];j--){
                dp[j]=Math.max(dp[j], dp[j - weights[i]] + values[i]);


            }
        }
        System.out.println(dp[N]);
    }}

1、分割等和子集

https://leetcode.cn/problems/partition-equal-subset-sum/description/

class Solution {
    public boolean canPartition(int[] nums) {
          int len=nums.length;
        int sum=0;
        for (int i = 0; i <len ; i++) {
            sum+=nums[i];
        }
        if(sum%2!=0)
            return false;
        int res=sum/2;
        int[] dp=new int[res+1];
        for (int i = 0; i < len; i++) {
            for (int j = res; j >=nums[i] ; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }
            if(dp[res]==res){
                return true;
            }

        }
        return false;
    }
}

2、最后一块石头的重量II

https://leetcode.cn/problems/last-stone-weight-ii/description/

class Solution {
    public int lastStoneWeightII(int[] stones) {

        int len=stones.length;
        int sum=0;
        for (int i = 0; i <len ; i++) {
            sum+=stones[i];
        }
        int res=sum/2;
        int[] dp=new int[res+1];
        for (int i = 0; i < len; i++) {
            for (int j = res; j >=stones[i] ; j--) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
         return sum-2*dp[res];
    }
}

尽可能的分配 比如说总和为11 那么我们希望 dp[5] 即总和5尽可能的装石头 看总和5 最大的石头数为多少,之后再用总和减。

3、目标和

https://leetcode.cn/problems/target-sum/description/

class Solution {
   public  int res=0;
    public  int findTargetSumWays(int[] nums, int target) {
     int sum=0;
        for (int i = 0; i <nums.length ; i++) {
            sum+=nums[i];
        }

        if(sum<Math.abs(target)) return 0;
        if((sum+target)%2!=0) return 0;
        int res=(sum+target)/2;
        int[] dp=new int[res+1];
        dp[0]=1;
        for (int i = 0; i <nums.length ; i++) {
            for (int j=res;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[res];

    }
}

假设 正数为r 负数为l

那么 r+l=sum ①

并且 r-l=target ②

l=sum-r

带入 ② r=(sum+target)/2

如果除不尽 其实就代表不能得到target的总和。

dp[j] 表示 得到为j和的组合数

如果 j=5 那么 dp[5]=dp[4]+dp[3]+d[2]+dp[1]

比如 我们装了1 那么就是有dp[4]种方法 以此类推 这里不要+1,因为是求方法数,已经有了装满4的dp[4]种方法,在这种基础上在装1即可。

    dp[j]+=dp[j-nums[i]];

4、一和零

https://leetcode.cn/problems/ones-and-zeroes/description/

回溯算法做 超时了

class Solution {
      private  int m_count=0;
    private  int n_count=0;
    private int res=0;
    public  int findMaxForm(String[] strs, int m, int n) {
        if (strs.length==0)
            return 0;
        backtracking(strs,m,n,0,0);
        return  res;
    }

    public  void backtracking(String[] strs, int m, int n,int index,int sum){
        if(m_count>m||n_count>n){
            return;
        }
        res=Math.max(res,sum);

        for (int i = index; i <strs.length ; i++) {
            char[] chars = strs[i].toCharArray();
            for (int j = 0; j <chars.length ; j++) {
                if (chars[j]=='0')
                    m_count++;
                if (chars[j]=='1')
                    n_count++;
            }
            sum++;
            backtracking(strs,m,n,i+1,sum);
            for (int j = 0; j <chars.length ; j++) {
                if (chars[j]=='0')
                    m_count--;
                if (chars[j]=='1')
                    n_count--;
            }
            sum--;
        }
    }}

dp

class Solution {

    private int res=0;
    public  int findMaxForm(String[] strs, int m, int n) {
      if (strs.length==0)
            return 0;
        int[][] dp=new int[m+1][n+1];
        for(int i=0;i<strs.length;i++){
            char[] chars = strs[i].toCharArray();
            int x=0;
            int y=0;
            for (int j = 0; j <chars.length ; j++) {
                if (chars[j]=='0')
                    x++;
                else
                    y++;
            }
            for (int j =m ; j >=x ; j--) {
                for (int k = n; k >=y; k--) {
                dp[j][k]=Math.max(dp[j][k],dp[j-x][k-y]+1);
            }
        }
       
    } return  dp[m][n];
    }
    }

这也是一个01背包问题,每个值只能被选择一次,不同点在于,这里有两个纬度,一个m一个n。

但是万变不离其宗,并且要注意,使用滚动数组要从右往左,因为我们依赖于上一行及其左边的值,因为上一行表示选择0-i-1,而本行表示选择0-i,如果我们不选择当前行的值,那么当前行的值应该与上一行的相同。

完全背包

//先遍历物品,再遍历背包
private static void testCompletePack(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++){ // 遍历物品
        for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

//先遍历背包,再遍历物品
private static void testCompletePackAnotherWay(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量
        for (int j = 0; j < weight.length; j++){ // 遍历物品
            if (i - weight[j] >= 0){
                dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]);
            }
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

因为可以重复选,所以我们的dp[i][j]=max(dp[i-1][j],dp[i][j-costs[i]]+val[i]) 即在选择i物品时 我们仍然可以选择这个物品

依赖的是当前行左边的 和上一行当前列 的。

所以空间压缩时,我们从左往右即可

左边 的是当前行更新的,dp[j]还没更新正好是之前的。

1、零钱兑换 II

https://leetcode.cn/problems/coin-change-ii/description/

class Solution {
    public int change(int amount, int[] coins) {
          if (coins.length==0){
            return 0;
        }
        int[] dp=new int[amount+1];
        dp[0]=1;
        for (int i = 0; i <coins.length ; i++) {
            for (int j = coins[i]; j <=amount ; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

这个不能先容量后物品,因为 先容量的话,是排列问题,

会出现1,2;2,1的情况,但是先物品,后容量就不会出现该问题。

2、组合总和 Ⅳ

https://leetcode.cn/problems/combination-sum-iv/description/

class Solution {
    public int combinationSum4(int[] nums, int target) {
        
      int[] dp=new int[target+1];
      dp[0]=1;
        for (int i = 0; i <=target ; i++) {
            for (int j =0 ; j <nums.length ; j++) {
                if(i>=nums[j])
                dp[i]+=dp[i-nums[j]];
            }
        }
       return dp[target];
    }
}

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

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

该题是排列

3、爬楼梯

https://kamacoder.com/problempage.php?pid=1067

import java.util.*;
public class Main{

   public static void main(String[] args)
    {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int m = sc.nextInt();
        int[] dp = new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for (int i = 2; i <=n ; i++) {
            for (int j =1 ; j <=m ; j++) {
                if(i>=j)
                    dp[i]+=dp[i-j];

            }
        }
           System.out.println(dp[n]);
       

    }}

也是排列问题

注意初始化

3、零钱兑换

https://leetcode.cn/problems/coin-change/description/

class Solution {
    public int coinChange(int[] coins, int amount) {
          int max = Integer.MAX_VALUE;
        int[] dp = new int[amount + 1];
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
      dp[0]=0;
        for (int i = 0; i <coins.length ; i++) {
            for (int j = coins[i]; j <=amount ; j++) {
                //只有不是最大值的时候 才有选择的必要  会溢出
                 if (dp[j - coins[i]] != max) {
                    //选择硬币数目最小的情况
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                }
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

dp[j] 表示凑够j元的饿最小金钱的个数 所以要加1

同时因为是最小值 我们要刚开始初始化为最大值

初始化也要注意,dp[0]=0;如果有金币5,dp[5-5]虽然是0,但是由于多了一个金币,所以加1,所以数量是1,所以应该初始化为0

4、完全平方数

https://leetcode.cn/problems/perfect-squares/description/

class Solution {
    public int numSquares(int n) {
            int max = Integer.MAX_VALUE;
      int[] dp=new int[n+1];
      dp[0]=0;
        for (int i = 1; i <=n ; i++) {
            dp[i]=max;
        }
        for (int i = 1; i*i <=n ; i++) {
            for (int j = i*i; j <=n ; j++) {
                dp[j]=Math.min(dp[j],dp[j-i*i]+1);
            }
        }
        return dp[n];
    }
}

和上一题其实是一样的,

本题相当于从正整数1,4,9.....这些中去选用最小的数量拼凑出 n

不久相当于上一题的从某些零钱中选取最小的个数拼凑出总金额amount吗,

不一样的是 这里的物品的重量相当于i*i;

5、单词拆分

https://leetcode.cn/problems/word-break/description/

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
            boolean[] dp=new boolean[s.length()+1];
       dp[0]=true;
       //先遍历背包 在遍历物品
        for (int i = 1; i <=s.length(); i++) {
            for (int j = 0; j <wordDict.size() ; j++) {
                String word = wordDict.get(j);
                int len = word.length(); // 当前物品的长度
                // 检查长度是否足够以及子串是否等于字典中的单词
                if (i >= len && s.substring(i - len, i).equals(word) && dp[i - len]) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

因为是求排列数 所以要先背包 后物品

这里可能会出现121 这些情况 且与112 不同

6、打家劫舍Ⅰ

https://leetcode.cn/problems/house-robber/description/

class Solution {
    public int rob(int[] nums) {
        if(nums.length==1) return nums[0];
          int[] dp=new int[nums.length+1];
        int sum=0;
        for (int i = 0; i <nums.length ; i++) {
            sum+=nums[i];
        }
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0],nums[1]);
        for (int i = 2; i <nums.length ; i++) { //物品
            dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[nums.length-1];
    }
}

7、打家劫舍Ⅱ

https://leetcode.cn/problems/house-robber-ii/description/

class Solution {
    public int rob(int[] nums) {

        if (nums == null || nums.length == 0)
            return 0;
        int len = nums.length;
        if (len == 1)
            return nums[0];
        if(len==2)
            return Math.max(nums[0],nums[1]);
        return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len));
    }

    private static int robAction(int[] nums,int start,int end) {
             int[] dp=new int[nums.length];
        dp[start]= nums[start];
        dp[start+1]=Math.max(nums[start], nums[start+1]);
        for (int i = start+2; i < end ; i++) { //物品
            dp[i]=Math.max(dp[i-2]+ nums[i],dp[i-1]);

        }
        return dp[end-1];
    }

    }

8、打家劫舍Ⅲ

https://leetcode.cn/problems/house-robber-iii/description/

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    public int rob(TreeNode root) {
        int[] res = robAction1(root);
        return Math.max(res[0], res[1]);
    }

    int[] robAction1(TreeNode root) {
        int res[] = new int[2];
        if (root == null)
            return new int[]{0,0};
        int[] left = robAction1(root.left);
        int[] right = robAction1(root.right);
        res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        res[1] = root.val + left[0] + right[0];
        return res;
    }
    }