Skip to content

Latest commit

 

History

History
4491 lines (3207 loc) · 111 KB

算法笔记.md

File metadata and controls

4491 lines (3207 loc) · 111 KB

算法笔记

7 二叉树

8 回溯

backtrack

8.1 组合算法

给定两个整数 nk,返回 1 ... n 中所有可能的 k 个数的组合。

示例 1:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入: n = 1, k = 1
输出: [[1]]

组合I回溯树

class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        res = new LinkedList<>();

        backtrack(n, k, 1, new LinkedList<>());

        return res;
    }

    private void backtrack(int n, int k, int start, LinkedList<Integer> path){
        // 结束条件
        if (path.size() == k){
            res.add(new LinkedList<>(path));
            return;
        }

        // 剪枝操作
        // k - path.size(): 剩下还需k - path.size()个元素
        for (int i = start; i <= n - (k - path.size()) + 1; i++){
            path.add(i);
            backtrack(n, k, i + 1, path);
            path.removeLast();
        }
    }
}

给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。

candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的唯一组合数少于 150 个。

示例 1:

输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

示例 4:

输入: candidates = [1], target = 1
输出: [[1]]

示例 5:

输入: candidates = [1], target = 2
输出: [[1,1]]
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new LinkedList<>();

        backtrack(candidates, target, 0, 0, new LinkedList<>());

        return res;
    }

    private void backtrack(int[] candidates, int target, int sum, int start, LinkedList<Integer> path){
        // 结束条件        
        if (target == sum){
            res.add(new LinkedList<>(path));
            return;
        }
        // sum + candidates[i] <= target 用于剪枝
        for (int i = start; i < candidates.length && sum + candidates[i] <= target; i++){
            sum += candidates[i];
            path.add(candidates[i]);
            // 可复选,所以start = i而不是i+1
            // 同时剪枝,前面用过的元素不再使用
            backtrack(candidates, target, sum, i, path); 
            path.removeLast();
            sum -= candidates[i];
        }
    }
}

给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次,解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new LinkedList<>();
        Arrays.sort(candidates);

        backtrack(candidates, target, 0, 0, new LinkedList<>());

        return res;
    }

    private void backtrack(int[] candidates, int target, int sum, int start, LinkedList<Integer> path){
        if (target == sum){
            res.add(new LinkedList<>(path));
            return;
        }

        // sum + candidates[i] <= target -> 剪枝
        for (int i = start; i < candidates.length && sum + candidates[i] <= target; i++){
            // 去重
            // 去除的是回溯树中同一层的相同元素,而不是同一条路径上的
            if (i > start && candidates[i] == candidates[i - 1]) continue;

            sum += candidates[i];
            path.add(candidates[i]);
            // 每个元素只能使用一次,start = i + 1
            backtrack(candidates, target, sum, i + 1, path);
            path.removeLast();
            sum -= candidates[i];
        }
    }
}

8.2 分割问题

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]
class Solution {
    List<List<String>> res;
    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        backtrack(s, 0, new LinkedList<>());

        return res;
    }

    private void backtrack(String s, int start, LinkedList<String> path){
        // ending
        if (start >= s.length()){
            res.add(new LinkedList<>(path));
            return;
        }

        for (int i = start; i < s.length(); i++){
            String sub = s.substring(start, i + 1);
            // check()用来检查输入是否是回文串
            if (check(sub)){
                path.add(sub);
                backtrack(s, i + 1, path);
                path.removeLast();
            }
        }
    }

    private boolean check(String s){
        int l = 0, r = s.length() - 1;
        while (l <= r){
            if (s.charAt(l) != s.charAt(r)){
                return false;
            }
            l++;
            r--;
        }
        return true;
    }
}

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""[email protected]"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
class Solution {
    List<String> res;
    public List<String> restoreIpAddresses(String s) {
        res = new LinkedList<>();
        backtrack(s, 0, new LinkedList());

        return res;
    }

    private void backtrack(String s, int start, LinkedList<String> path){
        if (path.size() > 4 || start > s.length()){
            return;
        }

        // ending
        if (start == s.length() && path.size() == 4){
            // 用时2ms,内存消耗41MB --> 用StringBuilder比内置函数快!!
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < path.size(); i++){
                sb.append(path.get(i));
                if (i != path.size() - 1){
                    sb.append(".");
                }
            }
            res.add(sb.toString());
            
            // res.add(String.join(".", path)); // 用时3ms,内存消耗41.1MB
        }

        for (int i = start; i < s.length(); i++){
            String sub = s.substring(start, i + 1);
            if (check(sub)){
                path.add(sub);
                backtrack(s, i + 1, path);
                path.removeLast();
            }
        }

    }

    private boolean check(String s){
        // 最多三位数
        if (s.length() > 3){
            return false;
        }
        // 不能以0开头
        if (s.length() > 1 && s.charAt(0) == '0'){
            return false;
        }
        // 只能取0~255
        if (Integer.parseInt(s) > 255){
            return false;
        }

        return true;
    }
}

8.3 子集问题

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        res = new LinkedList<>();
        backtrack(nums, 0, new LinkedList<>());

        return res;
    }

    // 结束条件跟组合有差别,而且不能剪枝,别的没有
    private void backtrack(int[] nums, int start, LinkedList<Integer> path){
        // 每一个结点都要
        res.add(new LinkedList<>(path));
        // ending
        // for 循环结束之后就会自动返回 

        for (int i = start; i < nums.length; i++){
            path.add(nums[i]);
            backtrack(nums, i + 1, path);
            path.removeLast();
        }
    }
}

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        res = new LinkedList<>();
        Arrays.sort(nums);
        backtrack(nums, 0, new LinkedList<>());

        return res;
    }

    // 对比子集I多了重复元素,需要对重复元素进行去重
    // 去重方法跟组合III相同
    private void backtrack(int[] nums, int start, LinkedList<Integer> path){
        // 每一个结点都要
        res.add(new LinkedList<>(path));

        for (int i = start; i < nums.length; i++){
            // 去重
            // 去除的是回溯树中同一层的相同元素,而不是同一条路径上的
            if (i > start && nums[i] == nums[i - 1]){
                continue;
            }
            path.add(nums[i]);
            backtrack(nums, i + 1, path);
            path.removeLast();
        }
    }
}

8.4 排列问题

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]
class Solution {
    List<List<Integer>> res;
    public List<List<Integer>> permute(int[] nums) {
        res = new LinkedList<>();

        backtrack(nums, new LinkedList<>(), new boolean[nums.length]);

        return res;
    }

