算法笔记:动规规划(java版)

动态规划
首先感谢由代码随想录提供的题目,本文章为我的一些见解。大家想做题,可关注代码随想录公众号,本文有参考网络上的优质解法,如侵删。

动规五步走

  1. 确定dp数组(dp table)以及下标的含义

  2. 确定递推公式

  3. dp数组如何初始化

  4. 确定遍历顺序

  5. 举例推导dp数组

    ⼀些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?

    因为⼀些情况是递推公式决定了dp数组要如何初始化!

爬楼梯

image-20210708174402744

1.确定dp数组以及下标含义

dp[i]:爬到第i层楼梯,有dp[i]种方法。

2.确定递推公式

这道题的精髓就是每次你可以爬1或2个台阶,所以我们计算上第n个台阶的方法的时候,就可以这么想,我可以先到n-2的台阶然后再爬两个台阶或者去到第n-1的台阶再爬一个台阶。所以我们这时候又要知道去n-2和n-1的台阶有多少种办法,那么dp[i]=dp[i-1]+dp[i-2]

3.dp数组如何初始化化

这里我们考虑dp[1]和dp[2]的初始化就可以了,dp[1]=1,上一层楼梯有一种,dp[2]=2,上两层楼梯的办法有两种。

4.确定遍历顺序

从前往后

5.举例推到dp数组

n=5时,dp数组应该是

下标 1 2 3 4 5
dp[i] 1 2 3 5 8

和自己推导的一样

java代码如下:

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

使用最小花费爬楼梯

image-20210708212500362

  1. 确定dp数组(dp table)以及下标的含义

    dp[i]指的是到第i层最少花费体力并随时可以出发(已花费到达台阶脚下的体力所需)

  2. 确定递推公式

    因为这个题目也是花费脚下的台阶的体力值可以往上走一到两步,所以除了最后一层外我们每一层都是他前一层或者前两层的所用最少体力加上当层所用体力。我们只需要知道到达倒数第一二层所用的最少体力,最后两者比较较小者赋值给dp[n]

    倒数第一层前面的每一层:dp[n]=min(dp[n-1],dp[n-2])+cost[n]
    最后一层:min(dp[cost.length-1],dp[cost.length-2])
    
  3. dp数组如何初始化

    int dp[]=new int[cost.length];
    dp[0]=cost[0];
    dp[1]=cost[1];
    
  4. 确定遍历顺序

    从前往后

    for(int n=2;n<=cost.length-1;n++){
         
    	dp[n]=Math.min(dp[n-1],dp[n-2])+cost[n];
    }
    
  5. 举例推导dp数组

    下标 0 1 2 3
    cost[i] 10 15 20 50
    dp[i] 0 0 10 30

    最后要的结果就是min(10,30),结果是30,成功。

    java代码如下:

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

第二种做法:

  1. 确定dp数组及下标意义

    dp[i]到达第i层所需体力值不能随时出发,只是单纯到达那个楼层

  2. 确定递推公式

    dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
    
  3. 初始化数组

    dp[0]=0;
    dp[1]=0;
    
  4. 确定递推方向

    从前到后

    for(int i=2;i<=cost.length;i++){
         
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
    }
    
  5. 举例推导dp数组

    输入:cost = [10, 15, 20]
    输出:15
    
    0 1 2 3
    dp 0 0 10 15

    答案15,正确

    java代码如下:

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

    不同路径

image-20210709104330947

image-20210709104421237

  1. 确定dp数组(dp table)以及下标的含义

    dp[m] [n]到达m行n列的方法的种数

  2. 确定递推公式

    由于每次都只能向下和想右移动一步,所以

    对于非边界上的就

    dp[m] [n]=dp[m-1] [n]+dp[m] [n-1]
    

    要注意当m或n为1的情况:

    if(m==1||n==1) return 1;
    
  3. dp数组如何初始化

    for(int i=0;i<=m-1;i++){
         dp[i][0]=1;}
    for(int i=0;i<=n-1;i++){
         dp[0][i]=1;}
    
  4. 确定遍历顺序

    从左到右从上到下

    for(int i=1;i<=m-1;i++){
         
    	for(int j=1;j<=n-1;j++){
         
    	}
    }
    
  5. 举例推导dp数组

    输入:m = 3, n = 2
    输出:3
    
    0 1 2
    0 1 1 1
    1 1 2 3

    答案3且正确

