动态规划入门题、背包类题总结

1.斐波拉契

思路

①dp[i]斐波拉契第i个值,i就是第i个数值

②递推公式dp[i]=dp[i-1]+dp[i+1]

③初始化前面两个0和1

④遍历顺序是从前到后

⑤举例

这个地方会出现问题的是值太大需要对1000000007进行取余的操作。

class Solution {
    
    
    public int fib(int n) {
    
    
         if(n<=1) return n;
         int[] dp=new int[n+1];
         dp[0]=0;
         dp[1]=1;
         for(int i=2;i<=n;i++){
    
    
             dp[i]=(dp[i-1]+dp[i-2])%1000000007;
         }
         return dp[n];
    }
}

2.爬楼梯

思路

第三层楼梯是不是就是第二层爬多一层,第一层走多两步。相当于只是在他们已有的选择上增加1步或者两步但不需要修改方法数量。也就是第二层方法+第一层方法数就能够得到第三层方法数。如此类推。那么就能得到

①数组下标是第几层,值是方法数量

②dp[i]=dp[i-1]+dp[i-2];

③dp[0]=1,dp[1]=1

④从前往后遍历,需要知道前两层的方法才能得到第三层的方法。

⑤举例第三层

class Solution {
    
    
    public int climbStairs(int n) {
    
    
        if(n<2) return 1;
        int[] dp=new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
    
    
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

3.使用最小花费爬楼梯

思路

①dp的意思就是爬完第i层楼梯之后最小花费的体力

②递推公式肯定是从前两个爬完的楼梯消耗的体力最小的那个找,然后再爬完本层楼梯

③初始化就是第0和第1层都需要先走一步也就是先花费对应的体力走完

④遍历顺序肯定就是从前到后,先走完前面才知道后面的体力最小的策略

这里其实有两种思路,第一种就是走第一步,相当于就是爬完某层楼梯。第二种就是dp前两个初始化都是0因为第一步默认不走,意思就是走到第i层楼梯但是我先不爬完第i层楼梯,就呆在那。第一种就是我先爬完了,再看看要不要走上去一层或者是走两层。

再举个例子就是每层楼梯都要吃巧克力才有体力,第一种就是选下面吃巧克力最少的楼层,因为已经吃过了可以走到当层,那么走到当层之后先吃完巧克力再去看看走几层,第二种就是不吃,看看下面谁吃最少的巧克力能够到达我这层,那么就在那层吃完巧克力然后再走上来这层,然后我又获得这层的巧克力,再次进行被选择。

class Solution {
    
    
    public int minCostClimbingStairs(int[] cost) {
    
    
         int[] dp=new int[cost.length];
         dp[0]=cost[0];
         dp[1]=cost[1];
         
         for(int i=2;i<cost.length;i++){
    
    
             dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];
         }

         return Math.min(dp[dp.length-1],dp[dp.length-2]);
    }
}

4.不同路径

深搜思路

这种思路其实就是可以看成是二叉树,执行走两个方向i+1或者是j+1。只要超过边界那么就返回,或者是到达目标那么结果+1返回。然后就是走两个方向,相当于就是二叉树,深度是m+n-2+1,走了m+n-2步,但是还有原来的位置也是一个节点所以+1。时间复杂度是2^(m+n-1)-1。

class Solution {
    
    
    int res=0;