    void backtrack(int[] nums, LinkedList<Integer> track, boolean[] used){
        // ending
        if (track.size() == nums.length){
            res.add(new LinkedList(track));
            return;
        }

        for (int i = 0; i < nums.length; i++){
            // 去重
            if (used[i]){
                continue;
            }
            track.add(nums[i]);
            used[i] = true;
            backtrack(nums, track, used);
            used[i] = false;
            track.removeLast();
        }
    }
}

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution {
    List<List<Integer>> res;
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        res = new LinkedList<>();
        used = new boolean[nums.length];

        backtrack(nums, new LinkedList<>());

        return res;
    }
	// 用了两个哈希表来去重,一个是数组used(全局),一个是哈希集合use(局部,只对for循环内有效)
	// used用来对同一条路径上的元素去重,use用来对同一层的元素去重
    private void backtrack(int[] nums, LinkedList<Integer> path){
        if (path.size() == nums.length){
            res.add(new LinkedList<>(path));
        }

        HashSet<Integer> use = new HashSet<>();
        for (int i = 0; i < nums.length; i++){
            if (used[i]){
                continue;
            }
            if (use.contains(nums[i])){
                continue;
            }
            
            used[i] = true;
            path.add(nums[i]);
            use.add(nums[i]);
            backtrack(nums, path);
            path.removeLast();
            used[i] = false;
        }
    }
}

9 贪心

贪心算法没有固定的套路,仅能通过模拟来判断能否使用贪心算法,如果不能举出反例,就可以试一试贪心

贪心

9.1 简单题目

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:

输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        // 思路1:优先考虑饼干,小饼干先喂饱小胃口
        Arrays.sort(g);
        Arrays.sort(s);
        int res = 0;
        int i = 0;
        int j = 0;
        while (i < g.length && j < s.length){
            if (s[j] >= g[i]){
                res++;
                i++;
            }
            j++;
        }
        return res;
    }
    
    public int findContentChildren(int[] g, int[] s) {
        // 思路2:优先考虑胃口,先喂饱大胃口
        Arrays.sort(g);
        Arrays.sort(s);
        int res = 0;
        int i = g.length;
        int j = s.length;
        while (i >=0 && j >= 0){
            if (s[j] >= g[i]){
                res++;
                j--;
            }
            i--;
        }
        return res;
    }

}

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]

重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

示例 1:

输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。

示例 2:

输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。

示例 3:

输入:nums = [2,-3,-1,5,-4], k = 2
输出:13
解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        // 每次对最小值取反
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int i = 0; i < nums.length; i++){
            pq.offer(nums[i]);
        }

        for (int i = 0; i < k; i++){
            int min = pq.poll();
            min *= -1;
            pq.offer(min);
        }

        int res = 0;
        while (!pq.isEmpty()){
            res += pq.poll();
        }

        return res;
    }
}

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:

输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。
class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;

        for (int i = 0; i < bills.length; i++){
            int bill = bills[i];
            if (bill == 5){
                // 不需要找零
                five++;
            }else if (bill == 10){
                ten++;
                // 找一张五块
                if (five >= 1){
                    five--;
                }else{
                    return false;
                }
            }else if (bill == 20){
                //找一张十块一张五块 or 三张五块
                if (ten >= 1 && five >= 1){
                    five--;
                    ten--;
                }else if (five >= 3){
                    five -= 3;
                }else{
                    return false;
                }
            }
        }
        return true;
    }
}

9.2 序列问题

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 **摆动序列 。**第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

要考虑有平坡的情况,平坡也有两种情况:

class Solution {
    public int wiggleMaxLength(int[] nums) {
        int n = nums.length;
        if (n <= 1){
            return n;
        }

        int preDiff = 0;
        int curDiff = 0;
        // 默认最右边有一个峰值
        int res = 1;

        for (int i = 1; i < n; i++){
            curDiff = nums[i] - nums[i - 1];
            if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)){
                res++;
                preDiff = curDiff;
            }
        }
        return res;
    }
}

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

示例 1:

输入: n = 10
输出: 9

示例 2:

输入: n = 1234
输出: 1234

示例 3:

输入: n = 332
输出: 299
class Solution {
    // 我写的
    // 位于后面的数字发生变化之后,前面的数字也需要重新判断
    public int monotoneIncreasingDigits(int n) {
        char[] s = String.valueOf(n).toCharArray();
        for (int i = 0; i < s.length - 1; i++){
            if (s[i] > s[i + 1]){
                s[i] -= 1;

                for (int j = i+1; j < s.length; j++){
                    s[j] = '9';
                }
                i = -1;
            }
        }

        return Integer.parseInt(new String(s));
    }
    
    // 题解
    // 从后往前遍历,用start记录需要发生变化的index
    // 在遍历完n之后再修改start以及之后的值
    public int monotoneIncreasingDigits(int n) {
        char[] chars  = String.valueOf(n).toCharArray();
        int start = s.length();
        for (int i = s.length() - 2; i >= 0; i--) {
            if (chars[i] > chars[i + 1]) {
                chars[i]--;
                start = i+1;
            }
        }
        for (int i = start; i < s.length(); i++) {
            chars[i] = '9';
        }
        return Integer.parseInt(String.valueOf(chars));
    }
}

9.3 股票问题

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
    public int maxProfit(int[] prices) {
        int low = Integer.MAX_VALUE;
        int res = 0;

        for (int i = 0; i < prices.length; i++){
            low = Math.min(low, prices[i]);
            res = Math.max(res, prices[i] - low);
        }
        return res;
    }
}

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

可以用贪心,但是dp简单

class Solution {
    // 贪心
    public int maxProfit(int[] prices) {
        int res = 0;
        for (int i = 1; i < prices.length; i++){
            res += Math.max(prices[i] - prices[i - 1], 0);
        }
        return res;
    }
}

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

**注意:**这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:

输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
class Solution {
    // dp
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n][2];

        for (int i = 0; i < n; i++){
            if (i == 0){
                dp[i][0] = 0;
                // 买入时付手续费
                dp[i][1] = -prices[i]-fee;
                continue;
            }
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i] - fee);
        }

        return dp[n-1][0];
    }
    
    // 贪心
    public int maxProfit(int[] prices, int fee) {
        int res = 0;
        int in_hand = prices[0];
        for (int i = 1; i < prices.length; i++){
            if (prices[i] < in_hand){
                in_hand = prices[i];
            }else if (prices[i] - fee > in_hand){
                res += prices[i] - in_hand - fee;
                in_hand = prices[i] - fee;
            }
        }
        return res;
    }
}

9.4 维度权衡问题

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例 2:

输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
     第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。
// 分开处理孩子左边和右边的情况
class Solution {
    public int candy(int[] ratings) {
        int n = ratings.length;
        int[] res = new int[n];
        res[0] = 1;

        // 从左到右
        for (int i = 1; i < n; i++){
            // 跟左边的孩子进行比较
            // 比左边的孩子评分高->糖果数比左边的孩子多一个
            // 比左边的孩子评分低->只获得一个糖果
            res[i] = ratings[i] > ratings[i - 1] ? res[i] = res[i - 1] + 1 : 1;
        }

        // 从右往左
        for (int i = n - 2; i >= 0; i--){
            // 和右边的孩子比较
            // 比右边的孩子评分高->比右边的孩子多一个糖果 or 原来的糖果数,看哪个大
            // 比右边的孩子评分低->糖果数不变
            if (ratings[i] > ratings[i + 1]){
                res[i] = Math.max(res[i], res[i + 1] + 1);
            }
        }

        int sum = 0;
        for (int i: res){
            sum += i;
        }
        return sum;
    }
}

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        Arrays.sort(people, (a, b)->{
            return a[0] == b[0] ? a[1] - b[1] : b[0] - a[0];
        });

        List<int[]> res = new ArrayList<>();

        for (int[] pple : people){
            // 在index=pple[1]的位置插入pple
            res.add(pple[1], pple);
        }

        return res.toArray(new int[0][0]);
    }
}

