70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

f(x) = f(x-1) + f(x-2)
f(0)=1 f(1)=1 f(2)=2  ---> f(3)=3 ---> f(4)=5
设一个存放三个整数的数组,分别保存 f(x-2) f(x-1) f(x) 。每次来了新的 f(x) ,前两个向前移动,然后把它放到数组末尾。
 public int climbStairs(int n) {
        // 分别存放 f(x-2) f(x-1) f(x)
        int[] arr = new int[]{0,1,2};
        if (n<=2) {
            return arr[n];
        }
        for (int i=3;i<=n;i++) {
            //  f(x) = f(x-1) + f(x-2)
            int fx = arr[2] + arr[1];
            // fx 放在末尾,数组往前移
            arr[0] = arr[1];
            arr[1] = arr[2];
            arr[2] = fx;
        }
        return arr[2];
    }

 

53. 最大子数组和

dp[i] 代表表示以 nums[i] 结尾 的 连续 子数组的最大和。 
要么把 nums[i] 附加到前一个连续最大和的后面,要么从 nums[i] 重新开始   
  • dp[i]=max{nums[i],dp[i1]+nums[i]} 
  • dp[0]= nums[0]
class Solution {
    public int maxSubArray(int[] nums) {
        int result = nums[0];
        int dp[] = new int[nums.length];
        dp[0] =  nums[0];
        for(int i=1;i<nums.length;i++){
           //递推公式
           dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
           //result用来记dp数组里的最大数值
           if(dp[i]>result)
                result = dp[i];
        }
        return result;
    }
}

 

96. 不同的二叉搜索树

如果整数1 ~ n中的 k 作为根节点值,则 1 ~ k-1 会去构建左子树,k+1 ~ n 会去构建右子树。
左子树出来的形态有a 种,右子树出来的形态有 b 种,则整个树的形态有 a∗b 种。

题解参考: https://leetcode.cn/problems/unique-binary-search-trees/solution/shou-hua-tu-jie-san-chong-xie-fa-dp-di-gui-ji-yi-h/

class Solution {

    public int numTrees(int n) {
        // 最后要返回 dp[n]
        int dp[] = new int[n+1];

        // dp[0] 代表用 0 个数去构建左子二叉搜索树,dp[n-1] 代表用 n-1 个数(除掉根节点)去构建右子二叉搜索树
        // dp[n] = dp[0]*dp[n-1] + dp[1]*dp[n-2] + dp[2]*dp[n-3] + ..... + dp[n-2]*dp[0] + ... dp[n-1]*dp[0]
        // 一共有 i 个数,左子树用 j 个,右子树用 i-j-1 个  dp[i] = dp[j]dp[i-j-1]之和 0<=j<=i-1(左子树可以用0个到i-1个(除根节点))

        // 空树,只有一种
        dp[0] = 1;
        // 一个数字的,只有一种
        dp[1] = 1;

        // dp[0] 和 dp[1] 已知,所以从 dp[2] 开始
        for (int i=2;i<=n;i++) {
            // 左子树可以用0个到i-1个(除根节点)
            for (int j=0;j<=i-1;j++) {
                dp[i] += dp[j]*dp[i-j-1];
            }
        }
        return dp[n];
    }

}

 

198. 打家劫舍

dp[0] = nums[0];
// 注意 dp[1] 即只有两家的时候,这时候最大是偷这两家之中大的那个
dp[1] = Math.max(nums[0], nums[1]);
// dp[i-1] 是指这一间不偷,i-1间偷;dp[i-2]+nums[i] 是 i-2 间偷,i-1 间不偷,这间偷
dp[i] = max{dp[i-1], dp[i-2]+nums[i]}

dp[i] 是 0~i 的最优解

https://leetcode.cn/problems/house-robber/solution/dong-tai-gui-hua-jie-ti-si-bu-zou-xiang-jie-cjavap/

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

        if (nums.length==1) {
            return nums[0];
        }

        if (nums.length==2) {
            return Math.max(nums[0], nums[1]);
        }

        int dp[] = new int[nums.length];
        dp[0] = nums[0];
        // 注意 dp[1] 即只有两家的时候,这时候最大是偷这两家之中大的那个
        dp[1] = Math.max(nums[0], nums[1]);
        
        // dp[i-1] 是指这一间不偷,i-1间偷;dp[i-2]+nums[i] 是 i-2 间偷,i-1 间不偷,这间偷
        // dp[i] = max{dp[i-1], dp[i-2]+nums[i]}
        for (int i=2;i<nums.length;i++) {
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[nums.length-1];
    }
}