    public void traversal(int i,int j,int m,int n){
    
    
        if(i>m||j>n) return ;
        if(i==m&&j==n) res++;
        
        traversal(i+1,j,m,n);
        traversal(i,j+1,m,n);
    }

    
    public int uniquePaths(int m, int n) {
    
    
        if(m==1&&n==1) return 1;
        
       traversal(1,1,m,n);
       return res;

    }
}

动态规划思路

①dp的意思是走到i,j位置有多少条路

②递推关系就是往上面走一步i-1或者是往后面走一步j-1也就是左边,那么当前的dp’[i]’[j]就肯定是由dp[i-1] [j]或者是dp[i] [j-1]走多一步就到达了,也就是知道走到dp[i-1] [j] 和dp[i] [j-1]有多少条路,那么dp[i] [j]就是这些路的总和。

③初始化直接就是i 0的时候肯定只能走直线,或是0 j的时候也是只有一条路径。那么就是1

④遍历方向从左到右从上到下。

时间复杂度是m*n,空间就占用了n

class Solution {
    
    
    public int uniquePaths(int m, int n) {
    
    
         int[][] dp=new int[m][n];
         
         for(int i=0;i<m;i++){
    
    
             dp[i][0]=1;
         }

         for(int i=0;i<n;i++){
    
    
             dp[0][i]=1;
         }

         for(int i=1;i<m;i++){
    
    
             for(int j=1;j<n;j++){
    
    
                 dp[i][j]=dp[i-1][j]+dp[i][j-1];
             }
         }
         return dp[m-1][n-1];
    }
}

5.不同路径2

思路

跟不同路径基本上一样,但是不同的地方是初始化和路障的判断。第一个是初始化,这个地方如果在dp[i] [0]遇到路障那么就是0,在路障之前的都是1。同样另外边。另外就是对路障的判断,如果当前dp[i] [j]的位置有路障说明是无法到达这个位置的。

class Solution {
    
    
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
    
    
         int[][] dp=new int[obstacleGrid.length][obstacleGrid[0].length];
         int m=obstacleGrid.length;
         int n=obstacleGrid[0].length;
         for(int i=0;i<m&&obstacleGrid[i][0]==0;i++) dp[i][0]=1;
         for(int i=0;i<n&&obstacleGrid[0][i]==0;i++) dp[0][i]=1;
         
         for(int i=1;i<m;i++){
    
    
             for(int j=1;j<n;j++){
    
    
                 if(obstacleGrid[i][j]==1) continue;
                 dp[i][j]=dp[i-1][j]+dp[i][j-1];
             }
         }
         return dp[m-1][n-1];
    }
}

6.整数拆分

思路

①dp是i的最大拆分乘积

②max(dp[i],max((i-j) * j,dp[i-j] * j)),意思其实就是这三个值进行比较,当前的,2个数的,因为j之前已经拆分过了,所以j和i-j的乘积是没有被考虑到的。dp[i-j]其实就是i-j被拆分之后的最大乘积,那么再乘上j相当于就是i-j+j也就是i的拆分的乘积了,j是可以被选择的,然后只要选出其中最大就可以了。

③初始值dp[0] 和dp[1]都是无法被拆分,那么就不进行处理。但是dp[2]是可以拆分成两个1的。那么最大乘积就是1。就可以从这里开始。dp[0]和dp[1]基本都会被过滤掉。

④遍历肯定就是从左到右。主要看依赖的状态方向是dp[i]依赖i之前的所有dp

class Solution {
    
    
    public int integerBreak(int n) {
    
    
        int[] dp=new int[n+1];
        dp[2]=1;
        for(int i=3;i<=n;i++){
    
    
            for(int j=1;j<i-1;j++){
    
    
                dp[i]=Math.max(dp[i],Math.max((i-j)*j,j*dp[i-j]));
            }
        }
        return dp[n];
    }
}

7.不同二叉搜索树

思路

无论数值如何变化,二叉搜索树都是固定的类型,只要个数相同。比如2个节点,但是数值不同最后结果还会是2。3个节点但是由于他们的大小排序一定是固定的,所以无论数值是谁二叉搜索树形成的类型都是相似的。

①dp[i]指的是1-i的节点最多生成的二叉搜索树个数

②dp[i]=(dp[j-1]*dp[j-i])相当于就是取谁做中间节点,然后两个子树的类型相乘就是当前节点为根的二叉树类型个数,然后把所有可能为根节点遍历一遍。

③初始化dp[0]也是一种情况.dp[1]=1。其它可以通过计算。

④从左往右遍历,因为他是依赖前面的二叉树类型个数。

class Solution {
    
    
    public int numTrees(int n) {
    
    
    if(n<2) return 1;
       int[] dp=new int[n+1];
       dp[0]=1;
       dp[1]=1;
       dp[2]=2;
       for(int i=3;i<=n;i++){
    
    
           for(int j=1;j<=i;j++){
    
    
               dp[i]+=(dp[j-1]*dp[i-j]);
           }
       }
       return dp[n];
    }
}

8.背包理论

  • 初始化这一点其实就是初始第一个物品就好了。如果背包重量够那么就加入第一个物品的价值。
  • 关于dp的定义其实就是0-i的物品中选择,达到j背包重量,能够装入的价值最大是多少。

9.分割等和子集

思路

①dp的定义是0-i个物品里面选择,j价值,如果能达到j那么就是true