java代码:

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

优化为一维数组

输入:m = 3, n = 2
输出:3
0 1 2
0 1 1 1
1 1 2 3

因为每一次都只是要自己头顶上的数和右边的数,所以我们先从左到右再从上到下,换一列后,每一个元素都是左边的元素,所以在加自己头顶数就行。

java代码

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

不同路径Ⅱ

image-20210709145222886

image-20210709145321148

  1. 确定dp数组及下标的意义

    dp[m] [n]=到达m行n列的路径有多少种

  2. 确定递推公式

    依旧是等于正左和正上的位置到达路径种数之和

    dp[m] [n]=dp[m-1]dp[n]+dp[m] [n-1]

  3. dp数组如何初始化

    由于机器人中能向正右和下方走,则边界上如果前面有阻碍物挡住了,后面的就到达不了了。所以在边界遇到障碍之前都是1,遇到之后,都是0(遇到就break就行,数组初始化就是全为0)

     for(int i=0;i<m;i++){
         
         if(obstacleGrid[i][0]==1)
          break;
    	dp[i][0]=1;}
     for(int i=0;i<n;i++){
         
         if(obstacleGrid[0][i]==1)
          break;
    	dp[0][i]=1;}
    
  4. 确定遍历方向

    从上到下,从左往右

    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];
    	}
    }        
    
  5. 举例推导dp数组

    obstacleGrid数组 = [[0,0,0],[0,1,0],[0,0,0]]

    障碍

    dp数组

    0 1 2
    0 1 1 1
    1 1 0 1
    2 1 1 2

    return dp[m-1] [n-1];

    答案为2,推导没问题

class Solution {
   
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
   
    int m=obstacleGrid.length;
    int n=obstacleGrid[0].length;
    int dp[][]=new int[m][n];
    for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++){
   
         dp[i][0] = 1;  
    }
    for(int j=0;j<n&&obstacleGrid[0][j]==0;j++){
   
		dp[0][j]=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];
	}
}

整数拆分

image-20210709152847269

  1. 确定dp数组及下标的意义

    dp[n]为正整数n的拆分出来的整数的最大积

  2. 确定推导公式

    以从1遍历j,然后有两种渠道得到dp[i].

    ⼀个是j * (i - j) 直接相乘。

    ⼀个是j * dp[i - j],相当于是拆分(i - j)

    那有同学问了,j怎么就不拆分呢?

    j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。 那么从1遍历j,⽐较(i - j) * j和dp[i - j] * j 取最⼤的。

    递推公式:dp[i] = Math.max(dp[i], Math.max((i - j) * j, dp[i - j] * j));

  3. dp数组初始化

    dp[2]=1;

  4. 确定遍历方向

    先看递推公式dp[i]=Math.max(dp[i],Math.max((i-j)*j,dp[i-j] *j));

    dp[i]是依靠dp[i-j]的状态,所以遍历一定是从前往后遍历,先有dp[i-j]再有dp[i]。

    枚举j的时候,是从1开始的。i是从3开始,这样dp[i-j]就是dp[2]正好可以通过我们初始化的数值求出来。

    所以遍历顺序为:

    for(int i=3;i<=n;i++){
         
        for(j=1;j<i-1;j++)
        {
         
            dp[i]=Math.max(dp[i],Math.max((i-j)*j,dp[i-j]*j));
        }     
    }
    
  5. 举例推导dp数组

    n=6时

下标 2 3 4 5 6
dp[i] 1 2 4 6 9

image-20210709163049637

答案为9,推导成功

java代码如下:

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(dp[i-j]*j,(i-j)*j));
            }
        }
        return dp[n];
	}
}