416. 分割等和子集

官方题解就很好

https://leetcode.cn/problems/partition-equal-subset-sum/solution/fen-ge-deng-he-zi-ji-by-leetcode-solution/

计算之前就排除掉的一些情况:

1、数组元素个数 n 小于 2,即只有 0 个 1 个元素,那么不可能被划分为两部分

2、如果数组元素总和是奇数,那么划分为两部分,使得两部分的和相等是不可能的

3、令 target = sum/2 即数组所有元素和的一半。最大元素记为 maxNum。那么:如果 maxNum>target,答案显然是 false ; 如果 maxNum = target ,那么显然是 true

  • i 是从 0 到 输入数组的长度
  • j 是 0~数组总和/2,即为分割后成两部分后,各部分的和,以下称作 target
  • dp[i][j] 是从数组的 [0,i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j
  • 最后的结果即为 dp[n-1][target] ,从数组下标 [0,n-1] 范围内,即数组的全部范围内,选取若干个整数,是否能使其和为 target

递推公式:

  • 如果 j<nums[i],即要选取的当前数字 nums[i] 已经比目标和 j 大的话,nums[i] 这个数字一定不能选,那么 dp[i][j] = dp[i-1][j]
  • 如果 j>=nums[i],即要选取的当前数字 nums[i] 不比目标和 j 大的话,nums[i] 这个数字可选可不选
    • 如果选 nums[i] 这个数字,[0,i-1] 仅需完成目标和 j-nums[i],那么 dp[i][j] = dp[i-1][j-nums[i]]
    • 如果不选 nums[i] 这个数字, 就要靠 [0,i-1] 去完成目标和 j,那么 dp[i][j] = dp[i-1][j]
    • 总结上面两个,那么 dp[i][j] =  dp[i-1][j-nums[i]] | dp[i-1][j]

边界条件:

  • 对于目标和 j 是 0,那么在数组下标 [0,i] 范围内,一定存在一种选取方案使得被选取的正整数和等于 0 —— 即什么都不选
  • 对于 i=0 ,那么在数组下标 [0,0] 范围内,只有一个 nums[0] 可以选,即 dp[0][nums[0]] = true,dp[0][其它大小的j] = false
class Solution {
    public boolean canPartition(int[] nums) {

        // 元素数量 1 或 2,不可以划分为两部分
        if (nums.length < 2) {
            return false;
        }

        int sum = 0;
        int maxNum = Integer.MIN_VALUE;
        for (int num : nums) {
            if (num>maxNum) {
                maxNum = num;
            }
            sum += num;
        }
        // 如果总和是奇数,那么划分为两部分,使得两部分的和相等是不可能的
        if (sum % 2 != 0) {
            return false;
        }

        // target 是总和的一半
        int target = sum/2;

        // 最大的数就比 target 大了,不可能分为和相等的两部分
        if (maxNum > target) {
            return false;
        }
        // 最大的数等于 target,那么最大的数一组,其它的数一组,就是结果
        if (maxNum == target && sum - maxNum == target) {
            return true;
        } 
        
        boolean[][] dp = new boolean[nums.length][target+1];
        // 边界条件
        for (int i=0; i<nums.length;i++) {
            // 对于目标和 j 是 0,那么在数组下标 [0,i] 范围内,一定存在一种选取方案使得被选取的正整数和等于 0 —— 即什么都不选
            dp[i][0] = true;
        }
        for (int j=0; j<=target;j++) {
            // 对于 i=0 ,那么在数组下标 [0,0] 范围内,只有一个 nums[0] 可以选,即 dp[0][nums[0]] = true,dp[0][其它大小的j] = false
            if (nums[0] <= target) {
                dp[0][nums[0]] = true;
            }
            else {
                dp[0][j] = false;
            }
        }
for (int i=1;i<nums.length;i++) { for (int j=1;j<=target;j++) { if (nums[i] < j) { /* 如果 j>=nums[i],即要选取的当前数字 nums[i] 不必目标和 j 大的话,nums[i] 这个数字可选可不选 1.如果选 nums[i] 这个数字,[0,i-1] 仅需完成目标和 j-nums[i],那么 dp[i][j] = dp[i-1][j-nums[i]] 2.如果不选 nums[i] 这个数字, 就要靠 [0,i-1] 去完成目标和 j,那么 dp[i][j] = dp[i-1][j] 总结上面两个,那么 dp[i][j] = dp[i-1][j-nums[i]] | dp[i-1][j] */ dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]]; } else { // 如果 j<nums[i],即要选取的当前数字 nums[i] 已经比目标和 j 大的话,nums[i] 这个数字一定不能选,那么 dp[i][j] = dp[i-1][j] dp[i][j] = dp[i-1][j]; } } } return dp[nums.length - 1][target]; } }

 

72. 编辑距离

官方题解:https://leetcode.cn/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/

我们可以对任意一个单词进行三种操作:

  1. 插入一个字符;
  2. 删除一个字符;
  3. 替换一个字符。

题目给定了两个单词,设为 A 和 B,这样我们就能够六种操作方法。

但我们可以发现,如果我们有单词 A 和单词 B:

  • 对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;
  • 同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;
  • 对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

这样以来,本质不同的操作实际上只有三种

  1. 单词 A 中插入一个字符;
  2. 单词 B 中插入一个字符;
  3. 修改单词 A 的一个字符。

当我们获得 D[i][j-1],D[i-1][j] 和 D[i-1][j-1] 的值之后就可以计算出 D[i][j]。

  • D[i][j-1] 为 A 的前 i 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们在 A 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i][j-1] + 1
  • D[i-1][j] 为 A 的前 i - 1 个字符和 B 的前 j 个字符编辑距离的子问题。即对于 A 的第 i 个字符,我们在 B 的末尾添加了一个相同的字符,那么 D[i][j] 最小可以为 D[i-1][j] + 1
  • D[i-1][j-1] 为 A 前 i - 1 个字符和 B 的前 j - 1 个字符编辑距离的子问题。即对于 B 的第 j 个字符,我们修改 A 的第 i 个字符使它们相同,那么 D[i][j] 最小可以为 D[i-1][j-1] + 1。特别地,如果 A 的第 i 个字符和 B 的第 j 个字符原本就相同,那么我们实际上不需要进行修改操作。在这种情况下,D[i][j] 最小可以为 D[i-1][j-1]

边界情况

一个空串和一个非空串的编辑距离为

  • D[i][0] = i ,从 word1 到一个空串,相当于对 word1 执行 i 次删除操作
  • D[0][j] = j ,从一个空串到 word1,相当于对 word1执行 j 次插入操作。
class Solution {
    public int minDistance(String word1, String word2) {

        // dp[i][j] 是 word1 的 1...i 到 word2 的 1...j 的编辑距离(0 表示空串)
        // 注意 dp 数组的大小是 length+1 ,而不是 length,因为 0 的位置要表示空串int[][] dp = new int[word1.length()+1][word2.length()+1];
        
        // 边界条件
        // dp[i][0]=i 从word1 的 0...i 到空串编辑距离,删除 i 次
        // dp[0][j]=j 从空串到 word2 的 0...j 的编辑举例,插入 j 次
        for (int i=0;i<word1.length()+1;i++) {
            dp[i][0] = i;
        }
        for (int j=0;j<word2.length()+1;j++) {
            dp[0][j] = j;
        }

        // 如果 word1[i] 和 word2[j] 相等,dp[i][j] = min{dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1}
        //                                         = 1 + min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}
        // 如果 word1[i] 和 word2[j] 相等,dp[i][j] = min{dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]}
        //                                         = 1 + min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]-1}
        for (int i=1;i<word1.length()+1;i++) {
            for (int j=1;j<word2.length()+1;j++) {
                // 为什么是 charAt i-1 和 charAt j-1 呢?
                // 因为 dp 的 0 被空串占了,dp[i][j]表示 word1第i个字符,word2第j个字符,word1 和 word2 字符下标又是从0开始的,所以是 charAt(i-1) 和 charAt(j-1)
                if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]-1);
                }
                else {
                    dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]);
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

 

1143. 最长公共子序列

dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子序列的长度

用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0

因此 dp 对应的下标比字符串的下标多1

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列
        // dp[0] 用来表示空字符串的情况了,所以要 length+1
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        // 要由左上角得来,所以从左上到右下
        // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1
        // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
        int maxLen = 0;
        for (int i=1;i<text1.length();i++) {
            dp[i][0] = 0;
        }
        for (int j=1;j<text2.length();j++) {
            dp[0][j] = 0;
        }
        for (int i=1;i<text1.length()+1;i++) {
            // dp[0] 用来表示空字符串的情况了,所以要 length+1
            // dp[1] 才是 text.charAt(0),所以要 -1 这里
            char c1=text1.charAt(i-1);
            for (int j=1;j<text2.length()+1;j++) {
                char c2=text2.charAt(j-1);
                if (c1 == c2) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    if (dp[i][j] > maxLen) {
                        maxLen = dp[i][j];
                    }
                }
                else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return maxLen;
    }
}

 

最长公共子串

和最长公共子序列类似

dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子串的长度

用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0

只是状态转移方程,在比较的当前字符不等时,不一样

  • dp[i][j] = dp[i-1][j-1]+1, text1(i-1)==text2(i-1)
  • dp[i][j] = 0, text1(i-1) != text2(i-1)
要求出结果子串,已知 dp[i][j] i j 相当于这个长度的字符串的结束下标
而且最后的结果不论在 text1 还是 text2 都是一样的,所以
用一个 map 保存 dp 当前最长公共子串的在 text1 的起始下标
map<int, int> key为 dp[i][j] 的i,即text1的子串结束下标,value为text1(0:i)的最长公共子串起始下标
如果走到 text1(i-1)==text2(i-1) 判断分支,那么 dp[i][j] = dp[i-1][j-1]+1
i 处的起始下标从 i-1 处得来
import java.util.HashMap;
import java.util.Map;

public class TestLongestCommonSubStr {

    public static void main(String[] args) {
        String res = longestCommonSubsequence("rghello123wohellord66665rld", "vfhello123abc4hellord66665");
        System.out.println(res);
    }

    public static String longestCommonSubsequence(String text1, String text2) {
        // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列
        // dp[0] 用来表示空字符串的情况了,所以要 length+1
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        // 要由左上角得来,所以从左上到右下
        // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1
        // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
        // key为dp[i][j]的i,即text1的子串结束下标,value为text1(0:i)的最长公共子串起始下标
        Map<Integer, Integer> map = new HashMap<>();
        int maxLen = 0;
        int[] maxLenIndex = new int[2];
        for (int i=1;i<text1.length();i++) {
            dp[i][0] = 0;
        }
        for (int j=1;j<text2.length();j++) {
            dp[0][j] = 0;
        }
        for (int i=1;i<text1.length()+1;i++) {
            // dp[0] 用来表示空字符串的情况了,所以要 length+1
            // dp[1] 才是 text.charAt(0),所以要 -1 这里
            char c1=text1.charAt(i-1);
            for (int j=1;j<text2.length()+1;j++) {
                char c2=text2.charAt(j-1);
                if (c1 == c2) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    // 起始下标从 i-1 来
                    int istart = put(map, i-1, i);
                    if (dp[i][j] > maxLen) {
                        // 最长公共子串的长度
                        maxLen = dp[i][j];
                        // 最长公共子串在 text1 的起始下标
                        maxLenIndex[0] = istart;
                        // 最长公共子串在 text1 的结束下标
                        maxLenIndex[1] = i;
                    }
                }
                else {
                    // 子序列
                    //dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                    // 子串
                    dp[i][j] = 0;
                }
            }
        }
        return text1.subSequence(maxLenIndex[0], maxLenIndex[1]).toString();
    }

    private static int put(Map<Integer, Integer> map, int istartFrom, int i) {
        // istartFrom 起始下标从 istartFrom 来
        Integer istart = map.get(istartFrom);
        if (istart == null) {
            // 当 i=1 时会走到这里
            map.put(i, i);
            return i;
        }
        else {
            // 放入 结束下标-起始下标
            map.put(i, istart);
            return istart;
        }
    }
}

 

718. 最长重复子数组

dp[i][j] 表示 nums1[0:i] nums2[0:j]的最长公共子数组

dp[0][0] 是两个数组为空的场景,所以 dp 初始化时要 new int[nums1.length][nums2.length]

if nums1[i]==nums2[j] dp[i][j] = dp[i-1][j-1]+1 从左上到右下

else dp[i][j]=0

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int dp[][] = new int[nums1.length+1][nums2.length+1];
        // dp[i][j] nums1[0:i] nums2[0:j]的最长公共子数组
        // dp[0][0] 是两个数组为空的场景
        // if nums1[i]==nums2[j] dp[i][j] = dp[i-1][j-1]+1 从左上到右下
        // else dp[i][j]=0
        for (int i=0;i<nums1.length+1;i++) {
            dp[i][0] = 0;
        }
        for (int j=0;j<nums2.length+1;j++) {
            dp[0][j] = 0;
        }
        int maxLen = 0;
        for (int i=1;i<nums1.length+1;i++) {
            for(int j=1;j<nums2.length+1;j++) {
                if (nums1[i-1] == nums2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    if (dp[i][j] > maxLen) {
                        maxLen = dp[i][j];
                    }
                }
                else {
                    dp[i][j] = 0;
                }
            }
        }
        return maxLen;
    }
}

 