②递推公式是dp[i] [j]= dp[i] [j]||dp[i-1] [j-nums[i]]相当于就是不加入nums[i]是否能够达到j,或者说是加入nums[i],那么0–i-1个物品是否能有子集到达j-nums[i].

③初始值就是第一个物品dp[0] [nums[i]]刚好就是true。意思就是在第一个物品中选择是否加入这个nums[0],当价值j刚好等于nums[0]的时候那么就是可行的。

④遍历是从左到右的。

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

         if(sum%2==1) return false;
         int len=nums.length;
         int target=sum/2;
        
        boolean[][] dp=new boolean[len][target+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];

                if(nums[i]==j){
    
    
                    dp[i][j]=true;
                    continue;
                }
                
                if(nums[i]<j){
    
    
                   dp[i][j]=dp[i][j]||dp[i-1][j-nums[i]];
                }
                
            }
        }
        return dp[len-1][target];
        
    }
}

思路2的方式就是模拟01背包。背包的容量相当于就是sum/2,这里的价值也是nums[i]。dp的意思就是在0-i物品中选择,是否加入nums[i]能够让容量刚好等于价值。这里的dp是0-i物品中选择,最大的子集价值。这里其实是会限制价值的,价值是没办法大于j的。j每次减去一个nums[i]相当于就会增加一个nums[i]。但是可以确定的是j用于大于或者是等于这个nums[i]。之前的dp[j]可能就已经是最大子集的出来的和。

class Solution {
    
    
    public boolean canPartition(int[] nums) {
    
    
        int len=nums.length;
        int sum=0;
        for(int num:nums){
    
    
            sum+=num;
        }

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

        if(sum%2==1) return false;
        int target=sum/2;
        int[][] dp=new int[len][target+1];

        for(int i=0;i<=target;i++){
    
    
            if(i>=nums[0]){
    
    
                dp[0][i]=nums[0];
            }
            
        }
        
        for(int i=1;i<len;i++){
    
    
            for(int j=0;j<=target;j++){
    
    
                dp[i][j]=dp[i-1][j];
                if(j>nums[i]){
    
    
                   dp[i][j]=Math.max(dp[i][j],dp[i-1][j-nums[i]]+nums[i]);
                }
                
            }
        }
        return dp[len-1][target]==target;
    }
}

总结:实际上和01背包的差别是这里求的是容量与价值相等的时候就是正确的。那么这里的价值和容量相当于就是子集的和与sum/2的一个比较。相当于就是一个不会价值溢出的背包,那么为什么不能价值溢出原因就是价值和重量相同,价值比较大的重量也很大,无法加入到背包里面去。难点就是理解背包是不能够超重的限制,最后是可以通过价值最大的子集的和。还有一个难点就是为什么最大子集的和一定是j?原因就是你不能超重,而且每次加入相同重量的物品相对的价值也是相同的,那么最大的价值也就只有可能是包多重你的价值就有多少。这里包的重量最大限制是sum/2,对于01背包的限制是重量。实际上就是一样的意思。

01背包实际上就是第i个物品选还是不选的问题。然后这个地方就是nums[i]选还是不选,有没有办法让前i个物品选择相加达到j的重量,如果可以那么就是true。如果不行那么就是false。

class Solution {
    
    
    public boolean canPartition(int[] nums) {
    
    
        int sum=0;
        for(int num:nums){
    
    
            sum+=num;
        }
        
        if(sum%2==1) return false;
        int len=nums.length;
        int target=sum/2;
        boolean[][] dp=new boolean[len][target+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];
                if(nums[i]==j){
    
    
                   dp[i][j]=true;
                   continue;
                }

                if(j>=nums[i]){
    
    
                    dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]];
                }
            }
        }

        return dp[len-1][target];
    }
}

10.最后一块石头的重量2

思路

这个地方的动规五部曲就是背包的套路。但是为什么可以转换成背包的思路呢?你思考一下每次x-y的虚拟石头,相当于只是更改了一下正负,两个原本石头改换了阵营,石头3减去新的虚拟石头把它重量拆解一下实际上还是正负号的切换。也就相当于是两堆石头了。如果你希望得到最小的石头,其实就是相当于两堆石头的重量越接近越好,那么肯定就是接近sum/2就可以了。那么就可以变成背包思路,最后求解两个石头堆的差值就可以了。