9.5 区间问题

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
// 模拟
// 每次计算能到达的最远的地方,farthest <= i -> nums[i] = 0
// 可跳跃的最大长度刚好到i跳不出去了,所以不能到达最后一个下标
class Solution {
    public boolean canJump(int[] nums) {
        int farthest = 0;
        int n = nums.length;
        for (int i = 0; i < n - 1; i++){
            farthest = Math.max(farthest, i + nums[i]);
            if (farthest <= i){
                return false;
            }
        }
        return true;
    }
}

给定一个长度为n0索引数量nums。初始位置为nums[0]

每个元素nums[i]表示从索引i向前跳转的最大长度。换句话说,如果你在nums[i]某处,你可以跳转到任意nums[i + j]处:

  • 0 <= j <= nums[i]
  • i + j < n

返回的 nums[n - 1]最小跳转次数。生成的测试实例可以到达目的地nums[n - 1]

示例1:

输入: nums = [2,3,1,1,4]
输出: 2
解释:跳到最后一个位置的最小跳跃数是2。
     从下标为0跳到下为1的位置,跳 1 步,然后跳 3 步到达集群的最后一个位置。

示例2:

输入: nums = [2,3,0,1,4]
输出: 2
class Solution {
    public int jump(int[] nums) {
        // 每次都跳到可跳跃区域最大的地方
        // 比如[3, 1, 4, 2, ...]
        // 站在0的位置,优先选择index=2,其可跳跃距离为2+4=6,比另外两个(1+1=2和3+2=5)要大
        int farthest = 0;
        int jumps = 0;
        int end = 0;
        int n = nums.length;
        for (int i = 0; i < n - 1; i++){
            farthest = Math.max(nums[i] + i, farthest);
            if (i == end){
                jumps++;
                end = farthest;
            }
        }
        return jumps;
    }
}

有一些气球贴在一个插头上,用XY平面 表示 points 的 阻碍 。points[i] = [xstart, xend]``xstart``xend

尖端弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出尖端箭,若有一个气球的直径的开始和结束坐标为x``start,,x``end且满足 ,则该气球会被引爆。可以射出弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。xstart ≤ x ≤ x``end

给你一个储备points返回引爆所有气球所必须射出的最小弓箭数

示例1:

输入: points = [[10,16],[2,8],[1,6],[7,12]]
输出: 2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例2:

输入: points = [[1,2],[3,4],[5,6],[7,8]]
输出: 4
解释:每个气球需要射出科研箭,总共需要4支箭。

示例3:

输入: points = [[1,2],[2,3],[3,4],[4,5]]
输出: 2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。
class Solution {
    public int findMinArrowShots(int[][] points) {
        // 按x_start从小到大排列,如果x_start相同,x_end从大到小排列
        Arrays.sort(points, (a, b)-> a[0] == b[0] ? b[1] - a[1] : a[0] - b[0]);
        // 求重叠区间的个数
        int res = points.length;
        int[] x = points[0];

        for (int i = 1; i < points.length; i++){
            int[] p = points[i];
            if (x[1] >= p[0] && x[1] < p[1]){ // x和p重叠了
                res--;
                x[0] = p[0];
            }else if (x[0] <= p[0] && x[1] >= p[1]){ // x包含p
                res--;
                x = p;
            }else if (x[1] < p[0]){
                x = p;
            }
        }
        return res;
    }
}

给定一个区间的集合 intervals ,其中 。返回移除需要区间的最小数量,使剩余区间互不重叠intervals[i] = [starti, endi]

示例1:

输入: Intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释:删除 [1,3] 后,相邻的区间没有重叠。

示例2:

输入: Intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释:你需要删除两个 [1,2] 来使相邻的区间不重叠。

示例 3:

输入: Intervals = [ [1,2], [2,3] ]
输出: 0
解释:你不需要删除任何区间,因为它们已经是无重叠的了。

不同的排序 ----> 不同的解法

class Solution {
    // 我写的
    public int eraseOverlapIntervals(int[][] intervals) {
        //求n-非重叠区间的个数
        Arrays.sort(intervals, (a, b)->a[0]==b[0]?b[1]-a[1]:a[0]-b[0]);

        // 至少有一个非重叠区间
        int res = intervals.length - 1;
        int[] x = intervals[0];

        for (int i = 1; i < intervals.length; i++){
            int[] p = intervals[i];
            if (x[1] > p[0] && x[1] < p[1]){
                x[0] = p[0];
            }else if (p[1] <= x[1] && p[0] >= x[0]){
                x = p;
            }else if (x[1] <= p[0]){
                x = p;
                res--;
            }
        }
        return res;
    }
    
    // 题解
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals, (a,b)->(a[1]-b[1]));
        int res = 0;
        int[] x = intervals[0];
        for (int i = 1; i < intervals.length; i++){
            if (x[1] > intervals[i][0]){
                res++;
            }else{
                x = intervals[i];
            }
        }
        return res;
    }
}

以队列数intervals表示个区间的集合,其中单个区间为。请您合并所有重叠的区间,并返回 一个不重叠的区间队列,该队列需恰好覆盖输入中的所有区间intervals[i] = [starti, endi]

示例1:

输入:间隔 = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释:区间[1,3]和[2,6]重叠,将它们合并为[1,6]。

示例2:

输入: Intervals = [[1,4],[4,5]]
输出: [[1,5]]
解释:区间 [1,4] 和 [4,5] 可视为重叠区间。
class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (a, b)->a[0]==b[0]?b[1]-a[1]:a[0]-b[0]);

        List<int[]> res = new ArrayList<>();
        int[] x = intervals[0];

        for (int i = 1; i < intervals.length; i++){
            int[] p = intervals[i];
            if (x[1] < p[0]){
                res.add(x);
                x = p;
            } else if (x[1] >= p[0] && x[1] < p[1]){
                x[1] = p[1];
            }
        }
        res.add(x);
        return res.toArray(new int[0][0]);
    }
}

给你一个字符串s。我们假设这个字符串划分为多个的片段,相同的字母在一个片段中最多出现。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是s

返回一个表示每个字符串的长度的列表。

示例1:

输入: s = "ababcbacadefegdehijhklij"
输出: [9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像“ababcbacadefegde”、“hijhklij”这样的划分是错误的,因为划分的碎片数很少。

示例2:

输入: s = "eccbbbbdec"
输出: [10]

class Solution {
    public List<Integer> partitionLabels(String s) {
        int[] hash = new int[26];
        // 记录每一个字符最后出现的位置
        for (int i = 0; i < s.length(); i++){
            char c = s.charAt(i);
            hash[c-'a'] = i;
        }


        int left = 0;
        int right = 0;
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < s.length(); i++){
            // 确定最右边边界
            right = Math.max(right, hash[s.charAt(i) - 'a']);
            if (i == right){
                res.add(right - left + 1);
                left = i + 1;
            }
        }
        return res;
    }
}

9.6 Hard

在一条环路上有n 一个加油站,其中第i 一个加油站有汽油 gas[i] 升。