279. 完全平方数

完全背包问题

class Solution {
    public int numSquares(int n) { 

        // 为了使数组下标和数字一一对应
        int[] dp = new int[n+1];
        // 题目要求 n>=1
        for (int i=1;i<=n;i++) {
            // 最坏情况是拆解为 1+1+1+1+......
            dp[i] = i;
            // j*j<=i 是因为要去掉 j*j 这个完全平方数,剩下的数要是一个子问题
            // 而 j*j==i 就是i自己本身就是个完全平方数的情况
            for (int j=1;j*j<=i;j++) {
                // 之前的子问题 + j*j这个完全平方数
                dp[i] = Math.min(dp[i], dp[i-j*j] + 1);
            }
        }

        return dp[n];
    }
}

 

322. 零钱兑换

完全背包问题

一、完全背包:每种硬币可以选择多个

dp 长度 amount+1,dp[0]=0,其它初始值为 dp[i]=amount+1,可以边循环边设置

最后返回 dp[amount]>amount? -1:dp[amount];

dp[i] = Math.min(dp[i], dp[i-coins[j]]+1)

外层循环金额从小到大,内层循环硬币

1. i=2 时,只有在 coin=2 时,dp[2] = min(13, dp[0]+1) = 1,即选择 2 这一个硬币

 2.i=4 时,在 coin=2,dp[4] = min(13,dp[2]+1) = 2,即选择两个面值 2 的硬币

 i=4 时,在 coin=4,dp[4] = min(2, dp[0]+1) = 1,即只选一个面值为 4 的,比选两个面值 2 的硬币个数更少

 3. i=6 时,在 coin=2,  dp[6] = min(13, dp[4]+1) = 2,即选择面值 2,4

 i=6 时,在 coin=4, dp[6] = min(2, dp[2]+1) = 2, 还是选择面值 2, 4

 i=6,在 coin=6, dp[6]=min(2, dp[0]+1)=1,只选一个面值 6 的比选择 2,4 的硬币个数更少