①dp的含义是容量,值是石头的重量,越大越好。在数组里面去挑

②递推公式同背包处理方式。

class Solution {
    
    
    public int lastStoneWeightII(int[] stones) {
    
    
        int len=stones.length;
        int sum=0;
        for(int i:stones){
    
    
            sum+=i;
        }
        int target=sum/2;
        int[] dp=new int[target+1];
         
        for(int i=0;i<len;i++){
    
    
            for(int j=target;j>=stones[i];j--){
    
    
                dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }

        return sum-dp[target]-dp[target];
    }
}

11.目标和

思路

这道题是需要转变成01背包的思路。可以知道加数是确定的,x-(sum-x)=target。这个地方加数也就是取正的数的和,sum-x就是减数取负的集合。最后算出加数是个固定的值。既然是个固定的值而且需要求有多少种可能能够得到这个加数。也就是直接在数组里面求和就行了。相当于就是背包容量是加数,物品就是多少个数字。价值就是对应有多少种方式能够凑成这个加数。动态规划五部曲。

①dp[j]这里的j指的是背包容量也就是加数,值就是方法数。

②dp[j]=dp[j]+dp[j-nums[i]]其实就是要么就选这个物品,要么就不选,在这个地方要求方法总和,所要相加,选和不选就是两种方式。如果不选那么就是前i-1个能凑出j的加数的方法数。如果选那么就是dp[j-nums[i]]的方法数只要加上nums[i]就能够得到j也就是只需要dp[j-nums[i]]的方法数就可以了。

③关于初始化这方面肯定就是dp[0]也就是0件物品的时候肯定就是1种能够凑出0的重量。

④关于遍历,可以从后往前,原因是防止从前往后把已经修改过的值重新拿来使用。因为每次取值都是要去0-i-1的这个范围而不是0-i。如果是从前面往后面,后面的递推就是错误的。

class Solution {
    
    
    public int findTargetSumWays(int[] nums, int target) {
    
    
          int sum=0;
          for(int num:nums){
    
    
              sum+=num;
          }
          
          if((sum+target)%2==1) return 0;

        if(sum<Math.abs(target)) return 0;
          
          //加数
          int x=(sum+target)/2;
          

          //求能够达到这个加数的方法数
          int[] dp=new int[x+1];
          
          dp[0]=1;
          
          for(int i=0;i<nums.length;i++){
    
    
              for(int j=x;j>=nums[i];j--){
    
    
                  dp[j]=dp[j]+dp[j-nums[i]];
              }
          }
          return dp[x];
           
          
    }
}

dfs暴力求解

01背包相当于就是二叉树求解。时间复杂度是2^n次方,空间复杂度是1,忽略递归的空间复杂度。

class Solution {
    
    
    int sum=0;
    public void dfs(int[] nums,int i,int cur,int target){
    
    
        if(i>=nums.length){
    
    
            if(cur==target){
    
    
                 sum++;
            }
           
            return;
        }
        
        dfs(nums,i+1,cur+nums[i],target);
        dfs(nums,i+1,cur-nums[i],target);
        
    }
    public int findTargetSumWays(int[] nums, int target) {
    
    
         dfs(nums,0,0,target);
         
         return sum;
    }
}

dfs的记忆化搜索

其实就是记录了每次第i个物品,到达cur数的时候的最大方法个数。每次进入递归都先判断一下是否存在记录,不存在那么就递归,存在那么就不需要进行递归操作。这里需要返回值的原因,dfs递归得到的值是前i个物品,能够组装到的值j最大的方法数,而且需要两个选择最大方法之和才是当前的最大方法数。两个选择分别是±nums[i]。然后记录下来。

class Solution {
    
    
    // int sum=0;
    Map<String,Integer> memory=new HashMap<>();
    public int  dfs(int[] nums,int i,int cur,int target){
    
    
        String key=i+"_"+cur;
        if(memory.containsKey(key)) return memory.get(key);
        if(i>=nums.length){
    
    
            memory.put(key,cur==target?1:0);
            return memory.get(key);
        }
        
        int left=dfs(nums,i+1,cur+nums[i],target);
        int right=dfs(nums,i+1,cur-nums[i],target);
        memory.put(key,left+right);
        
        return memory.get(key);
        
    }
    public int findTargetSumWays(int[] nums, int target) {
    
    
         
         
         return dfs(nums,0,0,target);
    }
}

12.一和零

思路

其实还是能够变成01背包的问题。这里相当于就是二维01背包。也就是两个容量。题目的意思就是最后子集里面最多m个0和n个1,换一种说法就是两个背包一个最大是m,一个最大是n。每个字符串其实就是重量,拆分成0和1,分别放入各自的背包。如果两个都能放入那么就给你1块钱的意思。

①dp[i] [j]这里的i和j都是背包容量,值就是最多多少个子集

②递推的关系就是从以前的重量里面挑出最大的子集的那个背包。比如说dp[i] [j]=max (dp[i] [j],dp[i-zero] [j-one]+1)相当于就是dp[j]=max(dp[j],dp[j-x]+1)这样的一个操作,只不过容量变成了两个书包而已,需要符合两个书包的条件,那么遍历的情况自然就更多了。相当于是每次选一个物品(字符串), 选或者是不选,如果不选就是按照没有这个物品时这两个背包最大子集数来处理,如果选,那么就要去

dp[i-zero] [j-one]里面看看最大子集数+1是否比不选的时候子集更多。这里i-zero,和j-one,相当于就是如果你要选当前字符串,那么就必须让位zero和one的容量出来,让出来之后再看看那个i-zero、j-one容量背包最大的子集数。

③初始化全部都是0因为就是一个每个物品价值是1,有两个重量的01背包问题。

④遍历还是从后往前。

拓展

本质还是背包但是不同的就是要把字符串拆出来0是一个重量,1也是一个重量,而且每次加入的时候背包一定要有足够的空间装物品,物品有两个重量,也可以看成两个背包,需要两个重量同时满足放入才可以。

class Solution {
    
    
    public int findMaxForm(String[] strs, int m, int n) {
    
    
        int[][] dp=new int[m+1][n+1];
        
        for(String str:strs){
    
    
            int one=0;
            int zero=0;
            for(char c:str.toCharArray()){
    
    
                if(c=='1') one++;
                else zero++;
            }

            for(int i=m;i>=zero;i--){
    
    
                for(int j=n;j>=one;j--){
    
    
                    dp[i][j]=Math.max(dp[i][j],dp[i-zero][j-one]+1);
                }
            }
        }
        return dp[m][n];
    }
}

最原始的处理方式,i纬度用来代表遍历物品,j和k都是遍历重量的。但是需要注意一定要从j=0和k=0开始遍历,原因是后面的处理比如dp[i-1] [j-x] [k-x]这个时候可能就都需要前面的数据,前面不够空间装第i个物品的背包可以装下最多的物品都是从上一个背包获取。如果物品不符合规则,那么就无法加入进来,比如说其中一个重量超重。

class Solution {
    
    
    public int findMaxForm(String[] strs, int m, int n) {
    
    
        int len=strs.length;
        int[][][] dp=new int[len+1][m+1][n+1];
        
        for(int i=1;i<=len;i++){
    
    
            int[] count=getCount(strs[i-1]);
             for(int j=0;j<=m;j++){
    
    
                 for(int k=0;k<=n;k++){
    
    
                     dp[i][j][k]=dp[i-1][j][k];
                     if(j-count[0]>=0&&k-count[1]>=0){
    
    
                         dp[i][j][k]=Math.max(dp[i][j][k],dp[i-1][j-count[0]][k-count[1]]+1);
                     }
                     
                 }
             }
        }
        return dp[len][m][n];
    }

