01背包

509. 斐波那契数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Solution {
    public int fib(int n) {
        if(n<2) 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];
        return dp[n];
    }
}

70.爬楼梯

思路:走第i层时,从i-1到i只有一种方法,从i-2到i也只有一种方法,所以从0到i-1到i有dp[i-1]种方法,从0到i-2到i有dp[i-2]种方法。所以从0到dp[i]有dp[i-2]+dp[i-1]种方法,所以dp[i]=dp[i-1]+dp[i-2]。其实也能转换为完全背包来做,物品分别是1,2。都可以无限次用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Solution {
    public int climbStairs(int n) {
        if(n<=2) return n;
        int [] dp = new int[n+1];
        dp[1]=1;dp[2]=2;
        for(int i=3;i<=n;i++)
            dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }
}

746. 使用最小花费爬楼梯

思路:主要是dp数组如何构建。dp数组的含义是,从 i 跳走必须花 dp[i] 的钱,所以最后是只有三个数的话,要跳出这三个数:dp[2]=( dp[2-1] , dp[2-2] )+ cost[i] ;dp 数组初始化好直接返回就行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        if(cost == null || cost.length == 0)   return 0;
        if(cost.length == 1)   return cost[0];
        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 - 2], dp[i - 1]) + cost[i];
        }
        return Math.min(dp[cost.length - 1], dp[cost.length - 2]);
    }
}

62.不同路径

思路:每个格子 i,j 都是从 i-1,j 或 i,j-1 走过来,所以 dp[ i ] [ j ] = dp[ i-1 ] [ j ] + dp[ i ] [ j - 1 ] ;上面、左边两行都要初始化为1,只能唯一,因为只有右移或者下移。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Solution {
    public int uniquePaths(int m, int n) {
        if(m <= 0 || n <= 0) return 0;
        if(m == 1 || n == 1)    return 1;
        int[][] dp = new int[m][n];
        for(int i=0;i<n;i++)    dp[0][i]=1;
        for(int i=0;i<m;i++)    dp[i][0]=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];
    }
}

63. 不同路径 II

这次只出了一个越界,其它全对,思路也是自己的,直接AC,兴奋。

思路:初始化要注意,只能向右👉,下👇走,所以在初始化第一行、第一列的时候如果遇见障碍物,该位置与后面所有直线位置dp[i][j] 都为0,注意初始化完成第一行、第一列,所以从 (1,1) 坐标开始初始化dp,否则就越界了。在其他位置遇见障碍物就直接 dp[i][j] = 0 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length,n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        int flag = 1;
        for(int i=0;i<m;i++){
            if(obstacleGrid[i][0] == 1)     flag = 0;
            dp[i][0] = flag;
        }  
        flag = 1;
        for(int i=0;i<n;i++){
            if(obstacleGrid[0][i] == 1)     flag = 0;
            dp[0][i] = flag;
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j] == 1)     dp[i][j] = 0;
                else    dp[i][j] = dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[m-1][n-1];
    }
}

343. 整数拆分

思路:拆分成两个、两个或两个以上个、并更新以便取得最大值。初始化 dp[2]=1 .为什么?dp 数组是记录 i 值对应的最大《拆分积》。为什么内循环的条件是 j≤i-j ? 相当于剪枝操作, [ i-j , i ] 的数在 [0,i-j ] 已经乘过了,比如数字4, 1~4 是在1~2和2~4相乘,而不是两者相加=4。1~4和1~4会越界。

主要代码:

1
dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));

为什么需要dp[i]、j*(i-j)、jdp[i-j]取最大值,dp[i] 是为了取得最大值,因为dp[i]是多个比较的结果。而j(i-j)是两个数的乘积,经过循环就是求得其分为两数的最大值,jdp[i-j] 是拆分为两个以上,dp是i-j拆分为2+个数最大的积,那么是可能拆分为n个数的,会涵盖所有拆分可能,并剩余值。


96. 不同的二叉搜索树

思路:很抽象,先把0、1个节点只有一种可能确定好,再是从dp[2]、dp[3]来找递推公式。

dp[2]在1为头结点时,只能2挂在右边这种可能,2为头结点时,只能1挂在左边,dp[2]=1+1=2 。