。。。。。

i=12,在 coin=2,dp[12]=min(13, dp[10]+1) = 3,即选择 4,6,2

i=12,在 coin=4,dp[12]=min(3, dp[8]+1) = 3,即选择 4,4,4 或者说 2,6,4

 i=12,在 coin=6,dp[12]=min(3, dp[6]+1) = 2,即选择 6,6,比前面两种选择硬币个数更少

二、01背包:每种硬币只可以选择一个

dp 长度 amount+1,dp[0]=0,其它初始值为 dp[i]=amount+1 ,要提前全部设置好。因为金额在内层循环不能边循环边设置。

最后返回 dp[amount]>amount? -1:dp[amount];

dp[i] = Math.min(dp[i], dp[i-coins[j]]+1)

外层循环硬币,内循环金额从大到小(大不依赖小,即每种一个)

在 coin = 2,金额从大到小全循环了一遍之后

  • dp[2]=min(13, dp[0]+1)=1 (取当前的硬币 2)

在 coin = 4,金额从大到小全循环了一遍之后

  • 在金额为 8 即 dp[8] 时,由于从大到小,dp[8-4]=dp[4] 还没产生,而从小达到就会先产生 dp[4]=1,就可以选 4,4 。这就是为什么从大到小可以防止重复选取。
  • dp[6]=min(13, dp[2]+1)=2 (取上一轮的硬币 2,和当前的硬币 4)
  • dp[4]=min(13, dp[0]+1)=1 (取当前的硬币 4)