    public int[] getCount(String s){
    
    
        int[] count=new int[2];
        for(char c:s.toCharArray()){
    
    
            if(c=='1'){
    
    
                count[1]+=1;
            }else{
    
    
                count[0]+=1;
            }
        }
        return count;
    }
}
class Solution {
    
    
    public int findMaxForm(String[] strs, int m, int n) {
    
    
        int len=strs.length;
        int[][] dp=new int[m+1][n+1];
        
        for(int i=1;i<=len;i++){
    
    
            int[] count=getCount(strs[i-1]);
             for(int j=m;j>=count[0];j--){
    
    
                 for(int k=n;k>=count[1];k--){
    
    
                     dp[j][k]=Math.max(dp[j-count[0]][k-count[1]]+1,dp[j][k]);
                 }
             }
        }
        return dp[m][n];
    }

    public int[] getCount(String s){
    
    
        int[] count=new int[2];
        for(char c:s.toCharArray()){
    
    
            if(c=='1'){
    
    
                count[1]+=1;
            }else{
    
    
                count[0]+=1;
            }
        }
        return count;
    }
}

13.518. 零钱兑换 II

思路

这个地方使用的完全背包。完全背包其实就是物品可以被无数次选中。那么如果要在dp数组里面实现被重复使用,那么可以从前往后遍历,那么前面处理完的结果能够被重新使用,也就是如果dp[1]=dpw[1-weight]+1是新的最大值,那么dp[2]=dp[2-weight]+1,weight如果是1,那么就相当于在dp[2]里面加入了两次同样的物品。如果是二维比较的话,那么就要比较0-i-1的还要比较0-i的。比较0-i的通常就是加入同样的物品。因为现在遍历的是同一层。

①dp[j]这里的j是指凑成的零钱,值是凑成j的最大方法数。

②递推dp[j]=dp[j]+dp[j-coins[i]]。

③初始化是dp[0]=1当零钱是1的时候只要不加入钱就是一种方法

④从前往后遍历,因为dp[j]更新之后可能还会被再使用,相当于就是加入了物品之后,还想加入一次。

还有一种解法就是直接dfs遍历到底。

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

14.组合总和4

思路

这道题其实就是完全背包问题,元素能够被多次选择使用。但是不同的是这里求的是全排列,那么就不能够通过物品在外层,重量遍历在内层。变成了重量在外层,物品在里面这层。实际上它的dp递归关系还是不会改变,dp[j]也就是达到目标j的方法总数是是多少。dp[0]=1作为初始化。那么选择可以是两个第一个是不选这个物品,第二个选择是可以选择这个物品,但是需要两种方法相加,才是总方法,因为他们都是不同的方式到达j容量。

①dp[j],j容量的背包,值是最大方法

②递推dp[j]=dp[j]+dp[j-nums[i]]

③遍历方向是重量外层,物品内层。而且从左到右原因是可以让物品多次被选择。相当于就是在加入了物品之后,后面的dp再次访问已经加入物品的dp也是可以的。但是倒序那么就一定就是前面的dp没有处理过,也就是都是上一个物品选择完之后的结果用来计算这次的结果。而没有进行对这次物品的叠加。而且外层是重量和内层是物品的好处就是同样的重量,选择的物品相同,可以排序不同这种也被加入到结果里面。

而且这个地方的物品并没有顺序,所以用二维数组进行的初始化非常困难。

这里的解释也可以是这样,dp[i-nums[0]]+dp[i-nums[1]]+dp[i-nums[2]]。。不同的数字在最后做的结尾都会导致顺序不同。而且是从前遍历到后所以是可以重复使用的。

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

二维的思路

由于数字排序不同会生成不同的方案,也就是说,第一维就是i纬度已经代表的不是第几个物品的,而是组合的长度。如果数字排序不同能够凑成target只是一种方案,比如2,5和5,2那么就可以直接套用完全背包的公式。那么什么是组合的长度?数组[1,2]比如说凑成4,那么组合长度为4的时候只能是1111这四个凑起来,如果是3长度,那么就是121,211,112这样子的凑起来。如果是这样那么对应的递推公式应该是怎么样的?其实就是dp[i] [j]也就是长度是i,目标是j的最大凑成方法数应该就是dp[i-1] [j-num]所有总和。因为num就相当于就是物品重量。要凑成组合长度是i的,那么肯定就是i-1的长度中再选一个物品出来,j-num选择的物品是num。那么目标j-num加上num的就是j了,也就是说再加上凑成j-num的方法数就能够得到j总方法数。最后还需要记录res的原因就是每次求出的只是当前组合长度的最大方法数,问题需要求的是得到target到底有多少方法,所以需要把所有长度的都搜集起来。

子问题的定义其实就是problem(i,j)=sum(problem(i-1,j-numx))

时间复杂度是target^2 *n空间复杂度是target^2

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

优化降维的思路

dp[j]其实就是凑成j的时候的方案数。那么和二维有什么不同?为什么这个不需要res累加?原因很简单因为dp定义其实不同,上一个是加上了组合长度纬度,但是这里没有这个纬度,而且dp的定义是凑成j的最大方法数。dp[j]其实就已经经历过所有dp[j-num]的方法数相加得到的答案,也就是它已经尝试过所有顺序的num。

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

15.爬楼梯(进阶)

思路

完全背包而且和组合总和4的思路基本上是一样的。这里的n就是目标,数组就是1和2.相当于就是物品和重量。那么这里只需要先遍历重量再遍历物品。让物品可以排序。而二维数组的局限就是i限制了物品的排序,但是可以通过nums来进行选择。但是难度要大很多。一维数组的好处就是可以处理排列和完全背包问题的重复选择。

class Solution {
    
    
    public int climbStairs(int n) {
    
    
        //相当于是物品只有两个遍历
        int[] dp=new int[n+1];
        int m=2;
        dp[0]=1;
        for(int i=1;i<=n;i++){
    
    
            for(int j=1;j<=m;j++){
    
    
                if(i>=j){
    
    
                   dp[i]+=dp[i-j];
                }
                
            }
        }
        return dp[n];
    }
}

16.关于爬楼梯和零钱兑换 II

他们不同的地方就是爬楼梯是一个排列,但是零钱兑换是一个组合。实际上爬楼梯的数组就是1,2,零钱兑换的数组是1,2,5,把爬楼梯数组拓展一下其实就是零钱兑换。那么导致他们最终不同的就是组合和排列问题,实际上就是子问题的定义。

如果是零钱兑换问题,本质可以看成是二维数组,一维是确定哪个零钱是否使用,二维就是是否凑齐目标i。也可以变成一维数组。这里的一维实质是可以与二维的定义相似。这里的子问题dp[k] [i]=dp[k-1] [i]+dp[k-1] [i-nums[k]]意思就是前k个能凑到i的有两方式第一种是没有第k个硬币,就已经凑到,第二种是dp[k] [i-nums[k]]就差第k个硬币。如果是两个顺序调换,先遍历是背包重量再到遍历物品,那么物品这个地方实际上就是dp[i] [j]=sum(dp[i] [i-j])这里的j是1,2,5.相当于就是这里的硬币i-1,i-2,i-5的和。对比原本的那种是组合的子问题是使用前k个硬币来组合成最后的amount,但是后面这种是只需要凑到不同硬币在结尾的不同顺序能够达到j的目标结果。也就是硬币是没有被限制顺序的。这种完全背包问题最好就是使用一维数组来处理,因为并不需要第一维来控制硬币的顺序和位置。

但是对于爬楼梯来说,数组的元素是有排序的,那么做到排序的关键就是先遍历背包的容量在遍历物品,这样才能够出现1,2或者是2,1这样的不同顺序。

本质不同

是他们的子问题不同。零钱是没有排序之分,但是爬楼梯每次爬几楼,同一次不同的爬楼走几步都是不同的答案,即使他们同样到达那高度。比如2,5能到达7高度,那么5,2同样也是可以,对于爬楼梯这就是不同的方法,当时对于凑零钱那么就只是同样的方法。

凑零钱的子问题其实就是 problem (i,j)=problem(i-1,j)+problem(i-1,j-nums[i]);这里的一次就是是否选择加上第i个硬币,不加硬币能达到j元和加上硬币之后得到j元的方法数相加。很明显这里的硬币是被排好序使用的,每层只能使用一个,而且每层硬币都能使用多次。

但是爬楼梯的子问题是problem(i,j)=sum(problem(i,j-nums[i]))。这里很明显就是不同,走到j层,那么只需要从j-nums[i]层走多nums[i]层的所有方法加起来就可以了,但是这种方法就是会出现2,5和5,2的相同的选择物品的情况,但是却是不同的上楼梯方法。换成硬币来说就是凑成7块钱,但是每次付钱的时候硬币顺序不同就是不同的方法,我先给2块再给5块,和先给5块再给2块都是相同的。

17.零钱兑换

思路

①dp[j]到达j最少需要多少硬币

②dp[j]=min(dp[j],dp[j-coins[i]])到达j最少的硬币可以通过dp[i-1] [j]或者是dp[i-1] [j-coins[i]]说明就差一个coins[i]就能够到达j

③关于初始化,这里dp[0]肯定是0,其它都是maxInteger,保证了最小硬币才会取。而且只有硬币dp[coins[i]-coins[i]]才会等于dp[0]也就是最后的结果一定是硬币刚好凑齐j的。如果不是凑齐j的那么就是maxInteger没办法取的。自底向上的求解。

这道题是一道完全背包。而且是求最小硬币的问题,遍历顺序是从物品开始,因为物品是组合,而不是排列,也就是规定好了物品的顺序了。某个硬币用完之后就不会再使用的一种遍历方式。

class Solution {
    
    
    public int coinChange(int[] coins, int amount) {
    
    
         int[] dp=new int[amount+1];
         int len=coins.length;
         Arrays.fill(dp,Integer.MAX_VALUE);
         dp[0]=0;
         for(int i=0;i<len;i++){
    
    
             for(int j=coins[i];j<=amount;j++){
    
    
                 if(dp[j-coins[i]]!=Integer.MAX_VALUE){
    
    
                    dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
                 }
                 
             }
         }
         return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
    }
}

18.完全平方数

思路

完全背包,平方和所有取值就是数组也就是物品。然后n就是对应的背包容量,这里求的是组合,只要得到最后的结果就可以了。所以不需要让所有的数字进行排序。

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

19单词拆分

思路

完全背包。其实就是单词就是物品,只不过需要对比背包规定好的次序和对比这个长度内是不是就是这个物品,如果不是那么就无法装这个物品进去。如果对应成功那么就能装进去。这里只能是先遍历背包容量然后再遍历物品,原因是先遍历物品导致物品的次序已经固定好,那么applepenapple,要先遍历apple但是发现到第二个就装不进去了,对应不上。但是遍历容量可以尝试各种物品放进去的组合。这样才有可能把背包填充好。但是本质还是放物品填充背包,只不过背包多了次序但是你不知道对不对,就像是规定了这个位置放什么那就只能放什么。

class Solution {
    
    
    public boolean wordBreak(String s, List<String> wordDict) {
    
    
          int len=s.length();
          boolean[] dp=new boolean[len+1];
          dp[0]=true;
          for(int i=0;i<=len;i++){
    
    
              for(String word:wordDict){
    
    
                  int wordLen=word.length();
                  if(i>=wordLen&&wordDict.contains(s.substring(i-wordLen,i))&&dp[i-wordLen]){
    
    
                      dp[i]=dp[i-wordLen];
                  }
              }
          }

          return dp[len];
          
    }
}

递归记忆搜索法

思路

其实就是回溯,切割字符串。start用于切割,并且每次切割出来之后看看是否对应单词表上的单词,如果对应那么就继续往下去切割。

class Solution {
    
    
    