dp[3]在1为头结点时,可以2、3依次深度排序,也可以1、3、2,所以两种可能。

dp[3]在2为头结点时,只能左右分别挂一个,所以只有一种可能。

dp[3]在3为头结点时,.类似1为头结点,两种可能。sum=2+1+2=5种可能。

dp[3] 递推公式:

1为头 sum+=dp[0]*dp[2]=2左孩子0个节点,右孩子2个节点。

2为头 sum+=dp[1]*dp[1]=1,左右孩子分别1个。

3为头 sum+=dp[2]*dp[0]=2,左孩子2个,右孩子一个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0]=dp[1]=1;
        for(int i=2;i<=n;i++){
            for(int j=0;j<=i-1;j++)
                dp[i]+=dp[i-j-1]*dp[j];
        }
        return dp[n];
    }
}

416. 分割等和子集

思路:以01背包背包的思路,dp[ i ][ j ] 的 i 代表从0到i选择,j代表背包容量为j,dp[i][j] 代表这样的前置条件下最大能装下的值。

主要还是数学逻辑:当背包容量为 sum/2 时,能在nums找到和为sum/2,也就是dp[ i ][ j ] =sum/2。就找到了,为什么?因为只要有和能为 sum 的一半,那么一定剩下的数字和一定也为 sum/2 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0,target;
        for(int i=0;i<nums.length;i++)
            sum += nums[i];
        if(sum % 2 == 1)    return false;
        target = sum/2;
        int[][] dp = new int[nums.length][target+1];
        for(int j = nums[0]; j <= target;j++)
            dp[0][j] = nums[0];
        for(int i = 1;i < nums.length;i++){
            for(int j = 1;j <= target;j++){
                if(j >= nums[i])   dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
                else    dp[i][j] = dp[i-1][j];
            }
        }
        return dp[nums.length-1][target] == target;
    }
}

1049. 最后一块石头的重量 II

思路:还是跟上题一样,就是数学逻辑。用sum/2的容量来看最大能拿到多大,然后sum-max,也就是另一半的值,这两个值相减就出来了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum=0,target;
        for(int i:stones)
            sum += i;
        target=sum/2;
        int[][] dp = new int[stones.length][target+1];
        for(int i=stones[0];i<target+1;i++)
            dp[0][i] = stones[0];
        for(int i=1;i<stones.length;i++)
            for(int j=1;j<target+1;j++)
                if(stones[i] > j)   dp[i][j] = dp[i-1][j];
                else    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
        return sum-dp[stones.length-1][target]-dp[stones.length-1][target];
    }
}

一维数组(滑动数组):相当于二维 dp[ i ] [ j ] = dp[ i-1 ] [ j ] 直接省去这一步骤,而是直接把上一层拷贝下来。

1
2
3
4
5
6
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
//变化
dp~~[i]~~[j] = Math.max(dp~~[i]~~[j],dp~~[i]~~[j-stones[i]]+stones[i]);把多行放在一行里面滑动操作了。
//简化
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);

代码变为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int i : stones) {
            sum += i;
        }
        int target = sum >> 1;
        //初始化dp数组
        int[] dp = new int[target + 1];
        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 - 2 * dp[target];
    }
}

在内循环直接条件限制为 j ≥ stones 其实是像第一层初始化这样理解:装不下就不装,就不变,保持等于原来的dp[ j ],也就是现在的dp[ j-1 ]。直接把二维数组时候的 if-else 省略了。


494. 目标和

思路:将这个数组分为两个子集:left、right,left+right=sum,left-right=target。两个方程相加2 * left=(sum+target)—> left=(sum+target)/ 2 。确定了左边就确定了右边,所以找到left 为总和的组合数为 n,整体组合数就是n。

区别:与以前不同在于,求可能可能性的总数,前几道题是最大价值。都是01背包问题。主要区别在于循环体、初始化。

  • 特殊情况1:容量为nums[0]:不放、放入nums[0]一共两种情况对吧? 错了,只能直接nums[0]–>nums[0],在0-0这个区间只有 nums[i] 能放进去,那么只有恰好nums[0]=size时候这种放法。

  • 特殊情况2:size为0时(初始化),此时就有“一种方法”就是“不放入”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0, leftBagSize;
        for(int i:nums) sum += i;
        if( (sum + target) % 2 == 1)    return 0;
        leftBagSize = Math.abs( (sum + target) / 2 );  //考虑负数
        int[] dp = new int[leftBagSize+1];//这里不是最大价值,是组合个数。

        //初始化
        dp[0] = 1;

        for(int i = 0; i < nums.length; i++){
            for(int j = leftBagSize; j >= nums[i]; j--){
            //求的是总和(所有可能性)要把不+nums[i]的也算上
                dp[j] += dp[j-nums[i]];
            }
        }

        return dp[leftBagSize];
    }
}