在 coin = 6,金额从大到小全循环了一遍之后

  • dp[12] = min(13, dp[6]+1) = 3 (取 dp[6] 的 2,4 ,和当前的硬币 6)
  • dp[10]=min(13, dp[4]+1) = 2 (取 dp[4] 的 4,和当前的硬币 6)
  • dp[8]=min(13, dp[2]+1)= 2 (取 dp[2] 的2,和当前的硬币 6)

class Solution {

    /*
     * 完全背包,每种硬币可以选择多个
     */
    public int coinChange(int[] coins, int amount) {
        // 金额
        int dp[] = new int[amount + 1];
        // 总金额i==conins[j],即刚好选择一个coins[j]的情况,此时希望dp[i-coins[j]]=0
        dp[0] = 0;
        for (int i=1;i<=amount;i++) {
            // 因为是求最小值,初始令它等于不可能的最大值(有1的零钱最大也是amount个,所以amount+1是不可能的值)
             dp[i] = amount + 1;
            // 硬币选择
            for (int j=0;j<coins.length;j++) {
                // 防止 i-coins[j] 小于0越界。
                // 注意不能把这个条件放到 for 循环里 j<coins.length && coins[j]<=i
                // 因为虽然不满足 coins[j]<=i,外层循环还是要继续,放在for里,循环就暂停了
                if (coins[j]<=i) {
                    // 不选/选 coins[j]
                    dp[i] = Math.min(dp[i], dp[i-coins[j]]+1);
                }
            }
        }
        // 最后还是初始时的不可能的值,说明没有解法,返回-1
        return dp[amount]>amount? -1:dp[amount];
    }