不同的二叉搜索树

image-20210710111515491

  1. 确定dp数组以及下标含义

    dp[n]:1到n的二叉搜索树有多少种(等同于n个公差为1的递减数列的排列组合)

  2. 确定初始值

    int dp[]=new int[n+1];
    dp[0]=1;
    
  3. 确定推导公式

    二叉搜索树的原理就是先定了一个头结点,如果比他小都放左边,如果比他大都放右边。这时候就看排列组合,就是左边的排列种数乘以右边排列种数,就等于以该数组里该数字作为头节点的二叉搜索树的排列种数,则如果我们要求1到n的二叉搜索树有多少种,则要将以1到n做头节点的排列数加起来。

    dp[i]=dp[i]+dp[j-1]*dp[i-j]

    有人会问了,为啥是dp相乘啊,我们回到dp意义等同于n个公差为1的递减数列的排列组合,dp[3]就是1、2、3的二叉树种类,那如果我是5、6、7或者5、8、10呢?他们都有一个共同特点个数为3,将它们按小到大一排,纵观一看就可知道三者答案都是dp[3],所以dp[n]的意义还有n长度的集合(不可重复)的二叉树组成种类。

  4. 确定递推方向

    因为二叉树起码都有个头结点,所以从1开始推导,从小到大

    for(i=1;i<=n;i++){
         
        for(j=1;j<=i;j++)
        {
         
            dp[i]=dp[i]+dp[j-1]*dp[i-j]
        }
    }
    
  5. 举例推导公式

    二叉搜索树举例证明

    答案为14,正确

    java代码如下:

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

    背包问题

    image-20210710123215611

    1. 确定dp数组以及下标含义

      dp[i] [j]表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

    2. 确定推导公式

      我们来看个例子:

      image-20210710163113480

      快进到笔记本电脑行

      下面以同样的方式处理笔记本电脑。笔记本电脑重3磅,没法将其装入1磅或者2磅的背包,因此前两个单元格的最大价值仍然是1500美元。

      img

      对于容量为3磅的背包,原来的最大价值为1500美元,但现在你可以选择偷窃价值2000美元的笔记本电脑而不是吉他,这样新的最大价值将为2000美元。

      img

      **对于容量为4磅的背包,情况很有趣。**这是非常重要的部分。当前的最大价值为3000美元,你可不偷音响,而偷笔记本电脑,但它只值2000美元。

      img

      价值没有原来高,但是等一等,笔记本电脑的重量只有3磅,背包还有1磅的重量没用!

      img

      在1磅的容量中,可装入的商品的最大价值是多少呢? 你之前计算过!

      img

      根据之前计算的最大价值可知,在1磅的容量中可装入吉他,价值1500美元。因此,你需要做如下的比较:

      img

      你可能始终心存疑惑:为何计算小背包可装入的商品的最大价值呢?但愿你现在明白了其中的原因!**当出现部分剩余空间时,你可根据这些子问题的答案来确定余下的空间可装入哪些商品。**笔记本电脑和吉他的总价值为3500美元,因此偷它们是更好的选择。

      最终的网格类似于下面这样。

      img

      答案如下:将吉他和笔记本电脑装入背包时价值更高,为3500美元。

      我们每新增一个物品的选择,选择的是该物品是否放进背包。

      如果前面都容量不够,放不进去都用他的少一个类型该容量的dp数组来代替(dp[i-1] [j])

      如果容量够我们放进去之后容量变少,价值增加,并且为了利益最大化,剩下的容量我们用除了该物品的其他物品填满(因为物品前面已经装进去了,不重复),最后得出公式

      dp[i-1] [j-weigth[i]]+value[i]]

      并且每次都要判断一次他和少一个物品该容量的最大 价值比较(dp[i-1] [j]),就是他的头顶那行,因为有可能不放该物品放回之前的物品的价值反而更高。

      总结:每一行的每一个小格就是在判断到底放不放这行的物品。放不下就是头顶格子的价值。放的下要做比较,不放就是看头顶,放了就是看上一行左边(左多少就要剩余空间了,看j-weigth[i])。

      完整推导公式:

      dp[i] [j]=max(dp[i-1] [j],dp[i-1] [j-weigth[i]]+value[i]);

    3. 数组初始化

      dp[0] [weight]就是说只有一件商品选择和weight大小背包。

      所以如果背包大小能放进第一个商品,那他的目前的最大价值就是第一件商品价值,反之为0;

      for(i=1;i<=bagWeight;i++){
             
          if(i>=weight[0])
              dp[0][i]==value[i];
      }
      
    4. 确定递推方向

      二维数组而言什么方向都行

      我选先遍历物品,再遍历容量

      for(int i=0;i<=weight.size();i++){
             
          for(int j=0;j<=bagWeight;j++){
             
              if(j<weight[i]) dp[i][j]=dp[i-1][j];
              else dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
          }
      }
      
    5. 举例推导公式

    image-20210710163544650