474.一和零

思路:理解题意!找出并返回 strs最大子集的长度,最大子集指的是最长的子集长度,即最多可以包含几个字符串作为子集。那么dp[i][j] 也就是个三维的01背包,进一个字符串先判断几个 0 几个 1 。然后逆序遍历来更新值,从m,n开始,i≥one,是为了至少有这个容量来装,防止 max 里面 index = -1 越界,Math.max里面更新很巧妙,如果是去掉当前串的 01 个数,就是看此时(去当前串01)容量是否已经初始化过,即是否装过了,装过了还能装就是1+1=2次。如果没装过就是0+1,相当于初始化了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int [m+1][n+1];
        for(String str:strs){
            int zero = 0, one = 0;
            for(char ch:str.toCharArray()){
                if(ch == '0')
                    zero++;
                else
                    one++;
            }
            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];
    }
}

完全背包

指的是物品可以无限次被使用,此时就是正序遍历,先遍历“物品”还是“背包”要看是 排列 还是 组合组合 先 遍历 物品,因为这种遍历方式物品有先后顺序,而排列没有,则会打乱顺序还有结果。

518. 零钱兑换 II

思路:完全背包问题(组合:无序)每个物品可以用无限次,正向遍历。容量为0的时候不放入也是一种方法。这样在以后j=coins[i]时可以dp[j]+=dp[j-coins[i]] 即dp[0] 即1;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Solution {
    public int change(int amount, int[] coins) {
        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];
    }
}

377. 组合总和 Ⅳ

思路:求的是排列(强调顺序,不同顺序为不同排列),排列先遍历背包,再遍历物品。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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];
    }
}

322. 零钱兑换

思路:满足条件的,最小硬币个数。无限使用 == 完全背包,求的是组合(不强调顺序)所以是先遍历物品。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Solution {
    public int coinChange(int[] coins, int amount) {
        if(amount == 0) return 0;
        int max = Integer.MAX_VALUE;
        int[] dp = new int[amount+1];
        dp[0] = 0;
        for(int i=1; i<=amount; i++)//初始化
            dp[i] = max;
        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-coins[i]]+1,dp[j]); 
        return dp[amount] == max ? -1 : dp[amount];
    }
}

279.完全平方数

思路:完全背包,自己构建一个长度为 (int) Math.sqrt(n) 的物品数组,为什么?因为target最多被 sqrt(target) 平方出 target,而自己也要构建物品数组,i对应i*i ;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        int len = (int)Math.sqrt(n);
        int[] dp = new int[n+1];
        dp[0] = 0;
        for(int i = 1; i <= n; i++)
            dp[i] = max;
        for(int i = 1; i <= len; i++)
            for(int j = i*i; j <= n; j++)
                if(dp[j-i*i] != max)  dp[j] = Math.min(dp[j],dp[j-i*i]+1);
        return dp[n];
    }
}
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// 0 1 2 3 4 5 6 7 8 9 10 11 12
// 0 1 2 3 1 2 3 4 2 3 4  5  3
// 0 1 2 3 1 2 3 4 2 1 2  3  3

139.单词拆分

思路:用字符串截取、List的contains方法来判断是否存在该截取的子串,存在且 d[j] 为 true 就 dp[i] = true .这样做的好处是每个位置每个子串都处理到了,而如果只移动i,等存在 并且 dp[i] = true 时,才移动j,那么在[“aaa”,“aaaa”] , s=“aaaaaaa” 这个测试用例就过不去,因为走的是两个aaa,剩下aa就走不动了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 < i; j++)
                if(wordDict.contains(s.substring(j,i)) && dp[j]) dp[i] = true;
        return dp[s.length()];
    }
}

持续更新…….
如有错误,敬请斧正!!!