    /*
     * 如果是01背包,每种硬币只能选择一个的话
     */
    public int coinChange2(int[] coins, int amount) {
        // 金额
        int dp[] = new int[amount + 1];
        // 总金额i==conins[j],即刚好选择一个coins[j]的情况,此时希望dp[i-coins[j]]=0
        dp[0] = 0;
        // 总金额从大到小,这样不依赖小的了,即每种只选择一个
        // 但是小的要总有一个不可能的数字,这样 min 比小的时候,小的才不会算进去
        // 由于金额是内层循环,所以不能在循环里顺便初始化,把初始化放在双层循环外
        for (int i=1;i<=amount;i++) {
            dp[i] = amount+1;
        }
        // 0-1 背包(每种硬币只能选1个),要先遍历可选硬币,再遍历总金额
        for (int j=0;j<coins.length;j++) {
            // 总金额从大到小,这样不依赖小的了,即每种只选择一个
            // 但是小的要总有一个不可能的数字,这样 min 比小的时候,小的才不会算进去
            for (int i=amount;i>=1;i--) {
                if (coins[j]<=i) {
                    // 不选/选 coins[j]
                    dp[i] = Math.min(dp[i], dp[i-coins[j]]+1);
                }
            }
        }
        // 最后还是初始时的不可能的值,说明没有解法,返回-1
        return dp[amount]>amount? -1:dp[amount];
    }
}

 

139. 单词拆分

可以由动态规划,也可以由 dfs 。由此题思考动态规划和 dfs 的联系

dfs 的暴力枚举(可重复) 和 枚举所有组合(组合即不可重复01背包,dfs 时加入 startIndex)

为什么要加 !dp[i] ? 且只要一次为真就可以退出内层循环 ?

比如 Leetcode [leet, code] i=4 时的内层循环:

词为"leet" 时 s.substring(4-wordLen, 末尾)=“leet”   dp[4]=dp[0]&&("leet".equals("leet"))=true 

词为"code" 时 s.substring(4-wordLen, 末尾)=“code” dp[4]=dp[0]&&("code".equals("leet"))=false

本来为真的又给弄成 false 了 ,但只要一次为真就可以了,所以后面的循环没必要。而且也不能有人把 true 又给改成 false,所以判断 if(!dp[i]) 就不改了

class Solution {

    private boolean dfsRes = false;

    Solution() {
        dfsRes= false;
    }

    public boolean wordBreak(String s, List<String> wordDict) {
        // dpComplete(s, wordDict)
        // dp01(s, wordDict)
        // dfs(s, wordDict, "");
        dfs2(s, wordDict, "", 0);
        return dfsRes;
    }

    // 完全背包,可重复选取
    public boolean dpComplete(String s, List<String> wordDict) {
        boolean dp[] = new boolean[s.length()+1];
        // 比如 Leetcode ["leet", "code"] i=4时,词为"leet" 时 dp[4]=dp[0]&&("leet".equals("leet"))
        // 所以 dp[0] 要为 true
        dp[0] = true;
        for (int i=1;i<=s.length();i++) {
            //初始化默认就是false了。dp[i] = false;
            for (int j=0;j<wordDict.size();j++) {
                String word = wordDict.get(j);
                int wordLen = word.length();
                /* 为什么要加 !dp[i] ?
                 * 比如 Leetcode [leet, code] i=4时,词为"leet" 时 dp[4]=dp[0]&&("leet".equals("leet"))=true
                 * 只要一次为真就可以退出循环
                 * 如果下次循环 i=4时,词为"code" 时 dp[4]=dp[0]&&("code".equals("leet"))=false
                 * 本来为真的又给弄成 false 了
                */
                if (!dp[i] && wordLen<=i) {
                    dp[i] = dp[i-wordLen] && (word.equals(s.substring(i-wordLen, i)));
                    if (dp[i]) {
                        break;
                    }
                }
            }
        }
        return  dp[s.length()];
    }