你有一个油箱容量无限的汽车,从第一个加油站开往第一个加油站需要消耗汽油 升压。你从其中的一个加油站出发,开始时油箱为空。 i i+1 cost[i]

给定多个整数队列gascost,如果可以按顺序绕路运行一周,则返回出发时加油站的数量,否则返回。-1如果存在解,则保证它是唯一的。

示例1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可以 4升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3替代初始索引。

示例2:

输入: gas=[2,3,4],cost=[3,4,3]
输出: -1
解释:
你不能从0号或1号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回2号加油站,因为返程需要消耗4升汽油,但是你的油箱只有3升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

class Solution {
    // 暴力解 超时
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;

        for (int i = 0; i < n; i++){
            // 以i为起点跑一圈
            int idx = (i + 1) % n;
            int sum = gas[i] - cost[i];
            while (sum > 0 && idx != i){
                sum += gas[idx] - cost[idx];
                idx = (idx + 1) % n;
            }
            if (sum >= 0 && idx == i){
                return i;
            }
        }
        return -1;
    }
    
    // 题解
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int totalSum = 0;
        int curSum = 0;
        int start = 0;
        for (int i = 0; i < n; i++){
            curSum += gas[i] - cost[i];
            totalSum += gas[i] - cost[i];
            if (curSum < 0){
                curSum = 0;
                start = i + 1;
            }
        }
        if (totalSum < 0) return -1;
        return start == n ? 0 : start;
    }
}

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例1:

img

输入: [0,0,null,0,0]
输出: 1
解释:如图所示,一个摄像头足以监控所有节点。

示例2:

img

输入: [0,0,null,0,null,0,null,null,0]
输出: 2
解释:至少需要两个摄像头来监视树的所有节点。上图显示了摄像头放置的有效位置之一。
class Solution {
    int res = 0;
    public int minCameraCover(TreeNode root) {
        if (traverse(root) == 0){
            res++;
        }
        return res;
    }

    // 分为3种情况
    // 0: 该节点无覆盖
    // 1: 该节点有摄像头
    // 2: 该节点有覆盖
    public int traverse(TreeNode root){
        // 空节点默认有覆盖
        if (root == null) return 2;

        int left = traverse(root.left);
        int right = traverse(root.right);

        // 左右子节点其中一个无覆盖
        if (left == 0 || right == 0) {
            // 需要在该节点放一个摄像头
            res++;
            return 1;
        }

        // 左右节点其中一个有摄像头
        if (left == 1 || right == 1){
            return 2;
        }

        if (left == 2 && right == 2){
            return 0;
        }

        return -1;
    }
}

10 动态规划

10.1 基础题

斐波那契数 (通常用 F(n)表示)组成的序列称为斐波那契数列。该数列由 01开始,后面的每一个数字都是前面三个数字的和。

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中n > 1

给定 n,请计算F(n)

示例1:

输入: n = 2
输出: 1
解释: F(2) = F(1) + F(0) = 1 + 0 = 1

示例2:

输入: n = 3
输出: 2
解释: F(3) = F(2) + F(1) = 1 + 1 = 2

示例3:

输入: n = 4
输出: 3
解释: F(4) = F(3) + F(2) = 2 + 1 = 3
class Solution {
    // dp数组
    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+1; i++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
    
    // 空间复杂度优化
    public int fib(int n) {
        if (n < 2) return n;
        int dp_0 = 0;
        int dp_1 = 1;
        int dp = 0;
        for (int i = 2; i < n + 1; i++){
            dp = dp_0 + dp_1;
            dp_0 = dp_1;
            dp_1 = dp;
        }
        return dp;
    }
}

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

你可以爬12个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例1:

输入: n = 2
输出: 2
解释:有两种方法可以爬到楼顶。
1. 1阶 + 1阶
2. 2阶

示例2:

输入: n = 3
输出: 3
解释:有清晰的方法可以爬到楼顶。
1. 1阶 + 1阶 + 1阶
2. 1阶+2阶
3. 2阶+1阶
class Solution {
    // dp数组
    public int climbStairs(int n) {
        if (n < 3) return n;
        int dp[] = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++){
            // 1. 从i - 1阶爬1阶
            // 2. 从i - 2阶爬2阶
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
    
    // 空间复杂度优化
    public int climbStairs(int n) {
        if (n < 3) return n;
        int dp_1 = 1;
        int dp_2 = 2;
        int dp_n = 0;
        for (int i = 3; i <= n; i++){
            // 1. 从i - 1阶爬1阶
            // 2. 从i - 2阶爬2阶
            dp_n = dp_1 + dp_2;
            dp_1 = dp_2;
            dp_2 = dp_n;
        }
        return dp_n;
    }
}

给你一个整数托盘cost,其中cost[i]是从楼梯第i一个台阶向上爬需要支付的费用。一旦你支付了这个费用,就可以选择向上爬一个或两个台阶。

您可以选择从下标为0或下标为1的台阶开始爬楼梯。

请您计算并返回到达楼梯顶部的最低花费。

示例1:

输入: cost = [10, 15 ,20]
输出: 15
解释:你的分数下标为 1 的台阶开始。
- 支付15,向上爬两个楼梯,到达楼梯顶部。
总花费为15。

示例2:

输入: cost = [ 1 ,100, 1 ,1, 1 ,100, 1 , 1 ,100, 1 ]
输出: 6
解释:你的问卷下标为 0 的台阶开始。
- 支付1,向上爬2个台阶,到达下标为2个台阶。
- 支付1,向上爬2个台阶,到达下标为4个台阶。
- 支付1,向上爬2个台阶,到达下标为6个台阶。
- 支付1,向上爬一个台阶,到达下标为7个台阶。
- 支付1,向上爬2个台阶,到达下标为9个台阶。
- 支付 1,爬上一个台阶,到达楼梯顶部。
总花费为6。
class Solution {
    // dp数组
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        if (n == 1) return cost[0];
        if (n == 2) return Math.min(cost[0], cost[1]);

        int[] dp = new int[n+1];
        
        for (int i = 2; i <= n; i++){
            dp[i] = Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
    
    // 空间复杂度优化
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        if (n == 1) return cost[0];
        if (n == 2) return Math.min(cost[0], cost[1]);

        int dp_1 = 0;
        int dp_2 = 0;
        int dp_n = 0;
        
        for (int i = 2; i <= n; i++){
            dp_n = Math.min(dp_1+cost[i-1], dp_2+cost[i-2]);
            dp_2 = dp_1;
            dp_1 = dp_n;
        }
        return dp_n;
    }
}

一个机器人位于m x n 网格的左上角(起始点在下面标记为“开始”)。

机器人每次只能继续或者向右移动一步。机器人尝试达到网格的右下角(在下图中标记为“完成”)。

问总共有多少条不同的路径?

示例1:

img

输入: m = 3, n = 7
输出: 28

示例2:

输入: m = 3, n = 2
输出: 3
解释:
从开始左上角,共有3条路径可以到右下角。
1. 向右 -> 向下 -> 向下
2. 下一代 -> 下一代 -> 向右
3. 向下 -> 向下 -> 向下

示例3:

输入: m = 7, n = 3
输出: 28

示例4:

输入: m = 3, n = 3
输出: 6
class Solution {
    public int uniquePaths(int m, int n) {
        // dp[i][j]表示坐标i,j到m-1,n-1的路径数
        int[][] dp = new int[m][n];
        Arrays.fill(dp[m-1], 1);
        for (int i = 0; i < m; i++){
            dp[i][n-1] = 1;
        }

        for (int i = m - 2; i >= 0; i--){
            for (int j = n - 2; j >= 0; j--){
                dp[i][j] = dp[i+1][j] + dp[i][j+1];
            }
        }

        return dp[0][0];
    }
    