    public boolean wordBreak(String s, List<String> wordDict) {
    
    
         int[] memory=new int[s.length()+1];
         return backtrack(s,0,wordDict,memory);
    }

    public boolean backtrack(String s,int start,List<String> wordDict,int[] memory){
    
    
        if(start>=s.length()){
    
    
            return true;
        }
        
        if(memory[start]!=0){
    
    
            return memory[start]==1?true:false;
        }

        for(int i=start;i<s.length();i++){
    
    
            String sub=s.substring(start,i+1);
            if(wordDict.contains(sub)&&backtrack(s,i+1,wordDict,memory)){
    
    
                memory[start]=1;
                return true;
            }
        }
        memory[start]=2;
        return false;
    }

}

BFS思路

这种思路其实就是层级遍历,多叉树的层级遍历,然后通过备忘录来进行剪枝。首先遍历的是截断字符串的位置i,然后往下遍历如果截断的字符串是字典里面的那么就加入队列这个时候的i+1可以加入队列,说明就是下一层的遍历。前面的节点都已经匹配了自己的字符串,所以在最后如果i是大于这个字符串长度的时候就可以返回正确了。因为start-i是一个单词,而且能够加入到节点行列都是前面匹配了一个单词的节点。不匹配是没办法加入的

class Solution {
    
    
    public boolean wordBreak(String s, List<String> wordDict) {
    
    
          boolean[] visited=new boolean[s.length()+1];
          
          Queue<Integer> queue=new LinkedList<>();
          queue.offer(0);
          while(!queue.isEmpty()){
    
    
              int start=queue.poll();
              if(visited[start]) continue;
              else visited[start]=true;

              for(int i=start;i<s.length();i++){
    
    
                  String sub=s.substring(start,i+1);
                   if(wordDict.contains(sub)){
    
    
                        if(i+1<s.length()){
    
    
                            queue.offer(i+1);
                        }else{
    
    
                            return true;
                        }    
                   }
              }
          }
          return false;
    }
}



猜你喜欢

转载自blog.csdn.net/m0_46388866/article/details/120806675
今日推荐