    // 01背包,不可重复选取
    public boolean dp01(String s, List<String> wordDict) {
        boolean dp[] = new boolean[s.length()+1];
        dp[0] = true;
        // 外层是可选择项
        for (int j=0;j<wordDict.size();j++) {
            // 里层是背包容量倒序
            for (int i=s.length();i>=0;i--) {
                String word = wordDict.get(j);
                int wordLen = word.length();
                if (!dp[i] && wordLen<=i) {
                    dp[i] = dp[i-wordLen] && (word.equals(s.substring(i-wordLen, i)));
                    if (dp[i]) {
                        break;
                    }
                }
            }
        }
        return  dp[s.length()];
    }

    // 可重复选取。递归解法。会超时。
    public void dfs(String s, List<String> wordDict, String curWord) {
        // 超出 s 长度,直接返回
        if (curWord.length()>s.length()) {
            return;
        }
        if (curWord.equals(s)) {
            dfsRes = true;
            return;
        }
        // 可重复选取,就要递归所有的可能性
        // 不可重复选取是属于组合, 要有 startIndex,然后 i=startIndex 下一层 i+1
        for (int i=0;i<wordDict.size();i++) {
            String curs = curWord + wordDict.get(i);
            if (s.startsWith(curs)) {
                // curWord 是 String, 无状态变量,所以不需要恢复现场
                // 因为可以重复选取,所以下一次的 startIndex 是 i 而不是 i+1
                dfs(s, wordDict, curs);
            }
        }
    }

    // 不可重复选取。递归解法
    public void dfs2(String s, List<String> wordDict, String curWord, int startIndex) {
        // 超出 s 长度,直接返回
        if (curWord.length()>s.length()) {
            return;
        }
        if (curWord.equals(s)) {
            dfsRes = true;
            return;
        }
        // 可重复选取,就要递归所有的可能性
        // 不可重复选取是属于组合, 要有 startIndex,然后 i=startIndex 下一层 i+1
        for (int i=startIndex;i<wordDict.size();i++) {
            String curs = curWord + wordDict.get(i);
            if (s.startsWith(curs)) {
                // curWord 是 String, 无状态变量,所以不需要恢复现场
                dfs2(s, wordDict, curs, i+1);
            }
        }
    }
}

 

 

300. 最长递增子序列

dp[i] 表示 nums[0:i] 的最长递增子序列

枚举所有 <i 的 j,即在 nums[i] 之前的所有递增子序列

如果 nums[i]>nums[j],dp[j] 是 [0:j] 的最长,又加上 nums[i] 这一个比 nums[j] 大的