    // 空间复杂度压缩
    // 从后往前遍历和从前往后是一样的,状态转移函数相反
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        
        for (int i = 0; i < m; i++){
            for (int j = 0; j < n; j++){
                if (i == 0 || j == 0){
                    dp[j] = 1;
                }else{
                    dp[j] = dp[j] + dp[j-1];
                }
            }
        }

        return dp[n-1];
    }
}

一个机器人位于 网格的左上角(起始点在下面标记为“开始”)。 m x n

机器人每次只能继续或者向右移动一步。机器人尝试达到网格的右下角(在下图中标记为“完成”)。

现在网格中有障碍物。那么从左上角到右下角要考虑有多少条不同的路径?

网格中的障碍物和空位置分别用10来表示。

示例1:

img

输入: barrierGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出: 2
解释: 3x3网格的正中间有一个障碍物。
从左上角到右下角共有2条不同的路径:
1. 向右 -> 向右 -> 升级 -> 升级
2. 下一代 -> 下一代 -> 向右 -> 向右

示例2:

img

输入: barrierGrid = [[0,1],[0,0]]
输出: 1
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m+1][n+1];
        // base case
        dp[1][1] = obstacleGrid[0][0] == 1 ? 0 : 1;

        // 必须从1开始,不然数组溢出,所以dp数组也得+1把0空出来
        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (i == 1 && j == 1){
                    continue;
                }
                if (obstacleGrid[i-1][j-1] == 1){
                    // 不显式赋值也默认是0
                    // dp[i][j] = 0;
                    continue;
                }
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        return dp[m][n];
    }
    
    // 空间压缩
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[n+1];
        // base case
        dp[1] = obstacleGrid[0][0] == 1 ? 0 : 1;

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (i == 1 && j == 1){
                    continue;
                }
                if (obstacleGrid[i-1][j-1] == 1){
                    // 必须显式赋值
                    dp[j] = 0;
                    continue;
                }
                dp[j] = dp[j-1] + dp[j];
            }
        }
        return dp[n];
    }
}

给定一个正整数 n ,将其拆分为k正整数的和( k >= 2 ),整理这些整数的乘积最大化。

返回您所能承受的最大乘积

示例1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

$n = 4$,我们可以把 4 拆分成 $1 + 3, 2 + 2$,对应的乘积就是 $1 * 3, 2 * 2$

但是这时候直接比较大小还不够,需要再把3和2拆分成$1 * 2$和$1 * 1$

最后: $$ integerBreak(4) = max(1 * 3, 1 * integerBreak(3), 2 * 2, 2 * integerBreak(2))\ = max( 1 * max(3, integerBreak(3)), 2 * max(2, integerBreak(2)) ) $$ 泛化一下获得状态转移方程为:

int res = Integer.MIN_VALUE;
for (int i = 1; i <= n/2; i++) {
    res = max(res, i * max(integerBreak(n - i), n - i));
}

完整题解:

class Solution {
    int[] memo;
    public int integerBreak(int n) {
        memo = new int[n + 1];
        return dp(n);
    }

    int dp(int n){
        if (n < 2){
            return n;
        }
        if (memo[n] > 0){
            return memo[n];
        }

        int res = Integer.MIN_VALUE;
        for (int i = 1; i <= n; i++){
            res = Math.max(res, i * Math.max(n-i, dp(n-i)));
        }
        memo[n] = res;
        return res;
    }
}

给你一个整数n,求恰由n个节点组成且节点值从1n不同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。

示例1:

img

输入: n = 3
输出: 5

示例2:

输入: n = 1
输出: 1

image-20230630101121996

class Solution {
    int[][] memo;
    public int numTrees(int n) {
        memo = new int[n+1][n+1];
        return count(1, n);
    }

    // 求lo...hi之间有多少种二叉搜索树
    public int count(int lo, int hi){
        if (lo > hi){
            return 1;
        }

        if (memo[lo][hi] != 0){
            return memo[lo][hi];
        }

        int res = 0;
        for (int i = lo; i <= hi; i++){
            int left = count(lo, i-1);
            int right = count(i+1, hi);
            res += left * right;
        }
        memo[lo][hi] = res;
        return res;
    }
}

10.2 背包问题

在有限容量的背包中装价值最多的东西

面试只掌握01背包和完全背包就够了

416.分割等和子集1

01背包 完全背包
遍历顺序 先遍历物品,再遍历背包 求最大最小数:先遍历哪个都行
求组合数(顺序不同视为同一个组合):外层for循环遍历物品,内层for遍历背包。
求排列数(顺序不同视为不同组合):外层for遍历背包,内层for循环遍历物品。
遍历方向 外层for正向遍历,内层for反向遍历 内外层for都是正向遍历
转移函数 求最大最小数:
$dp[i][j]=max(dp[i-1][j-weight[i]]+value[i], dp[i-1][j])$
$dp[j]=max(dp[j], dp[j-weight[i]+value[i]])$
求组合数/排列数(xxx有多少种):
$dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]$
$dp[j] += dp[j-nums[i]]$
同01背包
  • dp数组定义:$dp[i][j]$表示从下标为$[0-i]$的物品里任意取,放进容量为$j$的背包,价值总和最大是多少。

  • 初始化:

    • $dp[i][0]=0$,背包容量为0时,啥都装不了

    • 对于$i=0$,当背包容量$j>=weight[0]$时,$dp[0][j]=value[0]$

  • 遍历顺序:先遍历物品,再遍历背包容量

  • 转移函数:

    • 求最大/最小数

    $$ dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);\ or\ dp[j] = Math.max(dp[j], dp[j-weight[i]]+value[i]) $$ 不放物品$i$:$dp[i][j]=dp[i-1][j]$,背包的容量不会发生变化,最大价值总和也不会发生变化

    放物品$i$:$dp[i][j]=dp[i-1][j-weight[i]]+value[i]$,背包容量变小,最大价值总和变大

    • 求组合/排列数

    $$ dp[i][j] = dp[i-1][j] + dp[i-1][j-weight[i]]; \ or \ dp[j] += dp[j-weight[i]] $$

模板:

public class BagProblem {
    public static void main(String[] args) {
        int[] weight = {1,3,4};
        int[] value = {15,20,30};
        int bagSize = 4;
        testWeightBagProblem(weight,value,bagSize);
    }