答案就是35;

public class zeroonebag {
   
	    public int zeroonebag(int bagWeight,int weight[],int value[]) {
   
			int dp[][]=new int[weight.length][bagWeight+1];
	        for(int i=1;i<=bagWeight;i++){
   
	            if(i>=weight[0])
	            dp[0][i]=value[0];
	        }//初始化数组
	        for(int i=1;i<=weight.length-1;i++){
   
	            for(int j=1;j<=bagWeight;j++){
   
	 				if(j<weight[i])    dp[i][j]=dp[i-1][j];
	                else dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
	            }
	        }
			return dp[weight.length-1][bagWeight];        
		}
		public static void main(String[] args) {
   
			zeroonebag zeroonebag=new zeroonebag();
			int[] w = {
   1, 4, 3};
    		int[] v = {
   15, 30, 20};
    		int W = 4;
			System.out.println(zeroonebag.zeroonebag(W, w, v));
		}	    
}

01背包理论基础(滚动数组)

背包问题图:

image-20210802112308125

01背包问题优化

image-20210710123215611

问题提出:

我们看回我们举例推导的表格

image-20210710163544650

发现有很多重复的数字

⼀维dp数组(滚动数组)对于背包问题其实状态都是可以压缩的。 在使⽤⼆维数组的时候,递推公式:dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]); 其实可以发现如果把dp[i - 1]那⼀层拷⻉到dp[i]上,表达式完全可以是:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 于其把dp[i - 1]这⼀层拷⻉到dp[i]上,不如只⽤⼀个⼀维数组了,只⽤dp[j](⼀维数组,也可以理解是 ⼀个滚动数组)。 这就是滚动数组的由来,需要满⾜的条件是上⼀层可以重复利⽤,直接拷⻉到当前层。

  1. 确定dp数组定义

    在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值最大的值

  2. 一维dp数组递推公式

    dp[j]有两种选择,一个是取自己dp[i],一个是取dp[j-weight[i]]+value[i],指定是取最大的,毕竟是求最大价值

    dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  3. 数组初始化

    直接初始化就好了

  4. dp数组遍历顺序

    for(int i = 0; i < weight.length; i++) {
          // 遍历物品
    	for(int j = bagWeight; j >= weight[i]; j--) {
          // 遍历背包容量
    		dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
         }
    }
    

    我们发现和二维ap的写法中,遍历背包的顺序是不一样的。

    二维dp遍历是背包容量从小到打得,而一维dp遍历的时候,背包是 从大到小的。

    先看二维数组,遍历物品肯定是要从小到大,但是背包容量可以从大到小,因为我们看回那个推导公式

    image-20210710180358146

    他比较的是上一行的东西所以,同一行先后顺序并不重要。

    正序:

    image-20210710180551713

    倒序:

    image-20210710180114754

    但是一维数组就不一样,假如你按照正序

    举⼀个例⼦:物品0的重量weight[0] = 1,价值value[0] = 15

    如果正序遍历

    dp[1] = dp[1 - weight[0]] + value[0] = 15

    dp[2] = dp[2 - weight[0]] + value[0] = dp[1]+value[0]=30

    此时dp[2]就已经是30了,意味着物品0,被放⼊了两次,所以不能正序遍历。 为什么倒叙遍历,就可以保证物品只放⼊⼀次呢?

    倒叙就是先算dp[2]

    dp[2] = dp[2 - weight[0]] + value[0] = dp[1]+value[0]= 15 (dp数组已经都初始化为0)

    dp[1] = dp[1 - weight[0]] + value[0] = 15

    所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取⼀次了。

    再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量 嵌套遍历物品呢?

    不可以!

    我们就这么想吧!我们最后的出的答案大部分都是缝合怪,就是选了一件,剩下的容量还能再塞写东西进去的。然后我们回想一次我每一个for循环其实是往里面塞一件东西了,如果我们把背包容量作为遍历物品则就是一个循环后其实就一个循环就得出答案了。就选一个件就完事了。

    所以毫无疑问结果出来30

  5. 举例推导

    image-20210710201255850

    java代码如下:

    
    public class zeroonebagyouhua {
         
    	public static int zeroonebagyouhua(int bagWeight,int weight[],int value[]) {
         
    		int dp[]=new int[bagWeight+1];
    		dp[0]=0;		
    		for(int i=0;i<=weight.length-1;i++) {
         
    			for(int j=bagWeight;j>=weight[i];j--) {
         
    			dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
    			}
    		}
    		return dp[bagWeight];
    		
    	}
    	public static void main(String[] args) {
         
    		int[] w = {
         1, 4, 3};
    	    int[] v = {
         15, 30, 20};
    	    int W = 4;
    		System.out.println(zeroonebagyouhua.zeroonebagyouhua(W, w, v));
    	}	    
    }
    