dp[i] = Math.max(dp[i], dp[j] + 1);
class Solution {
    public int lengthOfLIS(int[] nums) {
        int res=1;
        // dp[i] nums[0:i] 的最长递增子序列
        int[] dp = new int[nums.length];
        for (int i=0;i<dp.length;i++) {
            // 最短默认1
            dp[i] = 1;
            for (int j=0;j<i;j++) {
                if (nums[i]>nums[j]) {
                    // dp[j] 是 [0:j] 的最长,又加上nums[i]这一个比nums[j]大的
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

 

 

152. 乘积最大子数组

一开始想的是 dp[i] 表示以 nums[i] 结尾的子数组的最大乘积和

所以 if(nums[i]*dp[i-1]>nums[i]) 就把 nums[i] 加到前面那个子数组里, dp[i]=nums[i]*dp[i-1]。否则的话重新从 nums[i] 开始,即 dp[i]=nums[i]

如果全是正数,没问题。可是 [-2,3,-4] 这个用例不通过,输出3,而正确答案是 12

dp[0]=-2 ;因为 dp[0]*-3<-3 所以 dp[1]=3;dp[2]=3

解决:

如果遇到负数,一乘就会颠倒,乘以最大的会变成最小的,乘以最小的会变成最大的

所以需要两个 dp 数组,一个存以 nums[i] 结尾的子数组的最大乘积和,一个存最小乘积和

这样,遇到负数最大乘积要从最小乘积和 dp 数组里取前一个来乘,最小乘积反而要从最大乘积和 dp 数组里取前一个来乘

以及和重新开始,即 nums[i] 进行比较

dpMax[i] = Math.max(dpMin[i-1]*nums[i], nums[i]);
dpMin[i] = Math.min(dpMax[i-1]*nums[i], nums[i]);
 
class Solution {

    /*
    * [-2,3,-4] 这个用例不通过,输出3,而正确答案是 12
    * 不具有最优子结构
    */
    public int maxProduct2(int[] nums) {
        int maxMulti = nums[0];
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        // dp[i] 以 Nums[i] 结尾的最大子数组
        for (int i=1;i<nums.length;i++) {
            int addi = nums[i]*dp[i-1]; 
            // 把 nums[i] 加到前面
            if (addi > nums[i]) {
                dp[i] = addi;
            }
            // 不把nums[i]加到前面。而是从nums[i]这里重新开始
            else {
                dp[i] = nums[i];
            }
            maxMulti = Math.max(maxMulti, dp[i]);
        }
        return maxMulti;
    }

    public int maxProduct(int[] nums) {
        int[] dpMax = new int[nums.length];
        int[] dpMin = new int[nums.length];
        int maxResult = nums[0];
        dpMax[0]=nums[0];
        dpMin[0]=nums[0];
        for (int i=1;i<nums.length;i++) {
            // 小于0了,一乘就会颠倒,乘以最大的会变成最小的,乘以最小的会变成最大的
            if (nums[i]<0) {
                dpMax[i] = Math.max(dpMin[i-1]*nums[i], nums[i]);
                dpMin[i] = Math.min(dpMax[i-1]*nums[i], nums[i]);
            }
            else {
                dpMax[i] = Math.max(dpMax[i-1]*nums[i], nums[i]);
                dpMin[i] = Math.min(dpMin[i-1]*nums[i], nums[i]);
            }
            maxResult = Math.max(maxResult, dpMax[i]);
        }
        return maxResult;
    }

}

 

62. 不同路径

class Solution {
    public int uniquePaths(int m, int n) {
        // dp[i][j] 表示到 [i][j] 格有多少种不同粒径
        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];

    }

    /* 只能向左和向右走,而且循环的时候也控制了小于边界。所以这个是否在格子里的判断是没有必要的
    private boolean isInGrid(int m, int n, int i, int j) {
        if (i>=0 && i<m && j>=0 && j<n) {
            return true;
        }
        return false;
    }
    */
}

 

 

64. 最小路径和

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int dp[][] = new int[m][n];
        dp[0][0] = grid[0][0];
        // 因为只能向下向右走,所以第一列只能顺直着向下走。路径和就是列的累加
        for (int i=1;i<m;i++) {
            dp[i][0] = dp[i-1][0] + grid[i][0];
        }
        // 因为只能向下向右走,所以第一行只能顺直着向右走。路径和就是行的累加
        for (int i=1;i<n;i++) {
            dp[0][i] += dp[0][i-1] + grid[0][i];
        }
        for (int i=1;i<m;i++) {
            for (int j=1;j<n;j++) {
                // 比较出从 左边向右走,和从上边走下来 二者中更小的那个
                // dp[i][j] = 从上边和从左边中较小的那个+当前格子的值
                dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
            }
        }
        return dp[m-1][n-1];
    }

/*  因为只能向下走,和向右走,所以不会越界,没有必要判断
    private boolean isInGrid(int m,int n,int i,int j) {
        if (i>=0 && i<m && j>=0 && j<n) {
            return true;
        }
        return false;
    }
    */


}

 

 5. 最长回文子串

class Solution {
    public String longestPalindrome(String s) {
        int maxLen = 1;
        int maxLeni = 0;
        int maxLenj = 0;
        int N = s.length();
        boolean dp[][] = new boolean[N][N];
        // dp[i][j] 表示 s[i:j] 是否是最长回文字符串
        // dp[i][j] = dp[i+1][j-1],所以要保证先得出 dp[i+1][j-1]
        // 即 i 递减(i+1到i),j 递增(j-1到j)
        for (int i=N-1;i>=0;i--) {
            for (int j=i;j<N;j++) {
                if (i==j) {
                    // 只有一个字符一定为真
                    dp[i][j] = true;
                }
                else if (j-i==1) {
                    // aa 只有两个字符,这两个字符要完全相等才为真
                    dp[i][j] = s.charAt(i)==s.charAt(j);
                }
                else if (s.charAt(i) == s.charAt(j)) {
                    // aba abba abcba
                    // 依赖 dp[i+1][j-1],所以要保证先得出 dp[i+1][j-1]
                    dp[i][j] = dp[i+1][j-1];
                }
                else {
                    dp[i][j] = false;
                }
                if (dp[i][j]) {
                    int thisLen = j-i+1;
                    if (thisLen>maxLen) {
                        maxLen = thisLen;
                        maxLeni = i;
                        maxLenj = j;
                    }
                }
            }
        }
        return s.substring(maxLeni, maxLenj+1);
    }
}