    /**
     * 动态规划获得结果
     * @param weight  物品的重量
     * @param value   物品的价值
     * @param bagSize 背包的容量
     */
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){

        // 创建dp数组
        int goods = weight.length;  // 获取物品的数量
        int[][] dp = new int[goods][bagSize + 1];

        // 初始化dp数组
        // 创建数组后,其中默认的值就是0
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }

        // 填充dp数组
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                if (j < weight[i]) {
                    /**
                     * 当前背包的容量都没有当前物品i大的时候,是不放物品i的
                     * 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
                     */
                    dp[i][j] = dp[i-1][j];
                } else {
                    /**
                     * 当前背包的容量可以放下物品i
                     * 那么此时分两种情况:
                     *    1、不放物品i
                     *    2、放物品i
                     * 比较这两种情况下,哪种背包中物品的最大价值最大
                     */
                    dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
                }
            }
        }

        // 打印dp数组
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + "\t");
            }
            System.out.println("\n");
        }
    }
}

给你一个只包含正整数非空数据库 nums。请你判断是否可以将这个数据库分割成两个子集,使得两个子集的要素并可能。

示例1:

输入: nums = [1,5,11,5]
输出: true
解释:吞吐量可以分割成 [1, 5, 5] 和 [11] 。

示例2:

输入: nums = [1,2,3,5]
输出: false
解释:吞吐量不能分割成多个元素且可靠的子集。
class Solution {
    public boolean canPartition(int[] nums) {
        int target = 0;
        for (int num : nums) target += num;
        if (target % 2 != 0) return false;
        // 背包容量为target
        target /= 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 = 0; j <= target; j++){
                if (j < nums[i]){
                    dp[i][j] = dp[i-1][j];
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i]);
                }
            }
        }
        return dp[nums.length-1][target] == target;
    }
    
    // 空间压缩
    public boolean canPartition(int[] nums) {
        int target = 0;
        for (int num : nums) target += num;
        if (target % 2 != 0) return false;
        // 背包容量为target
        target /= 2;

        int[] dp = new int[target+1];

        // 初始化
        for (int j = nums[0]; j <= target; j++){
            dp[j] = nums[0];
        }

        // 遍历
        for (int i = 1; i < nums.length; i++){
            // 背包容量必须反向遍历
            for (int j = target; j >= 0; j--){
                if (j >= nums[i]){
                    dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i]);
                }
            }
        }
        return dp[target] == target;
    }
}

有一堆石头,用整数 stones表示。其中 stones[i]表示第i1块石头的重量。

每一轮,总结出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将彻底粉碎,而重量为 y 石头新的重量 y-x

最后,最多只剩一块石头。返回此石头最大的可能重量。如果没有剩余石头,就返回0

示例1:

输入: stones = [2,7,4,1,8,1]
输出: 1
解释:
组合2和4,得到2,所以吞吐量转化为[2,7,1,8,1],
组合7和8,得到1,所以吞吐量转化为[2,1,1,1],
组合2和1,得到1,所以吞吐量转化为[1,1,1],
组合1和1,得到0,所以吞吐量转化为[1],这就是最优值。

示例2:

输入: stones = [31,26,33,21,40]
输出: 5
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int v: stones) sum += v;

        int n = stones.length;
        int[][] dp = new int[n][sum/2+1];

        for (int j = stones[0]; j <= sum/2; j++){
            dp[0][j] = stones[0];
        }

        for (int i = 1; i < n; i++){
            for (int j = 0; j <= sum/2; j++){
                if (j < stones[i]){
                    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 - 2*dp[n-1][sum/2];
    }
    
    // 空间压缩
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int v: stones) sum += v;

        int n = stones.length;
        int[] dp = new int[sum/2+1];

        for (int j = stones[0]; j <= sum/2; j++){
            dp[j] = stones[0];
        }

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

给你一个非负负载nums和一个整数target

向队列中的每个整数前添加 '+''-',然后串联起所有整数,可以构造一个表达式

  • 例如,nums = [2, 1],可以在2前面添加'+',在1添加'-',然后串联起来得到前面的表达式"+2-1"

返回可以通过上述方法构造的、结果相等的target不同表达式的个数。

示例1:

输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释:共有 5 种方法让目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例2:

输入: nums = [1], target = 1
输出: 1

可以直接用回溯做

将nums分为两堆,一堆全正数,一堆全负数,可以得出: $$ left + right = sum\ left - right = target\ left = \frac{sum + target}{2} $$

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        int n = nums.length;
        for (int i = 0; i < n; i++){
            sum += nums[i];
        }
        if (sum < Math.abs(target) || (target + sum) % 2 == 1) return 0;
        sum = (target + sum) / 2;

        // dp[i][j]为前i个物体恰好重量和为j的组合数
        int[][] dp = new int[n + 1][sum + 1];

        //base case
        dp[0][0] = 1;

        for (int i = 1; i <= n; i++){
            for (int j = 0; j <= sum; j++){
                // 1. 不放物品
                // 2. 放nums[i]
                if (j - nums[i - 1] >= 0)
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
                else{
                    dp[i][j] = dp[i - 1][j];
                } 
            }
        }

        return dp[n][sum];
    }
    
    // 一维dp
    public int findTargetSumWays(int[] nums, int target) {
        int n = nums.length;
        int sum = 0;
        for (int v: nums) sum += v;
        if (sum < Math.abs(target) || (target + sum) % 2 == 1) return 0;

        int bagSize = (sum + target) / 2;
        if (bagSize < 0) bagSize = -bagSize;

        int[] dp = new int[bagSize + 1];
        // 正数这堆一个不放,全是负数
        dp[0] = 1;

        for (int i = 0; i < n; i++){
            // 到nums[i]结束,不然就溢出了
            for (int j = bagSize; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[bagSize];
    }
}

给你一个二进制字符串数组 strs 和两个整数 mn

请你找出并返回 strs 的最大子集的长度,该子集中 最多m0n1

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y子集

示例 1:

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

本题是背包维度为二维的01背包问题

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // 背包的大小有两个维度,所以dp数组至少也是二维数组
        // dp数组定义:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
        int[][] dp = new int[m+1][n+1];
        //dp数组初始化为0

        for (String str: strs){ // 先遍历物品
            int oneNum = 0;
            int zeroNum = 0;
            for (char c: str.toCharArray()){
                if (c == '0') zeroNum++;
                else oneNum++;
            }

            // 遍历背包容量,并且必须是逆序
            // 边界取oneNum和zeroNum避免数组越界
            for (int i = m; i >= zeroNum; i--){
                for (int j = n; j >= oneNum; j--){
                    dp[i][j] = Math.max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}
10.2.2 完全背包问题

01背包和完全背包唯一不同就是体现在遍历顺序上

01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

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

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

// 完全背包的遍历顺序可变,可以先遍历物品,也可以先遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        //求最大/最小数
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        //求组合/排列数
		// dp[j] += dp[j - coins[i]];
    }
}

给你一个整数托盘coins表示不同面额的硬币,再给你一个整数amount表示总金额。

请您计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回0

假设每一枚面额的硬币有无限个。

题目数据保证结果符合32位带符号整数。

示例1:

输入: amount = 5, coin = [1, 2, 5]
输出: 4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例2:

输入: amount = 3, coin = [2]
输出: 0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例3:

输入: amount = 10, coin = [10] 
输出: 1

跟前面的目标和类似

class Solution {
    public int change(int amount, int[] coins) {
        // 背包容量是amount
        // 物品是coins
        int n = coins.length;
        int[][] dp = new int[n+1][amount+1];
        for (int i = 0; i <= n; i++){
            dp[i][0] = 1;
        }

        // 先遍历物品
        for (int i = 1; i <= n; i++){
            for (int j = 1; j <= amount; j++){
                if (j - coins[i-1] >= 0)
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i-1]];
                else
                    dp[i][j] = dp[i - 1][j];
            }
        }

        return dp[n][amount];
    }
    
    // 空间压缩
    public int change(int amount, int[] coins) {
        // 背包容量是amount
        // 物品是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];
    }
}