分割等子集

image-20210710203733609

  1. 确定dp数组及下标含义

    我们这个问题可以转化成为背包问题,一个容量为sum/2的背包能否被物品装的刚刚好。

    dp[j]在j容量背包下最大价值

  2. 确定推导公式

    这题他的价值刚好也等于占的空间

    dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i])
    
  3. 数组初始化

    普通初始化就可以了

  4. 确定递推方向

    先商品后容量,而且容量要倒序

    放到这题,先数字后目标

     for(int i=0;i<=nums.length-1;i++){
         
    	for(int j=sum/2;j>=nums[i];j--){
         
    	}
    }
    
  5. 举例推导数组

    我们回想一下01背包,总结就是把书包空间尽量用完,并且价值最大,最后出来的值都是最大化利用空间的结果,所以这道题也差不多,用着几个数字最大化的利用空间凑出小于等于他的数,所以我们最后返回dp[sum/2]的值查看是否等于sum/2就可以了

    dp[i]的数值肯定小于等于i的

    如果相等证明刚好凑成。

    ⽤例1,输⼊[1,5,11,5] 为例,如图:

    image-20210710210021177

    最后dp[11] == 11,说明可以将这个数组分割成两个⼦集,使得两个⼦集的元素和相等。

    java代码如下:

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

最后一块石头的重量

image-20210711140106787

image-20210711140001297

1.确定dp数组及下标含义

和上一道题思路差不多。

分成两堆一样大小的石子,一样的时候就互相抵消,剩余0,如果不一样则有剩。

Int是将一个数值向下取整为最接近的整数的函数。

1/2=0,所以例如sum=23,sum/2=11。

化成背包问题,一个容量为sum/2的背包装有的最大容量,最后输出sum-2*dp[sum/2]。

dp[i]为容量为 i的背包最大能装多大

2.确定推导公式

dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);

3.dp数组初始化

正常初始化就可以了

4.确定推导顺序

先遍历物品后遍历背包,并且背包要采用倒序

for (int i = 0; i < stones.length; i++) {
    // 遍历物品
	for (int j = target; j >= stones[i]; j--) {
    // 遍历背包
		dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
	}
}

5.举例推导

输入:stones = [2,7,4,1,8,1]
输出:1

(2+7+4+1+8+1)/2=11

sum-dp[sum/2]

dp[11]=11,

23-2*11=1,答案正确。

java代码如下:

class Solution {
   
    public int lastStoneWeightII(int[] stones) {
   
	int dp[]=new int[15000];
	int sum = 0;
     for (int i = 0; i < stones.length; i++){
   
         sum += stones[i];
     } 
    int target = sum / 2;
 	for (int i = 0; i < stones.length; 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]*2;
    }
}

目标和

image-20210712202224295

image-20210712202254580

如何转化为01背包问题呢。

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = S

x = (S + sum) / 2

此时问题就转化为,装满容量为x背包,有⼏种⽅法。

⼤家看到(S + sum) / 2 应该担⼼计算的过程中向下取整有没有影响。 这么担⼼就对了,例如sum 是5,S是2的话其实就是⽆解的,所以:

if ((S + sum) % 2 == 1) return 0; // 此时没有⽅案
  1. 确定dp数组以及下标的含义

    dp[j]表示:填满j(包括j)这么大容积的包,有dp[j]种方法(凑够了加法的数,减法自然也凑好了)

  2. 确定递推公式

    不考虑nums[i]的情况下,填满容量为j-nums[i]的背包,有dp[j-nums[i]]中方法。

    那么只要搞到nums[i]的话,凑成dp[j]就有dp[j-nums[i]]种方法。

    举一个例子,nums[i]=2,dp[3],填满背包容量为3的背包,相应的就有多少种办法可以凑齐容量为5的背包。

    那么需要把 这些⽅法累加起来就可以了,dp[j] += dp[j - nums[i]]

    所以求组合类问题的公式,都是类似这种:

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

  3. dp数组如何初始化

    dp[0]=1

  4. 确定递推方向

    选择放外边,容量放里面,而且倒序

    for(int i=0;i<=nums.length-1;i++){
         
        for(int j=s;j>=nums[i];j--){
         
            dp[j]+=dp[j-nums[i]];
         }
    }
    
  5. 举例推导dp数组

    输⼊:nums: [1, 1, 1, 1, 1], S: 3

    bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

    微信图片_20210716193254

    java代码如下:

class Solution {
   
    public int findTargetSumWays(int[] nums, int target) {
   
        int dp[]=new int[150000];
        dp[0]=1;
        int sum=0;
        for(int i:nums){
   
            sum+=i;
        }
        int s=(sum+target)/2;
        if ((target + sum) % 2 == 1) return 0;
        for(int i=0;i<=nums.length-1;i++){
   
            for(int j=s;j>=nums[i];j--){
   
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[s];
    }
}

一和零

image-20210712212941690

最大子集这里指个数,不是大小。如{“10”,“0001”,“1”,“0”}>{“0001”,“1”}

该子集中最多有m个0和 n个1。

  1. 确定dp数组及下标意义

转为01背包问题

dp[i] [j] [k]在str[i]中用容量m为j,n为k的背包装的最多价值

2.确定推导公式

dp[i] [j] [k]=max(dp[i] [j] [k],dp[i] [j-counto(str[i])] [k-(str[i].lemgth-counto(str[i]))])

3.初始化dp数组

普通初始化就可以了

4.确认递推方向

先正序遍历物品

再倒序遍历容量

for(int i=0;i<=str.length-1;i++){
   
    for(int j=m;j>=0;j--){
   
        for(int k=n;k>=0;k--){
   
            dp[i] [j] [k]=max(dp[i] [j] [k],dp[i] [j-counto(str[i])] [k-(str[i].lemgth-counto(str[i]))]+1)
        }
    }
}

这个代码看起来太臃肿了,而且还要多一个counto的函数来计算1的个数,我们优化一下,变成

其实就是优化了,变成只含背包容量的数组,dp[i] [j]两个背包,一个装0一个装1。

for (string str : strs) {
    // 遍历物品
 	int oneNum = 0, zeroNum = 0
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值