给你一个由不同整数组成的数组nums,和一个目标整数target。请nums你计算一下并返回总和为target元素组合的个数。

题目数据保证答案符合32位整数范围。

示例1:

输入: nums = [1,2,3], target = 4
输出: 7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1,1,2)
(1,2,1)
(1, 3)
(2,1,1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视为不同的组合。

示例2:

输入: nums = [9], target = 3
输出: 0
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 i = 0; i < nums.length; i++){
                if (j - nums[i] >= 0)
                    dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}

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

你可以爬12个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例1:

输入: n = 2
输出: 2
解释:有两种方法可以爬到楼顶。
1. 1阶 + 1阶
2. 2阶

示例2:

输入: n = 3
输出: 3
解释:有清晰的方法可以爬到楼顶。
1. 1阶 + 1阶 + 1阶
2. 1阶+2阶
3. 2阶+1阶

上一题(组合总数IV)一样

class Solution {
    public int climbStairs(int n) {
        // 背包容量是n
        int[] dp = new int[n + 1];
        // m阶对应m个物品 weight[i] = i

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

        // 求排列数,先遍历背包
        for (int j = 1; j <= n; j++){
            for (int i = 1; i <= 2; i++)
                if (j - i >= 0)
                    dp[j] += dp[j - i];
        }
        return dp[n];
    }
}

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0
class Solution {
    public int coinChange(int[] coins, int amount) {
        // bagSize = amount        
        // weight[i] = coins[i]
        int[] dp = new int[amount + 1];

        // 初始化
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;

        // 求最小数
        for (int i = 0; i < coins.length; 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];
    }
}

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];

        // 初始化
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;

        // 求最小数
        for (int i = 0; i * i <= n; i++){
            for (int j = i*i; j <= n; j++){
                if (dp[j - i*i] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j - i*i] + 1);
                }
            }
        }

        return dp[n];
    }
}

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length()+1];

        // 初始化
        dp[0] = true;
        
        Set<String> wordSet = new HashSet<>(wordDict);

        // 求排列数,先遍历背包
        for (int j = 1; j <= s.length(); j++){
            // 条件!dp[j]用于减少重复运算,dp[j]已经是true的就不用再算了
            for (int i = 0; i < j && !dp[j]; i++){
                if (dp[i] && wordSet.contains(s.substring(i, j))) {
                    dp[j] = true;
                }
            }
        }

        return dp[s.length()];
    }
}

10.3 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        // dp[i] -> nums[i...n-1]能获得的最高金额
        if (n == 0) return 0;
        else if (n == 1) return nums[0];
        else if (n == 2) return Math.max(nums[0],nums[1]);
        
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]);
        }

        return dp[n-1];
    }
    
    // 滚动数组实现空间压缩
    public int rob(int[] nums) {
        int n = nums.length;
        // dp[i] -> nums[i...n-1]能获得的最高金额
        if (n == 0) return 0;
        else if (n == 1) return nums[0];
        else if (n == 2) return Math.max(nums[0],nums[1]);
        
        int dp = 0;
        int dp_2 = nums[0];
        int dp_1 = Math.max(nums[0], nums[1]);

        for (int i = 2; i < n; i++) {
            dp = Math.max(dp_1, nums[i] + dp_2);
            dp_2 = dp_1;
            dp_1 = dp;
        }

        return dp;
    }
}

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;

        if (n == 0) return 0;
        else if (n == 1) return nums[0];
        else if (n == 2) return Math.max(nums[0], nums[1]);

        // 分两种情况,抢第一家or不抢第一家
        int[] dp1 = new int[n];
        int[] dp2 = new int[n];

        // 抢第一家
        dp1[0] = nums[0];
        dp1[1] = nums[0];

        for (int i = 2; i < n-1; i++) {
            dp1[i] = Math.max(dp1[i - 1], dp1[i - 2]+nums[i]);
        }

        // 不抢第一家
        dp2[0] = 0;
        dp2[1] = nums[1];

        for (int i = 2; i < n; i++) {
            dp2[i] = Math.max(dp2[i - 1], dp2[i - 2]+nums[i]);
        }

        return Math.max(dp1[n-2], dp2[n-1]);
    }
    
    // Solution 2:
    // dp递归更简洁
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];
        
        return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1));
    }

    public int rob(int[] nums, int start, int end){
        int dp_i = 0, dp_i_1 = 0, dp_i_2 = 0;
        for (int i = end; i >= start; i--){
            dp_i = Math.max(dp_i_1, nums[i]+dp_i_2);
            dp_i_2 = dp_i_1;
            dp_i_1 = dp_i;
        }
        return dp_i;
    }
}

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

img

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

img

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
class Solution {
    Map<TreeNode, Integer> memo = new HashMap<>();

    public int rob(TreeNode root) {
        if (root == null) return 0;
        if (root.left == null && root.right == null) return root.val;
        if (memo.containsKey(root)) return memo.get(root);

        // 偷父结点
        int val1 = root.val
                +(root.left == null ? 0 : rob(root.left.left) + rob(root.left.right))
                 +(root.right == null ? 0 : rob(root.right.left) + rob(root.right.right));

        // 不偷父节点
        int val2 = rob(root.left) + rob(root.right);

        memo.put(root, Math.max(val1, val2));
        return Math.max(val1, val2);
    }
}

10.4 股票问题

股票系列问题定义:

dp[i][k][0 or 1]
0 <= i <= n - 1, 1 <= k <= K
n 为天数 K 为交易数的上限0  1 代表是否持有股票

股票系列问题通用状态转移方程和base case:

状态转移方程dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

base casedp[0][1][0] = 0
dp[0][1][1] = -prices[0]

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        // dp[i][0]表示第i天未持有股票时,手上的现金
        // dp[i][1]表示第i天持有股票时,手上的现金
        int[][] dp = new int[n][2];

        // base case
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        for (int i = 1; i < n; i++){
            // 第i天持股
            // 只能买卖一次,所以第i天买入就是第一次买入,dp[i][1]=-prices[i]
            dp[i][1] = Math.max(-prices[i], dp[i-1][1]);
            // 第i天未持股
            dp[i][0] = Math.max(dp[i-1][1] + prices[i], dp[i-1][0]);
        }

        return dp[n-1][0];
    }
}

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        // dp[i][0]表示第i天未持有股票时,手上的现金
        // dp[i][1]表示第i天持有股票时,手上的现金
        int[][] dp = new int[n][2];

        // base case
        dp[0][0] = 0;
        dp[0][1] = -prices[0];

        for (int i = 1; i < n; i++){
            // 第i天持股
            dp[i][1] = Math.max(dp[i-1][0] - prices[i], dp[i-1][1]);
            // 第i天未持股
            dp[i][0] = Math.max(dp[i-1][1] + prices[i], dp[i-1][0]);
        }

        return dp[n-1][0];
    }
}

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入:prices = [7,6,4,3,1] 
输出:0 
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:

输入:prices = [1]
输出:0
class Solution {
    public int maxProfit(int[] prices) {
        int K = 2; // 交易次数
        int n = prices.length;
        int[][][] dp = new int[n][K+1][2];

        for (int i = 0; i < n; i++) {
            for (int k = K; k >= 1; k--){
                if (i == 0){
                    // base case
                    dp[0][k][0] = 0; // 第0天未购入股票
                    dp[0][k][1] = -prices[i]; // 第0天购入股票
                    continue;
                }
                dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
                dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]); // 第i天买入股票,消耗一次交易次数
            }
        }

        return dp[n-1][K][0];
    }
}

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
class Solution {
    public int maxProfit(int K, int[] prices) {
        int n = prices.length;
        int[][][] dp = new int[n][K+1][2];

        for (int i = 0; i < n; i++){
            // 处理k=0
            dp[i][0][1] = Integer.MIN_VALUE;
            dp[i][0][0] = 0;
        }

        for (int i = 0; i < n; i++) {
            for (int k = K; k >= 1; k--) {
                // base
                if (i == 0){
                    dp[i][k][0] = 0;
                    dp[i][k][1] = -prices[i];
                    continue;
                }
                dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
                dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
            }
        }
        return dp[n-1][K][0];
    }
}

给定一个整数数组prices,其中第 prices[i] 表示第 *i* 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:

输入: prices = [1]
输出: 0
class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        // dp[i][0]表示第i天未持有股票
        // dp[i][1]表示第i天持有股票
        int[][] dp = new int[n][2];

        for (int i = 0; i < n; i++){
            if (i == 0){
                dp[0][0] = 0;
                dp[0][1] = -prices[0];
                continue;
            }
            
            if (i == 1){
                dp[1][0] = Math.max(dp[0][1] + prices[1], dp[0][0]);
                dp[1][1] = Math.max(dp[0][1], -prices[1]);
                continue;
            }

            // 第i天持股 -> {第i-1天买入股票, 第i-1天就有股票}
            dp[i][1] = Math.max(dp[i-2][0] - prices[i], dp[i-1][1]);
            // 第i天未持股 -> {第i-2天卖出股票, 第i-1天就未持股}
            dp[i][0] = Math.max(dp[i-1][1] + prices[i], dp[i-1][0]);
        }

        return dp[n-1][0];
    }
}

10.5 子序列问题

10.5.1 不连续子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];

        Arrays.fill(dp, 1);

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

        int res = 0;
        for (int i = 0; i < n; i++){
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m+1][n+1];

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (text1.charAt(i-1) == text2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i]nums2[j] 的直线,这些直线需要同时满足满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

img

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:

输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

跟上面的[最长公共子序列](#1143. 最长公共子序列 - 力扣(LeetCode))一样,只是把字符数组(String)改成了整形数组

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m+1][n+1];

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
}

10.5.2 连续子序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 

示例 2:

输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

跟[最长递增子序列](#300. 最长递增子序列 - 力扣(LeetCode))类似

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int n = nums.length;
        // dp[i] 表示以下标i为结尾的连续递增的子序列长度为dp[i]
        int[] dp = new int[n];
        int res = 1;
        Arrays.fill(dp, 1);

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

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

示例 1:

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

示例 2:

输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5

跟[不相交的线](#1035. 不相交的线 - 力扣(LeetCode))类似

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m+1][n+1];
        int res = 0;

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (nums1[i-1] == nums2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                }
                res = Math.max(res, dp[i][j]);
            }
        }
        return res;
    }
}

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

时间复杂度必须是$O(n)$,$O(n^2)$会超时

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        // 以 nums[i] 为结尾的「最大子数组和」为 dp[i]
        int[] dp = new int[n];
        int res = Integer.MIN_VALUE;

        for (int i = 0; i < n; i++){
            if (i == 0){
                dp[i] = nums[0];
            }else{
                dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

10.5.3 编辑距离

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true

示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false
class Solution {
    public boolean isSubsequence(String s, String t) {
        int i = 0, j = 0;
        while (i < s.length() && j < t.length()){
            if (s.charAt(i) == t.charAt(j)){
                i++;
            }
            j++;
        }
        return i == s.length();
    }
}

给你两个字符串 st ,统计并返回在 s子序列t 出现的个数,结果需要对 109 + 7 取模。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag
class Solution {
    public int numDistinct(String s, String t) {
        int m = s.length(), n = t.length();
        // dp[i][j]表示s[0...i-1]中出现t[0...j-1]的个数
        int[][] dp = new int[m+1][n+1];

        // 初始化
        for (int i = 0; i <= m; i++) dp[i][0] = 1;
        // for (int j = 1; j <= n; j++) dp[0][j] = 0;   // 可省略

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (s.charAt(i-1) == t.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                }else{
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[m][n];
    }
}

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

输入:word1 = "leetcode", word2 = "etco"
输出:4
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        // dp[i][j]表示word1[0...i-1]和word2[0...j-1]有几个相同元素
        // 求word1和word2的最长公共子序列
        int[][] dp = new int[m+1][n+1];

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j - 1]);
                }
            }
        }

        return m - dp[m][n] + n - dp[m][n];
    }
}

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        // dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
        int[][] dp = new int[m+1][n+1];

        // 初始化
        for (int i = 0; i <= m; i++) dp[i][0] = i;
        for (int j = 0; j <= n; j++) dp[0][j] = j;

        for (int i = 1; i <= m; i++){
            for (int j = 1; j <= n; j++){
                if (word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1;
                }
            }
        }
        return dp[m][n];
    }

    public int min (int a, int b, int c){
        return Math.min(a, Math.min(b, c));
    }
}

10.5.4 回文

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
// dp解法
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int res = 0;

        // 从下到上,从左到右
        for (int i = n - 1; i >= 0; i--){
            for (int j = i; j < n; j++){
                if (s.charAt(i) == s.charAt(j)){
                    if (j - i <= 1){
                        dp[i][j] = true;
                        res++;
                    }else if (dp[i+1][j-1]){
                        dp[i][j] = true;
                        res++;
                    }
                }
            }
        }
        return res;
    }
}
// 双指针法
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        int res = 0;
        for (int i = 0; i < n; i++){
            res += count(s, i, i);
            res += count(s, i, i+1);
        }
        return res;
    }

    public int count(String s, int left, int right){
        int res = 0;
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
            res++;
            left--;
            right++;
        }
        return res;
    }
}

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        int[][] dp = new int[n+2][n+2];

        // 从左往右,从下往上
        for (int i = n; i >= 1; i--){
            for (int j = i; j <= n; j++){
                if (s.charAt(i-1) == s.charAt(j-1)){
                    if (i == j){
                        dp[i][j] = 1;
                    }else if (j - i == 1){
                        dp[i][j] = 2;
                    }else{
                        dp[i][j] = dp[i+1][j-1] + 2;
                    }
                }else{
                    dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
                }
            }
        } 
        return dp[1][n];
    }
}