Question:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出「和为目标值」的那「两个」整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:
```
输入:nums = [2,7,11,15], target = 9

输出:[0,1]

解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
```

示例 2:
```
输入:nums = [3,2,4], target = 6

输出:[1,2]
```

示例 3:
```
输入:nums = [3,3], target = 6

输出:[0,1]
```

提示:
* $2 <= nums.length <= 10^3$
* -$10^9 <= nums[i] <= 10^9$
* -$10^9 <= target <= 10^9$
* 只会存在一个有效答案

Solution:
### 朴素解法

由于我们每次要从数组中找两个数。

因此一个很简单的思路是:使用两重循环枚举下标 $i$ 和 $j$ ,分别代表要找的两个数。

然后判断 $nums[i] + nums[j] = target$ 是否成立。

另外为了防止得到重复的解,我们需要在第一层循环中让 $i$ 从 $0$ 开始,到 $n - 2$ 结束(确保能取到下一位数作为 j );在第二层循环中让 $j$ 从 $i + 1$ 开始,到 $n - 1$ 结束。

代码:

```Java
class Solution {
    public int[] twoSum(int[] nums, int t) {
        int n = nums.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                if (t == nums[i] + nums[j]) return new int[]{i,j};
            }
        }
        return new int[]{};
    }
}
```
* 时间复杂度:两重循环,以复杂度是 $O(n^2)$
* 空间复杂度:$O(1)$

---

### 哈希表

首先,任何优化的思路都不外乎「减少重复」。

从朴素解法中可以知道,逻辑上我们是先定下来一个数,然后从数组中往后找另外一个值是否存在。但其实我们在找第二个数的过程中是重复扫描了数组多次。

举个例子,对于 `nums = [2,3,8,4], target = 9` 的样例,我们先确定下来第一个数是 `2`,然后从后扫描到最后一个数,检查是否有 `7`。发现没有,再决策第一个数为 `3` 的情况,这时候我们应该利用前一次扫描的结果来帮助我们快速判断是否存在 `6`,而不是再重新进行一次扫描。

这是直观的优化思路,不难想到可以使用哈希表进行存储。以数值为 `key`,数值的下标为 `value`。

当动手将想法转化为代码时,会发现如果先敲定第一个数,将后面的数加入哈希表,再进行下一位的遍历的时候,还需要将该数值从哈希表中进行删除。

代码:
```Java
class Solution {
    public int[] twoSum(int[] nums, int t) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) map.put(nums[i], i);
        for (int i = 0; i < nums.length; i++) {
            int a = nums[i], b = t - a;
            if (map.get(a) == i) map.remove(a);
            if (map.containsKey(b)) return new int[]{i, map.get(b)};
        }
        return new int[]{};
    }
}
```
最坏情况下,每个数会对应一次哈希表的插入和删除。该解法本质是在循环过程中敲定第一个数,在哈希表中找该数后面是否存在第二个数。

这时候不妨将思路转换过来,遍历过程中敲定第二个数,使用哈希表在第二个数的前面去找第一个数。

具体的做法是,边遍历边存入哈希表,遍历过程中使用的下标 i 用作敲定第二个数,再从现有的哈希表中去找另外一个目标数(由于下标 $i$ 前面的数都被加入哈希表了,即在下标 $i$ 前面去找第一个数)。

```Java
class Solution {
    public int[] twoSum(int[] nums, int t) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int a = nums[i], b = t - a;
            if (map.containsKey(b)) return new int[]{map.get(b), i};
            map.put(a, i);
        }
        return new int[]{};
    }
}
```
从 LeetCode 上的执行时间来看,第一种哈希表做法是 4ms,而第二种哈希表的做法是 0ms(不足 1ms 的意思),快在了我们减少了哈希表的插入和删除操作。

但这只是常数级别上的优化,LeetCode 上的执行时间建议只作普通参考,还是要从算法的时空复杂度来分析快慢。

* 时间复杂度:第一种哈希表的做法会扫描数组两次,复杂度是 $O(n)$(忽略常数);第二种做法只会对数组进行一遍扫描,复杂度是 $O(n)$
* 空间复杂度:两种做法都使用了哈希表进行记录,复杂度是 $O(n)$

---

### 其他

可以看到,我将原题目的入参 `target` 替换成了 `t`,但并不影响正确性,目的是为了提高编码速度。如果你也经常参与 LeetCode 周赛,就会发现这是一个常用的小技巧。

这个技巧,我希望你在第一题就掌握。

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个字符串 `s` 和一个字符规律 `p`,请你来实现一个支持 `'.'` 和 `'*'` 的正则表达式匹配。

* `'.'` 匹配任意单个字符
* `'*'` 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖整个字符串 `s` 的,而不是部分字符串。

示例 1:
```
输入:s = "aa" p = "a"

输出:false

解释:"a" 无法匹配 "aa" 整个字符串。
```
示例 2:
```
输入:s = "aa" p = "a*"

输出:true

解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
```
示例 3:
```
输入:s = "ab" p = ".*"

输出:true

解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
```
示例 4:
```
输入:s = "aab" p = "c*a*b"

输出:true

解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
```
示例 5:
```
输入:s = "mississippi" p = "mis*is*p*."

输出:false
```

提示:
* $0 <= s.length <= 20$
* $0 <= p.length <= 30$
* `s` 可能为空,且只包含从 `a-z` 的小写字母。
* `p` 可能为空,且只包含从 `a-z` 的小写字母,以及字符 `.` 和 `*`。
* 保证每次出现字符 `*` 时,前面都匹配到有效的字符

Solution:
### 动态规划

整理一下题意,对于字符串 `p` 而言,有三种字符:

* 普通字符:需要和 `s` 中同一位置的字符完全匹配
* `'.'`:能够匹配 `s` 中同一位置的任意字符
* `'*'`:不能够单独使用 `'*'`,必须和前一个字符同时搭配使用,数据保证了 `'*'` 能够找到前面一个字符。能够匹配 `s` 中同一位置字符任意次。

所以本题关键是分析当出现 `a*` 这种字符时,是匹配 $0$ 个 `a`、还是 $1$ 个 `a`、还是 $2$ 个 `a` ...

本题可以使用动态规划进行求解:

* 状态定义:`f(i,j)` 代表考虑 `s` 中以 `i` 为结尾的子串和 `p` 中的 `j` 为结尾的子串是否匹配。最终我们要求的结果为 `f[n][m]` 。

* 状态转移:也就是我们要考虑 `f(i,j)` 如何求得,前面说到了 `p` 有三种字符,所以这里的状态转移也要分三种情况讨论:

  1. `p[j]` 为普通字符:匹配的条件是前面的字符匹配,同时 `s` 中的第 `i` 个字符和 `p` 中的第 `j` 位相同。

     `f(i,j) = f(i - 1, j - 1) && s[i] == p[j]` 。

	2. `p[j]` 为 `'.'`:匹配的条件是前面的字符匹配, `s` 中的第 `i` 个字符可以是任意字符。

     `f(i,j) = f(i - 1, j - 1) && p[j] == '.'`。

  3. `p[j]` 为 `'*'`:读得 `p[j - 1]` 的字符,例如为字符 `a`。 然后根据 `a*` 实际匹配 `s` 中 `a` 的个数是 $0$ 个、$1$ 个、$2$ 个 ...

    3.1. 当匹配为 $0$ 个:`f(i,j) = f(i, j - 2)`

    3.2. 当匹配为 $1$ 个:`f(i,j) = f(i - 1, j - 2) && (s[i] == p[j - 1] || p[j - 1] == '.')`

    3.3. 当匹配为 $2$ 个:`f(i,j) = f(i - 2, j - 2) && ((s[i] == p[j - 1] && s[i - 1] == p[j - 1]) || p[j] == '.')`

**我们知道,通过「枚举」来确定 `*` 到底匹配多少个 `a` 这样的做法,算法复杂度是很高的。**

**我们需要挖掘一些「性质」来简化这个过程。**

代码:
```Java
class Solution {
    public boolean isMatch(String ss, String pp) {
        // 技巧:往原字符头部插入空格,这样得到 char 数组是从 1 开始,而且可以使得 f[0][0] = true,可以将 true 这个结果滚动下去
        int n = ss.length(), m = pp.length();
        ss = " " + ss;
        pp = " " + pp;
        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();
        // f(i,j) 代表考虑 s 中的 1~i 字符和 p 中的 1~j 字符 是否匹配
        boolean[][] f = new boolean[n + 1][m + 1];
        f[0][0] = true;
        for (int i = 0; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 如果下一个字符是 '*',则代表当前字符不能被单独使用,跳过
                if (j + 1 <= m && p[j + 1] == '*' && p[j] != '*') continue;

                // 对应了 p[j] 为普通字符和 '.' 的两种情况
                if (i - 1 >= 0 && p[j] != '*') {
                    f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
                }

                // 对应了 p[j] 为 '*' 的情况
                else if (p[j] == '*') {
                    f[i][j] = (j - 2 >= 0 && f[i][j - 2]) || (i - 1 >= 0 && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.'));
                }
            }
        }
        return f[n][m];
    }
}
```
* 时间复杂度:$n$ 表示 `s` 的长度,$m$ 表示 `p` 的长度,总共 $n \times m$ 个状态。复杂度为 $O(n \times m)$
* 空间复杂度:使用了二维数组记录结果。复杂度为 $O(n \times m)$

**动态规划本质上是枚举(不重复的暴力枚举),因此其复杂度很好分析,有多少个状态就要被计算多少次,复杂度就为多少。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.10` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 $0$ 之外,这两个数都不会以 $0$ 开头。

示例 1:

```
输入:l1 = [2,4,3], l2 = [5,6,4]

输出:[7,0,8]

解释:342 + 465 = 807.
```
示例 2:
```
输入:l1 = [0], l2 = [0]

输出:[0]
```
示例 3:
```
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]

输出:[8,9,9,9,0,0,0,1]
```

提示:

* 每个链表中的节点数在范围 $[1, 100]$ 内
* $0 <= Node.val <= 9$
* 题目数据保证列表表示的数字不含前导零

Solution:
### 朴素解法(哨兵技巧)

这是道模拟题,模拟人工竖式做加法的过程:

从最低位至最高位,逐位相加,如果和大于等于 $10$,则保留个位数字,同时向前一位进 1 如果最高位有进位,则需在最前面补 $1$。

做有关链表的题目,有个常用技巧:添加一个虚拟头结点(哨兵),帮助简化边界情况的判断。

代码:
```Java
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode tmp = dummy;
        int t = 0;
        while (l1 != null || l2 != null) {
            int a = l1 != null ? l1.val : 0;
            int b = l2 != null ? l2.val : 0;
            t = a + b + t;
            tmp.next = new ListNode(t % 10);
            t /= 10;
            tmp = tmp.next;
            if (l1 != null) l1 = l1.next;
            if (l2 != null) l2 = l2.next;
        }
        if (t > 0) tmp.next = new ListNode(t);
        return dummy.next;
    }
}
```
* 时间复杂度:$m$ 和 $n$ 分别代表两条链表的长度,则遍历到的最远位置为 $max(m,n)$,复杂度为 $O(max(m,n))$
* 空间复杂度:创建了 $max(m,n) + 1$ 个节点(含哨兵),复杂度为 $O(max(m,n))$(忽略常数)

**注意:事实上还有可能创建 $max(m + n) + 2$ 个节点,包含哨兵和最后一位的进位。但复杂度仍为 $O(max(m,n))$。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.2` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


3. 无重复字符的最长子串

Question:
给定一个字符串,请你找出其中不含有重复字符的「最长子串」的长度。

示例 1:
```
输入: s = "abcabcbb"

输出: 3

解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
```
示例 2:
```
输入: s = "bbbbb"

输出: 1

解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
```
示例 3:
```
输入: s = "pwwkew"

输出: 3

解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
```
示例 4:
```
输入: s = ""

输出: 0
```

提示:

* $0 <= s.length <= 5 \times 10^4$
* `s` 由英文字母、数字、符号和空格组成

Solution:
### 滑动窗口

定义两个指针 `start` 和 `end`, `[start:end]` 表示当前处理到的子串。

子串 `[start:end]` 始终满足要求:无重复字符。

从前往后进行扫描 `s`,用哈希表记录 `[start:end]` 中每字符的出现次数。

遍历过程中,`end` 不断自增,将第 `end` 个字符在哈希表中出现的次数加一。

令 `right` 为下标 `end` 对应的字符,当满足 `map.get(right) > 1` 时,代表此前出现过第 `end` 位对应的字符。

此时更新 `start` 的位置(使其右移),直到不满足 `map.get(right) > 1` (代表 `[start:end]` 恢复满足无重复字符的条件),再用 `[start:end]` 长度更新答案。

Java 代码:
```Java
class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> map = new HashMap<>();
        int ans = 0;
        for (int start = 0, end = 0; end < s.length(); end++) {
            char right = s.charAt(end);
            map.put(right, map.getOrDefault(right, 0) + 1);
            while (map.get(right) > 1) {
                char left = s.charAt(start);
                map.put(left, map.get(left) - 1);
                start++;
            }
            ans = Math.max(ans, end - start + 1);
        }
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        map = {}
        ans = 0
        start = 0
        for end in range(len(s)):
            right = s[end]
            map[right] = map.get(right, 0) + 1
            while map[right] > 1:
                left = s[start]
                map[left] = map[left] - 1
                start += 1
            ans = max(ans, end - start + 1)
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> map;
        int ans = 0;
        for (int start = 0, end = 0; end < s.length(); end++) {
            char right = s[end];
            map[right] = map[right] + 1;
            while (map[right] > 1) {
                char left = s[start];
                map[left] = map[left] - 1;
                start++;
            }
            ans = max(ans, end - start + 1);
        }
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function lengthOfLongestSubstring(s: string): number {
  const map: { [key: string]: number } = {};
  let ans = 0;
  let start = 0;
  for (let end = 0; end < s.length; end++) {
      const right = s.charAt(end);
      map[right] = (map[right] || 0) + 1;
      while (map[right] > 1) {
          const left = s.charAt(start);
          map[left] = (map[left] || 0) - 1;
          start++;
      }
      ans = Math.max(ans, end - start + 1);
  }
  return ans;
};
```
* 时间复杂度:虽然有两层循环,但每个字符在哈希表中最多只会被插入和删除一次,复杂度为 $O(n)$
* 空间复杂度:使用了哈希表进行字符记录,复杂度为 $O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.3` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


4. 寻找两个正序数组的中位数

Question:
给定两个大小分别为 $m$ 和 $n$ 的正序(从小到大)数组 `nums1` 和 `nums2`。

请你找出并返回这两个正序数组的「中位数」。

示例 1:
```
输入:nums1 = [1,3], nums2 = [2]

输出:2.00000

解释:合并数组 = [1,2,3] ,中位数 2
```
示例 2:
```
输入:nums1 = [1,2], nums2 = [3,4]

输出:2.50000

解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
```
示例 3:
```
输入:nums1 = [0,0], nums2 = [0,0]

输出:0.00000
```
示例 4:
```
输入:nums1 = [], nums2 = [1]

输出:1.00000
```
示例 5:
```
输入:nums1 = [2], nums2 = []

输出:2.00000
```

提示:
* $nums1.length == m$
* $nums2.length == n$
* $0 <= m <= 1000$
* $0 <= n <= 1000$
* $1 <= m + n <= 2000$
* $-10^6 <= nums1[i], nums2[i] <= 10^6$

进阶:你能设计一个时间复杂度为 $O(\log (m+n))$ 的算法解决此问题吗?

Solution:
### 朴素解法

如果忽略进阶的 $O(\log{(m + n)})$ 要求,这道题就非常简单。

一个比较直观的做法:将两个数组合并,排序,然后分别取得 `total / 2` 和 `(total - 1) / 2` 两个位置的数,取两者平均值。

这样做的目的是为了避免分情况讨论:合并后的数组长度是奇数还是偶数。

```Java
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        int[] arr = new int[n + m];
        int idx = 0;
        for (int i : nums1) arr[idx++] = i;
        for (int i : nums2) arr[idx++] = i;
        Arrays.sort(arr);
        int l = arr[(n + m) / 2], r = arr[(n + m - 1) / 2];
        return (l + r) / 2.0;
    }
```
* 时间复杂度:合并两个数组的复杂度是 $O(m + n)$,对合并数组进行排序的复杂度是 $O((m + n)\log{(m + n)})$。整体复杂度是 $O((m + n)\log{(m + n)})$
* 空间复杂度:$O(1)$

**注意:`Arrays.sort()` 不只有双轴快排实现,这里的复杂度分析是假定其使用双轴快排。**

---

### 分治解法

首先可以将原问题等效为:从两个有序数组中找第 k 小的数。

分两种情况讨论:

1. 总个数为偶数:找到 `第 (total / 2) 个小的数` 和 `第 (total / 2 + 1) 个小的数`,结果为两者的平均值。

2. 总个数为奇数:结果为 `第 (total / 2 + 1) 个小的数` 。

具体思路为:

* 默认第一个数组比第二个数组的有效长度短,如果不满足,则调换两个数组(这也是一个常用技巧,目的是减少边界处理工作:原本需要对两个数组做越界检查,现在只需要对短的数组做越界检查)

* 第一个数组的有效长度从 `i` 开始,第二个数组的有效长度从 `j` 开始,其中 `[i,si - 1]` 是第一个数组的前 `k / 2` 个元素,`[j,sj - 1]` 是第二个数组的前 `k - k / 2` 个元素(为了确保 k 为奇数的时候正确)

* 当 `nums1[si - 1] > nums2[sj - 1]`:则表示第 `k` 小一定不在 `[j,sj - 1]` 中,即在 `[i,n]` 或 `[sj,m]` 中

* 当 `nums1[si - 1] <= nums2[sj - 1]`:则表示第 `k` 小一定不在 `[i,si - 1]` 中,即在 `[si,n]` 或 `[j,m]` 中

```Java
class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int tot = nums1.length + nums2.length;
        if (tot % 2 == 0) {
            int left = find(nums1, 0, nums2, 0, tot / 2);
            int right = find(nums1, 0, nums2, 0, tot / 2 + 1);
            return (left + right) / 2.0;
        } else {
            return find(nums1, 0, nums2, 0, tot / 2 + 1);
        }
    }
    int find(int[] n1, int i, int[] n2, int j, int k) {
        if (n1.length - i > n2.length - j) return find(n2, j, n1, i, k);
        if (i >= n1.length) return n2[j + k - 1];
        if (k == 1) {
            return Math.min(n1[i], n2[j]);
        } else {
            int si = Math.min(i + (k / 2), n1.length), sj = j + k - (k / 2);
            if (n1[si - 1] > n2[sj - 1]) {
                return find(n1, i, n2, sj, k - (sj - j));
            } else {
                return find(n1, si, n2, j, k - (si - i));
            }
        }
    }
}
```
* 时间复杂度:每次递归 k 的规模都减少一半,复杂度为 $O(\log{(m + n)})$
* 空间复杂度:$O(1)$

---

### 总结

今天这道题,我给你介绍了两种技巧:

1. 在机试或者竞赛中,目的是尽可能快的 AC,所以 Java 可以直接不写 `private` 的修饰符(不写代表使用默认的包权限),这没有问题,不用纠结
2. 在机试或者竞赛中,遇到一些是从文字上限制我们的题目,例如本题限制我们使用 $O(\log{(m+n)})$ 算法。可以分析是否能够不按照限制要求来做,具体分析思路为:
   1. 先有一个很容易实现的算法思路。例如本题很容易就想到直接使用双指针找第 k 个小的数,复杂度为 $O(n)$。
   2. 看题目的数据规模 1 是否支撑我们使用限制以外的算法。例如本题数据规模只有 1000 + 1000 = 2000。
   3. 根据数据规模,判断我们的朴素算法计算机是否可以在 1s 内处理完 2 ,即判断运算次数是否在 $10^7$ 以内 3 。例如本题使用双指针算法,指针移动和判断大小算一次运行,由于数据只有 2000,距离 $10^7$ 还很远,所以完全足够了

说明 1:正规的算法题目都会提供数据规模,LeetCode 上一些旧题目没有提供,是因为当时出的时候不太规范,LeetCode 新题、其他 OJ 平台题目,算法竞赛题目都会有。

说明 2:即使是最严格的 OJ 中最简单的题目,也会提供 1s 的运行时间,超过这个时间才算超时。

说明 3:计算器 1s 内极限的处理速度是 $10^8$ ,但为了尽可能不出现错误提交,使用技巧时尽量和 $10^7$ 进行比较。

**注意:这两个技巧,我只推荐在机试或者竞赛(尽可能快 AC 的场景)中使用。平时练习或者和面试的时候必须老实按照题目要求来。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.4` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个字符串 `s`,找到 `s` 中最长的回文子串。

示例 1:
```
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
```
示例 2:
```
输入:s = "cbbd"
输出:"bb"
```
示例 3:
```
输入:s = "a"
输出:"a"
```
示例 4:
```
输入:s = "ac"
输出:"a"
```

提示:

* $1 <= s.length <= 1000$
* `s` 仅由数字和英文字母(大写和/或小写)组成

Solution:
### 朴素解法

这道题有一个很容易就能想到的简单做法:枚举字符串 `s` 中的每一位,作为回文串的中心点,左右进行扩展,直到达到边界或者不满足回文串定义为止。

这样做的思路必然是正确的。

但很显然这是一个朴素(暴力)做法,那么我们如何确定这一做法是否可行呢?

还记得我们上一节的分析思路吗?当我们有了一个简单的实现方法之后,需要从**题目的数据规模**、**计算机的处理速度**和**实现方法的计算量**出发,判断这样的做法是否不会超时。

由于字符串长度最多只有 1000,而我们的实现方法是 $O(n^2)$,因此我们算法的计算量应该在 $10^6$ 以内,是在计算机每秒的处理范围内的。

首先枚举回文串的中心 i,然后分两种情况向两边扩展边界,直到达到边界或者不满足回文串定义为止:

* 回文串长度是奇数,则依次判断 `s[i − k] == s[i + k], k = 1,2,3...`

* 回文串长度是偶数,则依次判断 `s[i − k] == s[i + k − 1], k = 1,2,3...`

代码:
```Java
class Solution {
    public String longestPalindrome(String s) {
        String ans = "";
        for (int i = 0; i < s.length(); i++) {
            // 回文串为奇数
            int l = i - 1, r = i + 1;
            String sub = getString(s, l, r);
            if (sub.length() > ans.length()) ans = sub;

            // 回文串为偶数
            l = i - 1;
            r = i + 1 - 1;
            sub = getString(s, l, r);
            if (sub.length() > ans.length()) ans = sub;
        }
        return ans;
    }
    String getString(String s, int l, int r) {
        while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            l--;
            r++;
        }
        return s.substring(l + 1, r);
    }
}
```
* 时间复杂度:先枚举了 `s` 中的每个字符作为回文串的中心点,再从中心点出发左右扩展,最多扩展到边界。复杂度是 $O(n^2)$
* 空间复杂度:$O(1)$

---
### Manacher 算法

这是一个比较冷门的算法,使用范围也比较单一,只能用于解决「回文串」问题。

Manacher 确实是「回文串」问题的最优解。

但事实上我还没有见过必须使用 Manacher 算法才能过的回文串题。

因此我这里直接给解决方案(可以直接当做模板来使用),而不再讨论算法的具体实现原理。

Manacher 算法较长,为了避免回文串长度奇偶问题的分情况讨论,我会对原字符进行处理,在边界和字符之间插入占位符。

使用了这样的技巧之后,**当非占位字符作为回文串的中心时,对应了回文串长度为奇数的情况;当占位字符作为回文串的中心时,对应了回文串长度为偶数的情况。**。

举个例子:

> 原字符:"babad",转换后:"\*b\*a\*b\*a\*d\*",得到的回文串:"\*b\*a\*b\*",然后再去除占位符输出:"bab"。

*解释:"aba" 同样是符合题意的答案。*

代码:
```Java
class Solution {
    public String longestPalindrome(String s) {
        if (s.length() == 1) return s;

        char[] chars = manacherString(s);
        int n = chars.length;
        int[] pArr = new int[n];
        int C = -1, R = -1, pos = -1;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            pArr[i] = i < R ? Math.min(pArr[C * 2 - i], R - i) : 1;
            while (i + pArr[i] < n && i - pArr[i] > -1) {
                if (chars[i + pArr[i]] == chars[i - pArr[i]]) {
                    pArr[i]++;
                } else {
                    break;
                }
            }
            if (i + pArr[i] > R) {
                R = i + pArr[i];
                C = i;
            }
            if (pArr[i] > max) {
                max = pArr[i];
                pos = i;
            }
        }
        int offset = pArr[pos];
        StringBuilder sb = new StringBuilder();
        for (int i = pos - offset + 1; i <= pos + offset - 1; i++) {
            if (chars[i] != '#') sb.append(chars[i]);
        }
        return sb.toString();
    }
    char[] manacherString(String s) {
        char[] chars = new char[s.length() * 2 + 1];
        for (int i = 0, idx = 0; i < chars.length; i++) {
            chars[i] = (i & 1) == 0 ? '#' : s.charAt(idx++);
        }
        return chars;
    }
}
```
* 时间复杂度:只对字符串进行了一次扫描。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 总结

今天这道题目,三叶除了提供常规的、时间复杂度为 $O(n^2)$ 的朴素解法以外,还给你提供了关于「回文串」的最优解 Manacher 算法模板,建议有余力的同学可以背过。

背过这样的算法的意义在于:相当于大脑里有了一个时间复杂度为 $O(n)$ 的 api 可以使用,这个 api 传入一个字符串,返回该字符串的最大回文子串。

同时借助 Manacher 算法,还给大家介绍了如何避免回文串长度的分情况讨论,这个技巧只要涉及「回文串」问题都适用(无论是否使用 Manacher 算法)。

对于想要背过 Manacher 算法的同学,建议先敲 3 遍,默写 2 遍,然后过了 24 小时,再默写 2 遍,一周后,再进行重复,直到熟练。

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.5` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
将一个给定字符串 `s` 根据给定的行数 `numRows` ,以从上往下、从左到右进行 `Z` 字形排列。

比如输入字符串为 `"PAYPALISHIRING"` 行数为 $3$ 时,排列如下:
```
P   A   H   N
A P L S I I G
Y   I   R
```
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如 `"PAHNAPLSIIGYIR"` 。

请你实现这个将字符串进行指定行数变换的函数:`string convert(string s, int numRows);`

示例 1:
```
输入:s = "PAYPALISHIRING", numRows = 3

输出:"PAHNAPLSIIGYIR"
```
示例 2:
```
输入:s = "PAYPALISHIRING", numRows = 4

输出:"PINALSIGYAHRPI"

解释:
P     I    N
A   L S  I G
Y A   H R
P     I
```
示例 3:
```
输入:s = "A", numRows = 1

输出:"A"
```

提示:
* $1 <= s.length <= 1000$
* `s` 由英文字母(小写和大写)、`','` 和 `'.'` 组成
* $1 <= numRows <= 1000$

Solution:
### 模拟

由于最终是要我们对 `Z` 型矩阵进行从上往下、从左往右的构建输出。

因此可以构建一个矩阵 `g` 存储 `Z` 型中的每行字符(忽略 `Z` 中的空格间隙),同时使用 `idxs` 存储 `Z` 型中每行用到的下标。

目标 `Z` 型 和 矩阵 `g` 以及 `idxs` 三者关系:
```
P     I    N             PIN                3
A   L S  I G    =>       ALSIG     =>       5
Y A   H R                YAHR               4
P     I                  PI                 2
```

代码:
```Java
class Solution {
    static int N = 1010;
    static char[][] g = new char[N][N];
    static int[] idxs = new int[N];
    public String convert(String s, int m) {
        if (m == 1) return s;
        int n = s.length();
        Arrays.fill(idxs, 0);
        for (int i = 0, j = 0, k = 1; i < n; i++) {
            g[j][idxs[j]++] = s.charAt(i);
            j += k;
            if (j < 0) {
                j += 2; k = 1;
            } else if (j == m) {
                j -= 2; k = -1;
            }
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < idxs[i]; j++) {
                sb.append(g[i][j]);
            }
        }
        return sb.toString();
    }
}
```
* 时间复杂度:创建数组的工作只会发生一次,清空 `idxs` 数组操作会发生在每个样例中,复杂度为 $O(m)$;将 `s` 的每个字符填入矩阵的复杂度为 $O(n)$;从矩阵中取出字符构建答案复杂度为 $O(n)$。整体复杂度为 $O(m + n)$
* 空间复杂度:$O(n \times m)$

---

### 数学规律

对于本题,我们可以不失一般性的将规律推导为「首项」和「公差公式」。

这通常能够有效减少一些判断。

分情况讨论:

* 对于第一行和最后一行:公差为 `2 * (n − 1)` 的等差数列,首项是 `i`
* 对于其他行:两个公差为 `2 * (n − 1)` 的等差数列交替排列,首项分别是 `i` 和 `2 * n − i − 2`

代码:
```Java
class Solution {
    public String convert(String s, int r) {
        int n = s.length();
        if (n == 1 || r == 1) return s;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < r; i++) {
            if (i == 0 || i == r - 1) {
                int pos = i, offset = 2 * (r - 1);
                while (pos < n) {
                    sb.append(s.charAt(pos));
                    pos += offset;
                }
            } else {
                int pos1 = i, pos2 = 2 * r - i - 2;
                int offset = 2 * (r - 1);
                while (pos1 < n || pos2 < n) {
                    if (pos1 < n) {
                        sb.append(s.charAt(pos1));
                        pos1 += offset;
                    }
                    if (pos2 < n) {
                        sb.append(s.charAt(pos2));
                        pos2 += offset;
                    }
                }
            }
        }
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.6` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个 $32$ 位的有符号整数 `x` ,返回将 `x` 中的数字部分反转后的结果。

如果反转后整数超过 $32$ 位的有符号整数的范围 $[−2^{31}, 2^{31} − 1]$ ,就返回 $0$。

假设环境不允许存储 $64$ 位整数(有符号或无符号)。

示例 1:
```
输入:x = 123

输出:321
```
示例 2:
```
输入:x = -123

输出:-321
```
示例 3:
```
输入:x = 120

输出:21
```
示例 4:
```
输入:x = 0

输出:0
```

提示:
* -$2^{31} <= x <= 2^{31}$ - 1

Solution:
### 不完美解法

在机试或者周赛这种需要快速 AC 的场景中,遇到这种从文字上进行限制的题目,可以选择性的忽略限制。

对于本题,题目从文字上限制我们只能使用 $32$ 位的数据结构(`int`)。

但由于数据范围过大,使用 `int` 会有溢出的风险,所以我们使用 `long` 来进行计算,在返回再转换为 `int`。

代码:
```Java
class Solution {
    public int reverse(int x) {
        long ans = 0;
        while (x != 0) {
            ans = ans * 10 + x % 10;
            x = x / 10;
        }
        return (int)ans == ans ? (int)ans : 0;
    }
}
```
* 时间复杂度:数字 $x$ 的位数,数字大约有 $\log{x}$ 位。复杂度为 $O(\log{x})$
* 空间复杂度:$O(1)$

---

### 完美解法

在「不完美解法」中,我们使用了不符合文字限制的 `long` 数据结构。

接下来我们看看,不使用 `long` 该如何求解。

从上述解法来看,我们在循环的 `ans = ans * 10 + x % 10` 这一步会有溢出的风险,因此我们需要边遍历边判断是否溢出:

* 对于正数而言:溢出意味着 `ans * 10 + x % 10 > Integer.MAX_VALUE`,对等式进行变化后可得 `ans > (Integer.MAX_VALUE - x % 10) / 10)`。所以我们可以根据此变形公式进行预判断

* 对于负数而言:溢出意味着 `ans * 10 + x % 10 < Integer.MIN_VALUE`,对等式进行变化后可得 `ans < (Integer.MIN_VALUE - x % 10) / 10)`。所以我们可以根据此变形公式进行预判断

代码:
```Java
class Solution {
    public int reverse(int x) {
        int ans = 0;
        while (x != 0) {
            if (x > 0 && ans > (Integer.MAX_VALUE - x % 10) / 10) return 0;
            if (x < 0 && ans < (Integer.MIN_VALUE - x % 10) / 10) return 0;
            ans = ans * 10 + x % 10;
            x /= 10;
        }
        return ans;
    }
}
```
* 时间复杂度:数字 $x$ 的位数,数字大约有 $\log{x}$ 位。复杂度为 $O(\log{x})$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.7` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
请你来实现一个 `myAtoi(string s)` 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 `myAtoi(string s)` 的算法如下:

* 读入字符串并丢弃无用的前导空格
* 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
* 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
* 将前面步骤读入的这些数字转换为整数(即,`"123" -> 123`, `"0032" -> 32`)。如果没有读入数字,则整数为 $0$ 。必要时更改符号(从步骤 $2$ 开始)。
* 如果整数数超过 $32$ 位有符号整数范围 [−$2^{31}$,  $2^{31}$ − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −$2^{31}$ 的整数应该被固定为 −$2^{31}$ ,大于 $2^{31}$ − 1 的整数应该被固定为 $2^{31}$ − 1 。
* 返回整数作为最终结果。

注意:

* 本题中的空白字符只包括空格字符 `' '` 。
* 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。

示例 1:

```
输入:s = "42"

输出:42

解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。

第 1 步:"42"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"42"(读入 "42")
           ^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。
```
示例 2:
```
输入:s = "   -42"

输出:-42

解释:
第 1 步:"   -42"(读入前导空格,但忽视掉)
            ^
第 2 步:"   -42"(读入 '-' 字符,所以结果应该是负数)
             ^
第 3 步:"   -42"(读入 "42")
               ^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。
```
示例 3:
```
输入:s = "4193 with words"
输出:4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
             ^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。
```
示例 4:
```
输入:s = "words and 987"

输出:0

解释:
第 1 步:"words and 987"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"words and 987"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"words and 987"(由于当前字符 'w' 不是一个数字,所以读入停止)
         ^
解析得到整数 0 ,因为没有读入任何数字。
由于 0 在范围 [-231, 231 - 1] 内,最终结果为 0 。
```
示例 5:
```
输入:s = "-91283472332"

输出:-2147483648

解释:
第 1 步:"-91283472332"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"-91283472332"(读入 '-' 字符,所以结果应该是负数)
          ^
第 3 步:"-91283472332"(读入 "91283472332")
                     ^
解析得到整数 -91283472332 。
由于 -91283472332 小于范围 [-231, 231 - 1] 的下界,最终结果被截断为 -231 = -2147483648 。
```

提示:
* $0 <= s.length <= 200$
* `s` 由英文字母(大写和小写)、数字(`0-9`)、`' '`、`'+'`、`'-'` 和 `'.'` 组成

Solution:
### 朴素解法

这是道模拟题。

一看这题目描述,多少有点做工程项目各种判断的意思了。

但对于这类题目,切忌一上来直接写代码,很容易会陷入各种边界判断。

这时候一个比较推荐的做法是先对要求进行提炼整理:

1. 忽略前导空格
2. 首字符只能是 正号/负号/数字,否则不合法(返回 0)
3. 继续往后匹配字符,直到结尾或不为数字为止(匹配过程中如果出现溢出,根据正负直接返回 `Integer.MAX_VALUE` 或 `Integer.MIN_VALUE`)。

当整理出来具体的逻辑之后,记得再次检查是否有遗漏掉某些边界情况。

确定逻辑之后,代码就好写了。

代码:
```Java
class Solution {
    public int myAtoi(String s) {
        int n = s.length();
        char[] chars = s.toCharArray();
        int idx = 0;

        // 去除前导空格,如果去完前导空格后无字符了,返回 0
        while (idx < n && chars[idx] == ' ') idx++;
        if (idx == n) return 0;

        // 检查第一个字符:可以为正负号/数字
        boolean isNeg = false;
        if (chars[idx] == '-') {
            idx++;
            isNeg = true;
        } else if (chars[idx] == '+') {
            idx++;
        } else if (!Character.isDigit(chars[idx])) {
            return 0;
        }

        int ans = 0;
        while (idx < n && Character.isDigit(chars[idx])) {
            int cur = chars[idx++] - '0';
            // 和上一题的“【刷穿 LeetCode】7. 整数反转”一样,防止 ans = ans * 10 + cur 溢出
            // 等价变形为 ans > (Integer.MAX_VALUE - cur) / 10 进行预判断
            if (ans > (Integer.MAX_VALUE - cur) / 10) {
                return isNeg ? Integer.MIN_VALUE : Integer.MAX_VALUE;
            }
            ans = ans * 10 + cur;
        }
        return isNeg ? -ans : ans;
    }
}
```
* 时间复杂度:会对整个字符串 `s` 扫描一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.8` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个整数 `x` ,如果 `x` 是一个回文整数,返回 `true` ;否则,返回 `false` 。

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

例如,$121$ 是回文,而 $123$ 不是。

示例 1:
```
输入:x = 121

输出:true
```
示例 2:
```
输入:x = -121

输出:false

解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
```
示例 3:
```
输入:x = 10

输出:false

解释:从右向左读, 为 01 。因此它不是一个回文数。
```
示例 4:
```
输入:x = -101

输出:false
```

提示:
* -$2^{31} <= x <= 2^{31}$ - 1

进阶:你能不将整数转为字符串来解决这个问题吗?

Solution:
### 字符串解法

虽然进阶里提到了不能用字符串来解决,但还是提供一下吧。

代码:
```Java
class Solution {
    public boolean isPalindrome(int x) {
        String s = String.valueOf(x);
        StringBuilder sb = new StringBuilder(s);
        sb.reverse();
        return sb.toString().equals(s);
    }
}
```
* 时间复杂度:数字 $n$ 的位数,数字大约有 $\log_{10}^n$ 位,翻转操作要执行循环。复杂度为 $O(\log_{10}^n)$
* 空间复杂度:使用了字符串作为存储。复杂度为 $O(\log_{10}^n)$

---
### 非字符串解法(完全翻转)

原数值 `x` 的不超过 int 的表示范围,但翻转后的值会有溢出的风险,所以这里使用 `long` 进行接收,最后对比两者是否相等。

代码:
```Java
class Solution {
    public boolean isPalindrome(int x) {
        if (x < 0) return false;
        long ans = 0;
        int t = x;
        while (x > 0) {
            ans = ans * 10 + x % 10;
            x /= 10;
        }
        return ans - t == 0;
    }
}
```
* 时间复杂度:数字 $n$ 的位数,数字大约有 $\log_{10}^n$ 位。复杂度为 $O(\log_{10}^n)$
* 空间复杂度:$O(1)$

---
### 非字符串解法(部分翻转)

如果在进阶中增加一个我们熟悉的要求:环境中只能存储得下 $32$ 位的有符号整数。

那么我们就连 `long` 也不能用了,这时候要充分利用「回文」的特性:前半部分和后半部分(翻转)相等。

这里的前半部分和后半部分(翻转)需要分情况讨论:

* 回文长度为奇数:回文中心是一个独立的数,即 `忽略回文中心后,前半部分 == 后半部分(翻转)`。如 1234321 回文串

* 回文长度为偶数:回文中心在中间两个数中间,即 `前半部分 == 后半部分(翻转)`。如 123321

代码:
```Java
class Solution {
    public boolean isPalindrome(int x) {
        // 对于 负数 和 x0、x00、x000 格式的数,直接返回 flase
        if (x < 0 || (x % 10 == 0 && x != 0)) return false;
        int t = 0;
        while (x > t) {
            t = t * 10 + x % 10;
            x /= 10;
        }
        // 回文长度的两种情况:直接比较 & 忽略中心点(t 的最后一位)进行比较
        return x == t || x == t / 10;
    }
}
```
* 时间复杂度:数字 $n$ 的位数,数字大约有 $\log_{10}^n$ 位。复杂度为 $O(\log_{10}^n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.9` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
在大小为 `n x n` 的网格 `grid` 上,每个单元格都有一盏灯,最初灯都处于 关闭 状态。

给你一个由灯的位置组成的二维数组 `lamps`,其中 $lamps[i] = [row_i, col_i]$ 表示 打开 位于 `grid[rowi][coli]` 的灯。即便同一盏灯可能在 `lamps` 中多次列出,不会影响这盏灯处于 打开 状态。

当一盏灯处于打开状态,它将会照亮 自身所在单元格 以及同一 行 、同一 列 和两条 对角线 上的 所有其他单元格 。

另给你一个二维数组 `queries`,其中 $queries[j] = [row_j, col_j]$ 。对于第 $j$ 个查询,如果单元格 $[row_j, col_j]$ 是被照亮的,则查询结果为 $1$ ,否则为 $0$ 。

在第 `j` 次查询之后 [按照查询的顺序] ,关闭 位于单元格 $grid[row_j][col_j]$ 上及相邻 $8$ 个方向上(与单元格 $grid[row_i][col_i]$ 共享角或边)的任何灯。

返回一个整数数组 `ans` 作为答案, `ans[j]` 应等于第 `j` 次查询 `queries[j]` 的结果,$1$ 表示照亮,$0$ 表示未照亮。

示例 1:

```
输入:n = 5, lamps = [[0,0],[4,4]], queries = [[1,1],[1,0]]

输出:[1,0]
```

```
解释:最初所有灯都是关闭的。在执行查询之前,打开位于 [0, 0] 和 [4, 4] 的灯。第 0 次查询检查 grid[1][1] 是否被照亮(蓝色方框)。该单元格被照亮,所以 ans[0] = 1 。然后,关闭红色方框中的所有灯。
```

```
第 1 次查询检查 grid[1][0] 是否被照亮(蓝色方框)。该单元格没有被照亮,所以 ans[1] = 0 。然后,关闭红色矩形中的所有灯。
```
示例 2:
```
输入:n = 5, lamps = [[0,0],[4,4]], queries = [[1,1],[1,1]]

输出:[1,1]
```
示例 3:
```
输入:n = 5, lamps = [[0,0],[0,4]], queries = [[0,4],[0,1],[1,4]]

输出:[1,1,0]
```

提示:
* $1 <= n <= 10^9$
* $0 <= lamps.length <= 20000$
* $0 <= queries.length <= 20000$
* $lamps[i].length == 2$
* $0 <= row_i, col_i < n$
* $queries[j].length == 2$
* $0 <= row_j, col_j < n$

Solution:
### 哈希表 + 线映射

棋盘大小的数据范围为 $n = 10^9$,硬模拟「亮灯」的操作必然会 `TLE`,而 `lamps` 和 `queries` 数据范围为 $20000$ 是一个较为常见的突破口。

由于点亮每一盏灯,可以使得当前 **行、列 和 对角线** 的位置被照亮,行列可直接使用棋盘坐标的 $(x, y)$ 来代指,而对角线则可以使用「截距」来进行代指,即使用 $x + y$ 和 $x - y$ 进行代指。

分别使用四个「哈希表」`row`、`col`、`left` 和 `right` 来记录 **行、列 和 对角线** 的点亮情况(`key` 为线编号,`value` 为点亮次数)。

这样我们可以在 $O(1)$ 的复杂度处理掉所有的 $lamps[i]$,某个位置被照亮的充要条件为:**「当前位置所在行被点亮」或「当前位置所在列被点亮」或「当前位置所处的对角线被点亮」。**

同时,由于每次查询后要进行「灭灯」操作(注意:灭灯只能灭有灯的位置,而不是灭有光的位置 🤣),因此我们还需要另外记录每个灯的位置,可以使用利用「二维转一维」的技巧进行编号:$idx = x * n + y$,并使用 `HashSet` 进行记录(忽略重复的 $lamps[i]$)。

由于询问次数最多为 $20000$,因此直接在查询完成后模拟「灭灯」即可(访问自身及相邻格子,共 $9$ 个),计算量为 $2 * 10^5$ 以内,可以接受。若某个位置存在灯,将其从 `HashSet` 中移除,并更新对应线的点亮情况。

代码:
```Java
class Solution {
    int[][] dirs = new int[][]{{0,0},{0,-1},{0,1},{-1,0},{-1,-1},{-1,1},{1,0},{1,-1},{1,1}};
    public int[] gridIllumination(int n, int[][] lamps, int[][] queries) {
        long N = n;
        Map<Integer, Integer> row = new HashMap<>(), col = new HashMap<>();
        Map<Integer, Integer> left = new HashMap<>(), right = new HashMap<>();
        Set<Long> set = new HashSet<>();
        for (int[] l : lamps) {
            int x = l[0], y = l[1];
            int a = x + y, b = x - y;
            if (set.contains(x * N + y)) continue;
            increment(row, x); increment(col, y);
            increment(left, a); increment(right, b);
            set.add(x * N + y);
        }
        int m = queries.length;
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            int[] q = queries[i];
            int x = q[0], y = q[1];
            int a = x + y, b = x - y;
            if (row.containsKey(x) || col.containsKey(y) || left.containsKey(a) || right.containsKey(b)) ans[i] = 1;

            for (int[] d : dirs) {
                int nx = x + d[0], ny = y + d[1];
                int na = nx + ny, nb = nx - ny;
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                if (set.contains(nx * N + ny)) {
                    set.remove(nx * N + ny);
                    decrement(row, nx); decrement(col, ny);
                    decrement(left, na); decrement(right, nb);
                }
            }
        }
        return ans;
    }
    void increment(Map<Integer, Integer> map, int key) {
        map.put(key, map.getOrDefault(key, 0) + 1);
    }
    void decrement(Map<Integer, Integer> map, int key) {
        if (map.get(key) == 1) map.remove(key);
        else map.put(key, map.get(key) - 1);
    }
}
```
* 时间复杂度:令 $lamps$ 长度为 $a$,$queries$ 长度为 $b$,处理所有的 $lamp[i]$ 复杂度为 $O(a)$,处理所有的 $queries[j]$ 复杂度为 $O(C * b)$,其中 $C$ 为模拟灭灯时所联通的格子数量,固定为 $9$。整体复杂度为 $O(a + C * b)$
* 空间复杂度:$O(a)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1001` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给定一个由若干 $0$ 和 $1$ 组成的数组 `A`,我们最多可以将 $K$ 个值从 $0$ 变成 $1$ 。

返回仅包含 $1$ 的最长(连续)子数组的长度。

示例 1:
```
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
```
示例 2:
```
输入:A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。
```

提示:
* $1 <= A.length <= 20000$
* $0 <= K <= A.length$
* $A[i]$ 为 $0$ 或 $1$

Solution:
### 动态规划(TLE)

看到本题,其实首先想到的是 DP,但是 DP 是 $O(nk)$ 算法。

看到了数据范围是 $10^4$,那么时空复杂度应该都是 $10^8$。

空间可以通过「滚动数组」优化到 $10^4$,但时间无法优化,会超时。

**PS. 什么时候我们会用 DP 来解本题?通过如果 K 的数量级不超过 1000 的话,DP 应该是最常规的做法。**

定义 $f[i,j]$ 代表考虑前 $i$ 个数(并以 $A[i]$ 为结尾的),最大翻转次数为 $j$ 时,连续 $1$ 的最大长度。

* 如果 $A[i]$ 本身就为 1 的话,无须消耗翻转次数,$f[i][j] = f[i - 1][j] + 1$。
* 如果 $A[i]$ 本身不为 1 的话,由于定义是必须以 $A[i]$ 为结尾,因此必须要选择翻转该位置,$f[i][j] = f[i - 1][j - 1] + 1$。

代码:
```Java
class Solution {
    public int longestOnes(int[] nums, int k) {
        int n = nums.length;
        int[][] f = new int[2][k + 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= k; j++) {
                if (nums[i - 1] == 1) {
                    f[i & 1][j] = f[(i - 1) & 1][j] + 1;
                } else {
                    f[i & 1][j] = j == 0 ? 0 : f[(i - 1) & 1][j - 1] + 1;
                }
                ans = Math.max(ans, f[i & 1][j]);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(nk)$
* 空间复杂度:$O(k)$

---

### 前缀和 + 二分

从数据范围上分析,**平方级别**的算法过不了,往下优化就应该是**对数级别**的算法。

因此,很容易我们就会想到「二分」。

当然还需要我们对问题做一下等价变形。

**最大替换次数不超过 `k` 次,可以将问题转换为找出连续一段区间 `[l,r]`,使得区间中出现 0 的次数不超过 `k` 次。**

我们可以枚举区间 左端点/右端点 ,然后找到其满足「出现 0 的次数不超过 `k` 次」的最远右端点/最远左端点。

为了快速判断 `[l,r]` 之间出现 0 的个数,我们需要用到前缀和。

假设 `[l,r]` 的区间长度为 `len`,区间和为 `tot`,那么出现 0 的格式为 `len - tol`,再与 `k` 进行比较。

由于数组中不会出现负权值,因此前缀和数组具有「单调性」,那么必然满足「其中一段满足 $len - tol <= k$,另外一段不满足 $len - tol <= k$」。

**因此,对于某个确定的「左端点/右端点」而言,以「其最远右端点/最远左端点」为分割点的前缀和数轴,具有「二段性」。可以通过二分来找分割点。**

代码:
```Java
class Solution {
    public int longestOnes(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        int[] sum = new int[n + 1];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        for (int i = 0; i < n; i++) {
            int l = 0, r = i;
            while (l < r) {
                int mid = l + r >> 1;
                if (check(sum, mid, i, k)) {
                    r = mid;
                } else {
                    l = mid + 1;
                }
            }
            if (check(sum, r, i, k)) ans = Math.max(ans, i - r + 1);
        }
        return ans;
    }
    boolean check(int[] sum, int l, int r, int k) {
        int tol = sum[r + 1] - sum[l], len = r - l + 1;
        return len - tol <= k;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

**关于二分结束后再次 `check` 的说明:由于「二分」本质是找满足某个性质的分割点,通常我们的某个性质会是「非等值条件」,不一定会取得 `=`。**

**例如我们很熟悉的:从某个非递减数组中找目标值,找到返回下标,否则返回 -1。**

**当目标值不存在,「二分」找到的应该是数组内比目标值小或比目标值大的最接近的数。因此二分结束后先进行 `check` 再使用是一个好习惯。**

---

### 双指针

由于我们总是比较 `len`、`tot` 和 `k` 三者的关系。

因此我们可以使用「滑动窗口」的思路,动态维护一个左右区间 `[j, i]` 和维护窗口内和 `tot`。

右端点一直右移,左端点在窗口不满足「`len - tol <= k`」的时候进行右移,即可做到线程扫描的复杂度。

代码:
```Java
class Solution {
    public int longestOnes(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0, j = 0, tot = 0; i < n; i++) {
            tot += nums[i];
            while ((i - j + 1) - tot > k) tot -= nums[j++];
            ans = Math.max(ans, i - j + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 总结

**除了掌握本题解法以外,我还希望你能理解这几种解法是如何被想到的(特别是如何从「动态规划」想到「二分」)。**

**根据数据范围(复杂度)调整自己所使用的算法的分析能力,比解决该题本身更加重要。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1004` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/1001-1010/1005. K 次取反后最大化的数组和（简单）.md

1005. K 次取反后最大化的数组和

Question:
给你一个整数数组 `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] 。
```

提示:
* $1 <= nums.length <= 10^4$
* $-100 <= nums[i] <= 100$
* $1 <= k <= 10^4$

Solution:
### 贪心 + 分情况讨论 + 模拟

假设取反前的总和为 $sum$,取反一个任意值 $x$ 后,对 $sum$ 的影响为 $- 2 * x$。

即取反一个负数会使得结果变大,取反正数会使结果变小,取反 $0$ 值对结果没有影响。

因此,为了让取反后的结果尽可能的大,我们应当取反 $-2*x$ 尽可能大的数值。即按照「负数从小到大的顺序进行取反」。

对取反次数 $k$ 和 负数个数 $cnt$ 进行分情况讨论:

* $k <= cnt$:按照负数从小到大的顺序进行取反即可;
* $k > cnt$:按照负数从小到大的顺序进行取反后,根据「是否存在 $0$ 值」和「剩余取反次数的奇偶性」进行分情况讨论:
    * 存在 $0$ 值 或 剩余取反次数为偶数:直接返回当前取反数组的总和( $0$ 值可抵消任意次数的取反操作,将偶数次的取反操作应用在同一数值上,结果不变);
    * 不存在 $0$ 值且剩余取反次数为奇数:此时从当前数值中取一个绝对值最小值(使用 $idx$ 记录该值下标)进行取反,得到最终的取反数组。

最后对取反数组进行求和操作即可。

代码:
```Java
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        int n = nums.length, idx = 0;
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->nums[a]-nums[b]);
        boolean zero = false;
        for (int i = 0; i < n; i++) {
            if (nums[i] < 0) q.add(i);
            if (nums[i] == 0) zero = true;
            if (Math.abs(nums[i]) < Math.abs(nums[idx])) idx = i;
        }
        if (k <= q.size()) {
            while (k-- > 0) nums[q.peek()] = -nums[q.poll()];
        } else {
            while (!q.isEmpty() && k-- > 0) nums[q.peek()] = -nums[q.poll()];
            if (!zero && k % 2 != 0) nums[idx] = -nums[idx];
        }
        int ans = 0;
        for (int i : nums) ans += i;
        return ans;
    }
}
```
* 时间复杂度:对 $nums$ 进行遍历,得到 $idx$ 以及优先队列的复杂度为 $O(n\log{n})$;从优先队列中取出元素进行取反操作的复杂度为 $O(k\log{n})$;对取反数组进行求和复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 优化优先队列

由于 $nums$ 长度范围为 $10000$,但值域范围在 $[-100, 100]$,因此我们可以使用计数数组 $cnt$ 来替代优先队列的作用。

注意由于 $nums[i]$ 存在负数,因此需要增 $100$ 的偏移量。同时对于翻转操作,仅需要修改相应计数即可。

代码:
```Java
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        int[] cnts = new int[210];
        for (int i : nums) cnts[i + 100]++;
        for (int i = -100; i < 0 && k > 0; i++) {
            while (cnts[i + 100] != 0 && k-- > 0) {
                cnts[i + 100]--; cnts[-i + 100]++;
            }
        }
        if (cnts[0 + 100] == 0 && k > 0 && k % 2 != 0) {
            int val = 1;
            while (cnts[val + 100] == 0) val++;
            cnts[val + 100]--; cnts[-val + 100]++;
        }
        int ans = 0;
        for (int i = -100; i <= 100; i++) ans += i * cnts[i + 100];
        return ans;
    }
}
```
* 时间复杂度:需要对 $nums$ 以及大小为 $C = 210$ 的计数数组进行常数次扫描,复杂度为 $O(n + C)$
* 空间复杂度:$O(C)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1005` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
通常,正整数 n 的阶乘是所有小于或等于 n 的正整数的乘积。

例如,factorial(10) = 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1。

相反,我们设计了一个笨阶乘 clumsy:在整数的递减序列中,我们以一个固定顺序的操作符序列来依次替换原有的乘法操作符:乘法(*),除法(/),加法(+)和减法(-)。

例如,clumsy(10) = 10 * 9 / 8 + 7 - 6 * 5 / 4 + 3 - 2 * 1。然而,这些运算仍然使用通常的算术运算顺序:我们在任何加、减步骤之前执行所有的乘法和除法步骤,并且按从左到右处理乘法和除法步骤。

另外,我们使用的除法是地板除法(floor division),所以 10 * 9 / 8 等于 11。这保证结果是一个整数。

实现上面定义的笨函数:给定一个整数 N,它返回 N 的笨阶乘。

示例 1:
```
输入:4

输出:7

解释:7 = 4 * 3 / 2 + 1
```
示例 2:
```
输入:10

输出:12

解释:12 = 10 * 9 / 8 + 7 - 6 * 5 / 4 + 3 - 2 * 1
```

提示:

* 1 <= N <= 10000
* -$2^{31}$ <= answer <= $2^{31}$ - 1  (答案保证符合 32 位整数)

Solution:
### 通用表达式解法

第一种解法是我们的老朋友解法了,使用「双栈」来解决通用表达式问题。

**事实上,我提供这套解决方案不仅仅能解决只有 `+ - ( )`([224. 基本计算器](https://leetcode-cn.com/problems/basic-calculator/)) 或者 `+ - * /`([227. 基本计算器 II](https://leetcode-cn.com/problems/basic-calculator-ii/solution/)) 的表达式问题,还能能解决 `+ - * / ^ % ( )` 的完全表达式问题。**

甚至支持自定义运算符,只要在运算优先级上进行维护即可。

**对于「表达式计算」这一类问题,你都可以使用这套思路进行解决。我十分建议你加强理解这套处理逻辑。**

对于「任何表达式」而言,我们都使用两个栈 `nums` 和 `ops`:
* `nums` : 存放所有的数字
* `ops` :存放所有的数字以外的操作

然后从前往后做,对遍历到的字符做分情况讨论:
* 空格 : 跳过
* `(` : 直接加入 `ops` 中,等待与之匹配的 `)`
* `)` : 使用现有的 `nums` 和 `ops` 进行计算,直到遇到左边最近的一个左括号为止,计算结果放到 `nums`
* 数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入 `nums`
* `+ - * / ^ %` : 需要将操作放入 `ops` 中。**在放入之前先把栈内可以算的都算掉(只有「栈内运算符」比「当前运算符」优先级高/同等,才进行运算)**,使用现有的 `nums` 和 `ops` 进行计算,直到没有操作或者遇到左括号,计算结果放到 `nums`

我们可以通过 🌰 来理解 **只有「栈内运算符」比「当前运算符」优先级高/同等,才进行运算** 是什么意思:

因为我们是从前往后做的,假设我们当前已经扫描到 `2 + 1` 了(此时栈内的操作为 `+` )。

1. 如果后面出现的 `+ 2` 或者 `- 1` 的话,满足「栈内运算符」比「当前运算符」优先级高/同等,可以将 `2 + 1` 算掉,把结果放到 `nums` 中;
2. 如果后面出现的是 `* 2` 或者 `/ 1` 的话,不满足「栈内运算符」比「当前运算符」优先级高/同等,这时候不能计算 `2 + 1`。

更为详细的讲解可以看这篇题解 :[使用「双栈」解决「究极表达式计算」问题](https://leetcode-cn.com/problems/basic-calculator-ii/solution/shi-yong-shuang-zhan-jie-jue-jiu-ji-biao-c65k/)

代码:

```Java
class Solution {
    public int clumsy(int n) {
        Deque<Integer> nums = new ArrayDeque<>();
        Deque<Character> ops = new ArrayDeque<>();
        // 维护运算符优先级
        Map<Character, Integer> map = new HashMap<>(){{
            put('*', 2);
            put('/', 2);
            put('+', 1);
            put('-', 1);
        }};
        char[] cs = new char[]{'*', '/', '+', '-'};
        for (int i = n, j = 0; i > 0; i--, j++) {
            char op = cs[j % 4];
            nums.addLast(i);
            // 如果「当前运算符优先级」不高于「栈顶运算符优先级」,说明栈内的可以算
            while (!ops.isEmpty() && map.get(ops.peekLast()) >= map.get(op)) {
                calc(nums, ops);
            }
            if (i != 1) ops.add(op);
        }
        // 如果栈内还有元素没有算完,继续算
        while (!ops.isEmpty()) calc(nums, ops);
        return nums.peekLast();
    }
    void calc(Deque<Integer> nums, Deque<Character> ops) {
        int b = nums.pollLast(), a = nums.pollLast();
        int op = ops.pollLast();
        int ans = 0;
        if (op == '+') ans = a + b;
        else if (op == '-') ans = a - b;
        else if (op == '*') ans = a * b;
        else if (op == '/') ans = a / b;
        nums.addLast(ans);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 数学解法(打表技巧分析)

这次在讲【证明】之前,顺便给大家讲讲找规律的题目该怎么做。

由于是按照特定顺序替换运算符,因此应该是有一些特性可以被我们利用的。

通常我们需要先实现一个**可打表的算法(例如上述的解法一,这是为什么掌握「通用表达式」解法具有重要意义)**,将连续数字的答案打印输出,来找找规律:

```Java
    Solution solution = new Solution();
    for (int i = 1; i <= 10000; i++) {
        int res = solution.clumsy(i);
        System.out.println(i + " : " + res);
    }
```

似乎 $n$ 与 答案比较接近,我们考虑将两者的差值输出:

```Java
    Solution solution = new Solution();
    for (int i = 1; i <= 10000; i++) {
        int res = solution.clumsy(i);
        System.out.println(i + " : " + res + " : " + (res - i));
    }
```

咦,好像发现了什么不得了的东西。似乎每四个数,差值都是  [1, 2, 2, -1]

再修改我们的打表逻辑,来验证一下(只输出与我们猜想不一样的数字):

```Java
    Solution solution = new Solution();
    int[] diff = new int[]{1,2,2,-1};
    for (int i = 1; i <= 10000; i++) {
        int res = solution.clumsy(i);
        int t = res - i;
        if (t != diff[i % 4]) {
            System.out.println(i + " : " + res);
        }
    }
```

只有前四个数字被输出,其他数字都是符合我们的猜想规律的。

**到这里我们已经知道代码怎么写可以 AC 了,十分简单。**

代码:

```java
class Solution {
    public int clumsy(int n) {
        int[] special = new int[]{1,2,6,7};
        int[] diff = new int[]{1,2,2,-1};
        if (n <= 4) return special[(n - 1) % 4];
        return n + diff[n % 4];
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

***

### 证明

讲完我们的【实战技巧】之后,再讲讲如何证明。

**上述的做法比较适合于笔试或者比赛,但是面试,通常还需要证明做法为什么是正确的。**

我们不失一般性的分析某个 `n`,当然这个 `n` 必须是大于 4,不属于我们的特判值。

然后对 `n` 进行讨论(**根据我们的打表猜想去证明规律是否可推广**):

1. `n % 4 == 0` : $f(n) = n * (n - 1) / (n - 2) + ... + 5 - 4 * 3 / 2 + 1 = n + 1$,即 `diff = 1`

2. `n % 4 == 1` : $f(n) = n * (n - 1) / (n - 2) + ... + 6 - 5 * 4 / 3 + 2 - 1 = n + 2$,即 `diff = 2`

3. `n % 4 == 2` : $f(n) = n * (n - 1) / (n - 2) + ... + 7 - 6 * 5 / 4 + 3 - 2 * 1 = n + 2$,即 `diff = 2`

4. `n % 4 == 3` : $f(n) = n * (n - 1) / (n - 2) + ... + 8 - 7 * 6 / 5 + 4 - 3 * 2 / 1 = n - 1$,即 `diff = -1`

**上述的表达式展开过程属于小学数学内容,省略号部分的项式的和为 0,因此你只需要关注我写出来的那部分。**

**至此,我们证明了我们的打表猜想具有「可推广」的特性。**

**甚至我们应该学到:证明可以是基于猜想去证明,而不必从零开始进行推导。**

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1006` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/101-110/108. 将有序数组转换为二叉搜索树（简单）.md

108. 将有序数组转换为二叉搜索树

Question:
给你一个整数数组 `nums`,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 $1$ 」的二叉树。

示例 1:

```
输入:nums = [-10,-3,0,5,9]

输出:[0,-3,9,-10,null,5]

解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
```
示例 2:

```
输入:nums = [1,3]

输出:[3,1]

解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
```

提示:
* $1 <= nums.length <= 10^4$
* $-10^4 <= nums[i] <= 10^4$
* `nums` 按严格递增顺序排列

Solution:
### 递归分治

题目给定的 `nums` 严格有序,为满足构造出来的 `BST` 整体平衡,我们需要保证每个子树的构造也是平衡的。

一个容易想到的思路:使用 `nums` 中最靠近中心的位置作为整棵 `BST` 的根节点,例如下标 $mid = \left \lfloor \frac{l + r}{2} \right \rfloor$ 的位置,确保左右子树节点数量平衡。随后递归构造 `nums` 中下标范围为 $[0, mid - 1]$ 作为左子树,递归构造 `nums` 中下标范围为 $[mid + 1, n - 1]$ 作为右子树。

Java 代码:

```Java
class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        return build(nums, 0, nums.length - 1);
    }
    TreeNode build(int[] nums, int l, int r) {
        if (l > r) return null;
        int mid = l + r >> 1;
        TreeNode ans = new TreeNode(nums[mid]);
        ans.left = build(nums, l, mid - 1);
        ans.right = build(nums, mid + 1, r);
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        return self.build(nums, 0, len(nums) - 1)

    def build(self, nums, l, r):
        if l > r:
            return None
        mid = l + r >> 1
        ans = TreeNode(nums[mid])
        ans.left = self.build(nums, l, mid - 1)
        ans.right = self.build(nums, mid + 1, r)
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        return build(nums, 0, nums.size() - 1);
    }
    TreeNode* build(vector<int>& nums, int l, int r) {
        if (l > r) return nullptr;
        int mid = l + r >> 1;
        TreeNode* ans = new TreeNode(nums[mid]);
        ans->left = build(nums, l, mid - 1);
        ans->right = build(nums, mid + 1, r);
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function sortedArrayToBST(nums: number[]): TreeNode | null {
    const build = function (nums: number[], l: number, r: number): TreeNode | null {
        if (l > r) return null;
        const mid = l + r >> 1;
        const ans = new TreeNode(nums[mid]);
        ans.left = build(nums, l, mid - 1);
        ans.right = build(nums, mid + 1, r);
        return ans;
    }
    return build(nums, 0, nums.length - 1);
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.108` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/101-110/109. 有序链表转换二叉搜索树（中等）.md

109. 有序链表转换二叉搜索树

Question:
给定一个单链表的头节点  `head`,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树。

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 $1$。

示例 1:

```
输入: head = [-10,-3,0,5,9]

输出: [0,-3,9,-10,null,5]

解释: 一个可能的答案是[0,-3,9,-10,null,5],它表示所示的高度平衡的二叉搜索树。
```
示例 2:
```
输入: head = []

输出: []
```

提示:
* `head` 中的节点数在 $[0, 2 \times 10^4]$ 范围内
* $-10^5 <= Node.val <= 10^5$

Solution:
### 递归分治

与上一题 [108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/solutions/2436639/gong-shui-san-xie-jian-dan-di-gui-fen-zh-nzqx/) 类似,但链表相对于数组,无法 $O(1)$ 找到构建当前 `BST` 根节点的“中点”下标。

一个仍可确保 $O(n)$ 时间复杂度(瓶颈在于需要创建 $n$ 个节点),但需要 $O(n)$ 空间复杂度的做法是:对链表进行一次遍历,转成数组后套用上一题的做法。

一个不使用 $O(n)$ 空间复杂度的做法,需要每次遍历来找“中点”下标:起始我们先对 `head` 进行一次遍历,得到链表长度 $n$,随后仍然利用递归分治的思路进行构造,每次对入参的左右端点找“中点”,先通过直接结算的方式定位到偏移量 $t = l + \left \lfloor \frac{l + r}{2} \right \rfloor$,然后再通过从入参节点 `head` 出发往前走 $t$ 的做法找到“中点”节点。

该做法每个节点的访问次数为在递归过程中被 $[l, r]$ 所覆盖的次数,我们知道一个节点数量为 $n$ 的平衡 `BST`  树高为 $\log{n}$,因此整体复杂度为 $O(n\log{n})$。

Java 代码:
```Java
class Solution {
    public TreeNode sortedListToBST(ListNode head) {
        int n = 0;
        ListNode cur = head;
        while (cur != null && ++n >= 0) cur = cur.next;
        return build(head, 0, n - 1);
    }
    TreeNode build(ListNode head, int l, int r) {
        if (l > r) return null;
        int mid = l + r >> 1, t = mid - l;
        ListNode cur = head;
        while (t-- > 0) cur = cur.next;
        TreeNode ans = new TreeNode(cur.val);
        ans.left = build(head, l, mid - 1);
        ans.right = build(cur.next, mid + 1, r);
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
        n = 0
        cur = head
        while cur:
            n += 1
            cur = cur.next
        return self.build(head, 0, n - 1)

    def build(self, head: ListNode, l: int, r: int) -> TreeNode:
        if l > r:
            return None
        mid = l + r >> 1
        t = mid - l
        cur = head
        while t > 0:
            cur = cur.next
            t -= 1
        ans = TreeNode(cur.val)
        ans.left = self.build(head, l, mid - 1)
        ans.right = self.build(cur.next, mid + 1, r)
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    TreeNode* sortedListToBST(ListNode* head) {
        int n = 0;
        ListNode* cur = head;
        while (cur && ++n >= 0) cur = cur->next;
        return build(head, 0, n - 1);
    }
    TreeNode* build(ListNode* head, int l, int r) {
        if (l > r) return nullptr;
        int mid = l + r >> 1, t = mid - l;
        ListNode* cur = head;
        while (t-- > 0) cur = cur->next;
        TreeNode* ans = new TreeNode(cur->val);
        ans->left = build(head, l, mid - 1);
        ans->right = build(cur->next, mid + 1, r);
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function sortedListToBST(head: ListNode | null): TreeNode | null {
    const build = function (head: ListNode | null, l: number, r: number): TreeNode | null {
        if (l > r) return null;
        let mid = l + r >> 1, t = mid - l;
        let cur = head;
        while (t-- > 0) cur = cur.next;
        const ans = new TreeNode(cur!.val);
        ans.left = build(head, l, mid - 1);
        ans.right = build(cur!.next, mid + 1, r);
        return ans;
    }
    let n = 0;
    let cur = head;
    while (cur != null && ++n >= 0) cur = cur.next;
    return build(head, 0, n - 1);
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 递归分治 - 中序遍历

由于给定的 `nums` 本身严格有序,而 `BST`  的中序遍历亦是有序。因此我们可以一边遍历链表,一边对 `BST`  进行构造。

具体的,我们仍然先对链表进行遍历,拿到链表长度 `n`。递归构造过程中传入左右端点 `l` 和 `r`,含义为使用链表中 $[l, r]$ 部分节点,但不再在每次递归中传入当前头结点,而是使用全局变量 `head` 来记录。

递归构造过程中,计算“中点”位置 $mid  = \left \lfloor \frac{l + r}{2} \right \rfloor$,并根据如下流程进行构造:

1. 使用 $[l, mid - 1]$ 构建左子树,使用变量 `left` 保存当前左子树的根节点
2. 构建完左子树后,全局变量 `head` 必然来到了“中点”位置,用其构建根节点 `ans`,并将根节点与此前构造的 `left` 关联。同时让链表节点 `head` 后移
3. 使用 $[mid + 1, r]$ 构建右子树,并将其挂载到根节点 `ans` 中

如此一来,即可确保「链表遍历」和「`BST`  构造」的同步性。

Java 代码:
```Java
class Solution {
    ListNode head;
    public TreeNode sortedListToBST(ListNode _head) {
        head = _head;
        int n = 0;
        ListNode cur = head;
        while (cur != null && ++n >= 0) cur = cur.next;
        return build(0, n - 1);
    }
    TreeNode build(int l, int r) {
        if (l > r) return null;
        int mid = l + r >> 1;
        TreeNode left = build(l, mid - 1);
        TreeNode ans = new TreeNode(head.val);
        head = head.next;
        ans.left = left;
        ans.right = build(mid + 1, r);
        return ans;
    }
}
```
Python 代码:
```Python
class Solution:
    def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
        self.head = head
        n = 0
        cur = self.head
        while cur is not None:
            n += 1
            cur = cur.next
        return self.build(0, n - 1)

    def build(self, l: int, r: int) -> TreeNode:
        if l > r:
            return None
        mid = l + r >> 1
        left = self.build(l, mid - 1)
        ans = TreeNode(self.head.val)
        ans.left = left
        self.head = self.head.next
        ans.right = self.build(mid + 1, r)
        return ans
```
C++ 代码:
```C++
class Solution {
public:
    ListNode* head;
    TreeNode* sortedListToBST(ListNode* _head) {
        head = _head;
        int n = 0;
        ListNode* cur = head;
        while (cur && n++ >= 0) cur = cur->next;
        return build(0, n - 1);
    }
    TreeNode* build(int l, int r) {
        if (l > r) return nullptr;
        int mid = l + r >> 1;
        TreeNode* left = build(l, mid - 1);
        TreeNode* ans = new TreeNode(head->val);
        ans->left = left;
        head = head->next;
        ans->right = build(mid + 1, r);
        return ans;
    }
};
```
TypeScript 代码:
```TypeScript
function sortedListToBST(_head: ListNode | null): TreeNode | null {
    const build =function(l: number, r: number): TreeNode | null {
        if (l > r) return null;
        const mid = l + r >> 1;
        const left = build(l, mid - 1);
        const ans = new TreeNode(head.val);
        ans.left = left;
        head = head.next;
        ans.right = build(mid + 1, r);
        return ans;
    }
    let head = _head;
    let n = 0, cur = head;
    while (cur && n++ >= 0) cur = cur.next;
    return build(0, n - 1);
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(\log{n})$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.109` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/1011-1020/1011. 在 D 天内送达包裹的能力（中等）.md

1011. 在 D 天内送达包裹的能力

Question:
传送带上的包裹必须在 `D` 天内从一个港口运送到另一个港口。

传送带上的第 `i` 个包裹的重量为 $weights[i]$。

每一天,我们都会按给出重量的顺序往传送带上装载包裹。

我们装载的重量不会超过船的最大运载重量。

返回能在 `D` 天内将传送带上的所有包裹送达的船的最低运载能力。

示例 1:
```
输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5

输出:15

解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
```
示例 2:
```
输入:weights = [3,2,2,4,1,4], D = 3

输出:6

解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
```
示例 3:
```
输入:weights = [1,2,3,1,1], D = 4

输出:3

解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1
```

提示:
* $1 <= D <= weights.length <= 5 \times 10^4$
* $1 <= weights[i] <= 500$

Solution:
### 二分解法(精确边界)

假定「`D` 天内运送完所有包裹的最低运力」为 `ans`,那么在以 `ans` 为分割点的数轴上具有「二段性」:
* 数值范围在 $(-\infty, ans)$ 的运力必然「不满足」 `D` 天内运送完所有包裹的要求
* 数值范围在 $[ans, +\infty)$ 的运力必然「满足」 `D`天内运送完所有包裹的要求

**我们可以通过「二分」来找到恰好满足 `D`天内运送完所有包裹的分割点 `ans`。**

接下来我们要确定二分的范围,由于不存在包裹拆分的情况,考虑如下两种边界情况:
* 理论最低运力:只确保所有包裹能够被运送,自然也包括重量最大的包裹,此时理论最低运力为 `max`,`max` 为数组 `weights` 中的最大值
* 理论最高运力:使得所有包裹在最短时间(一天)内运送完成,此时理论最高运力为 `sum`,`sum` 为数组 `weights` 的总和

**由此,我们可以确定二分的范围为 $[max, sum]$。**

Java 代码:
```Java
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int max = 0, sum = 0;
        for (int w : weights) {
            max = Math.max(max, w);
            sum += w;
        }
        int l = max, r = sum;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(weights, mid, days)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int[] weights, int t, int days) {
        int n = weights.length, cnt = 1;
        for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int shipWithinDays(vector<int>& weights, int days) {
        int maxv = 0, sum = 0;
        for (int w : weights) {
            maxv = max(maxv, w);
            sum += w;
        }
        int l = maxv, r = sum;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(weights, mid, days)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    bool check(vector<int>& weights, int t, int days) {
        int n = weights.size(), cnt = 1;
        for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
};
```
Python 代码:
```Python
class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        def check(weights: List[int], t: int, days: int) -> bool:
            n, cnt = len(weights), 1
            i, sumv = 1, weights[0]
            while i < n:
                while i < n and sumv + weights[i] <= t:
                    sumv += weights[i]
                    i += 1
                cnt += 1
                sumv = 0
            return cnt - 1 <= days

        maxv, sumv = max(weights), sum(weights)
        l, r = maxv, sumv
        while l < r:
            mid = l + r >> 1
            if check(weights, mid, days):
                r = mid
            else:
                l = mid + 1
        return r
```
TypeScript 代码:
```TypeScript
function shipWithinDays(weights: number[], days: number): number {
    const check = function(weights: number[], t: number, days: number): boolean {
        let n = weights.length, cnt = 1;
        for (let i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
    let maxv = 0, sumv = 0;
    for (const w of weights) {
        maxv = Math.max(maxv, w);
        sumv += w;
    }
    let l = maxv, r = sumv;
    while (l < r) {
        const mid = l + r >> 1;
        if (check(weights, mid, days)) r = mid;
        else l = mid + 1;
    }
    return r;
};
```
* 时间复杂度:二分范围为 $[max, sum]$,`check` 函数的复杂度为 $O(n)$。整体复杂度为 $O(n\log({\sum_{i= 0}^{n - 1}ws[i]}))$
* 空间复杂度:$O(1)$

---

### 二分解法(粗略边界)

当然,一个合格的「二分范围」只需要确保包含分割点 `ans` 即可。因此我们可以利用数据范围来确立粗略的二分范围(从而少写一些代码):

* 利用运力必然是正整数,从而确定左边界为 $1$
* 根据 $1 \leqslant D \leqslant weights.length \leqslant 50000$ 和 $1 \leqslant weights[i] \leqslant 500$,从而确定右边界为 $1e8$

*PS. 由于二分查找具有折半效率,因此「确立粗略二分范围」不会比「通过循环取得精确二分范围」效率低。*

Java 代码:
```Java
class Solution {
    public int shipWithinDays(int[] weights, int days) {
        int l = 1, r = (int)1e8;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(weights, mid, days)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    boolean check(int[] weights, int t, int days) {
        if (weights[0] > t) return false;
        int n = weights.length, cnt = 1;
        for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            if (weights[i] > t) return false;
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int shipWithinDays(vector<int>& weights, int days) {
        int l = 1, r = 1e8;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(weights, mid, days)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    bool check(vector<int>& weights, int t, int days) {
        if (weights[0] > t) return false;
        int n = weights.size(), cnt = 1;
        for (int i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            if (weights[i] > t) return false;
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
};
```
Python 代码:
```Python
class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        def check(weights: List[int], t: int, days: int) -> bool:
            if weights[0] > t: return False
            n, cnt = len(weights), 1
            i, sumv = 1, weights[0]
            while i < n:
                if weights[i] > t: return False
                while i < n and sumv + weights[i] <= t:
                    sumv += weights[i]
                    i += 1
                cnt += 1
                sumv = 0
            return cnt - 1 <= days

        l, r = 1, 10**8
        while l < r:
            mid = l + r >> 1
            if check(weights, mid, days):
                r = mid
            else:
                l = mid + 1
        return r
```
TypeScript 代码:
```TypeScript
function shipWithinDays(weights: number[], days: number): number {
    const check = function(weights: number[], t: number, days: number): boolean {
        if (weights[0] > t) return false;
        let n = weights.length, cnt = 1;
        for (let i = 1, sum = weights[0]; i < n; sum = 0, cnt++) {
            if (weights[i] > t) return false;
            while (i < n && sum + weights[i] <= t) sum += weights[i++];
        }
        return cnt - 1 <= days;
    }
    let l = 0, r = 1e8;
    while (l < r) {
        const mid = l + r >> 1;
        if (check(weights, mid, days)) r = mid;
        else l = mid + 1;
    }
    return r;
};
```
* 时间复杂度:二分范围为 $[1, 1e8]$,`check` 函数的复杂度为 $O(n)$。整体复杂度为 $O(n\log{1e8})$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1011` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/1011-1020/1012. 至少有 1 位重复的数字（困难）.md

1012. 至少有 1 位重复的数字

Question:
给定正整数 $n$,返回在 $[1, n]$ 范围内具有 至少 1 位 重复数字的正整数的个数。

示例 1:
```
输入:n = 20

输出:1

解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。
```
示例 2:
```
输入:n = 100

输出:10

解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。
```
示例 3:
```
输入:n = 1000

输出:262
```

提示:
* $1 <= n <= 10^9$

Solution:
### 数位 DP

**首先 $[1, n]$ 共有 $n$ 个数,而求解 $[1, n]$ 范围内有多少个「至少有 $1$ 位重复数字」的数的个数,等价于「总数 $n$」减去「没有重复数的数的个数」。**

于是问题转换为:**如何求解「没有重复数」的数的个数,同时为了更具一般性,我们将求解 $[1, n]$ 范围内「没有重复数」的数的个数,修改为求解 $[0, n]$ 范围内「没有重复数」的数的个数**。

>即将问题转换为 [(题解) 357. 统计各位数字都不同的数字个数](https://leetcode-cn.com/problems/count-numbers-with-unique-digits/solution/by-ac_oier-6tfl/) 中的进阶部分。

假定我们存在函数 `int dp(int x)` 函数,能够返回区间 $[0, x]$ 内合法数的个数,那么配合「容斥原理」我们便能够回答任意区间合法数的查询:

$$
ans_{(l, r)} = dp(r) - dp(l - 1)
$$

然后考虑如何实现 `int dp(int x)` 函数,我们将组成 $[0, x]$ 的合法数分成三类:
* 位数和 $x$ 相同,且最高位比 $x$ 最高位要小的,这部分统计为 `res1`;
* 位数和 $x$ 相同,且最高位与 $x$ 最高位相同的,这部分统计为 `res2`;
* 位数比 $x$ 少,这部分统计为 `res3`。

其中 `res1` 和 `res3` 求解相对简单,重点落在如何求解 `res2` 上。

**对 $x$ 进行「从高到低」的处理(假定 $x$ 数位为 $n$),对于第 $k$ 位而言($k$ 不为最高位),假设在 $x$ 中第 $k$ 位为 $cur$,那么为了满足「大小限制」关系,我们只能在 $[0, cur - 1]$ 范围内取数,同时为了满足「相同数字只能使用一次」的限制,我们需要使用一个 `int` 变量 $s$ 来记录使用情况(用 $s$ 的低十位来代指数字 $[0, 9]$ 是否被使用),统计 $[0, cur - 1]$ 范围内同时符合两个限制条件的数的个数,记为 $cnt$。**

**当第 $k$ 位有 $cnt$ 种合法选择之后,后面的位数可以在满足「相同数字只能使用一次」的限制条件下任意选择(因为大小关系已经由第 $k$ 位保证),为了快速知道剩下的 $n - k$ 位有多少种方案,我们还需要预处理乘积数组,其中 $f[l][r]$ 代表 $l * (l + 1) * ... * (j - 1) * j$ 的乘积之和。**

> 上述讲解若是觉得抽象,我们可以举个 🌰,假设 $x = 678$,我们该如何求解 `res2`:由于限定了 `res2` 为「位数和 $x$ 相同,且最高位与 $x$ 最高位相同的」的合法数个数,因此最高位没有选,只能是 $6$,然后考虑处理次高位,次高位在 $x$ 中为 $7$,为了满足大小关系,我们只能在 $[0, 6]$ 范围内做限制,同时由于 $6$ 已用过,因此次高位实际只有 $[0, 5]$,共 $6$ 种选择,当确定次高位后,后面的位数任意取,由于前面已经填充了 $p = 2$ 位(即消耗了 $p$ 个不同数字),因此从后面的位数开始应该是 $a = 10 - p$ 开始往后自减累乘到 $b = (10 - p) - (n - p) + 1$ 为止,即此时方案数为 $cnt * f[b][a]$(当前位不是最低位)或者 $cnt$(当前位是最低位)。按照此逻辑循环处理所有位数即可,直到遇到重复数值或正常结束。

需要说明的是,上述的举例部分只是为方便大家理解过程,看懂了举例部分不代表理解了数位 DP 做法成立的内在条件,阅读的重点还是要放在前面加粗字体部分,只会使用样例理解算法永远不是科学的做法。

其他细节:乘积数组的预处理与样例无关,我们可以使用 `static` 进行打表优化,同时可以将 `res1` 和 `res2` 两种情况进行合并。

代码:
```Java
class Solution {
    // f[l][r] 代表 i * (i + 1) * ... * (j - 1) * j
    static int[][] f = new int[10][10];
    static {
        for (int i = 1; i < 10; i++) {
            for (int j = i; j < 10; j++) {
                int cur = 1;
                for (int k = i; k <= j; k++) cur *= k;
                f[i][j] = cur;
            }
        }
    }
    int dp(int x) {
        int t = x;
        List<Integer> nums = new ArrayList<>();
        while (t != 0) {
            nums.add(t % 10);
            t /= 10;
        }
        int n = nums.size();
        if (n <= 1) return x + 1; // [0, 9]
        // 位数和 x 相同(res1 + res2)
        int ans = 0;
        for (int i = n - 1, p = 1, s = 0; i >= 0; i--, p++) {
            int cur = nums.get(i), cnt = 0;
            for (int j = cur - 1; j >= 0; j--) {
                if (i == n - 1 && j == 0) continue;
                if (((s >> j) & 1) == 0) cnt++;
            }
            int a = 10 - p, b = a - (n - p) + 1;
            ans += b <= a ? cnt * f[b][a] : cnt;
            if (((s >> cur) & 1) == 1) break;
            s |= (1 << cur);
            if (i == 0) ans++;
        }
        // 位数比 x 少(res3)
        ans += 10;
        for (int i = 2, last = 9; i < n; i++) {
            int cur = last * (10 - i + 1);
            ans += cur; last = cur;
        }
        return ans;
    }
    public int numDupDigitsAtMostN(int n) {
        return (n + 1) - dp(n);
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(C)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1012` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个大小为 $m x n$ 的二进制矩阵 $grid$ ,其中 $0$ 表示一个海洋单元格、$1$ 表示一个陆地单元格。

一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 $grid$ 的边界。

返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。

示例 1:

```
输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]

输出:3

解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。
```
示例 2:

```
输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]]

输出:0

解释:所有 1 都在边界上或可以到达边界。
```

提示:
* $m == grid.length$
* $n == grid[i].length$
* $1 <= m, n <= 500$
* $grid[i][j]$ 的值为 $0$ 或 $1$

Solution:
### 并查集 + DFS

根据题目定义,我们知道需要统计所有不和「边缘陆地」相连通的「普通陆地」数量。

我们可以用「并查集」来维护连通块,使用 `DFS` 对所有「边缘陆地连通块」进行标记(设定编号为 $0$ 的超级源点,对于所有的「边缘陆地连通块」,将其与超级源点联通)。

具体的,我们按照如下流程进行处理:

* 初始化并查集:起始让每个单元格独立作为一个连通块;
* 使用 `DFS`  标记所有「边缘陆地连通块」:从位于边缘的「边缘陆地」进行出发,将其所在连通块与超级源点 $0$ 进行联通标记(同时为了确保复杂度,我们在进行 `DFS` 前需要先检查当前陆地与超级源点的联通关系,如果已联通,说明当前陆地隶属于之前的某个连通块,已被整体标记过,进行跳过即可);
* 统计答案:遍历整个棋盘,统计所有不与超级源点 $0$ 联通的陆地数量。

> 一些细节:由于我们人为规定了超级源点编号为 $0$,同时棋盘下标从 $0$ 开始,因此对某个点 $(x, y)$ 的编号,我们需要增加一个偏移量,例如 $idx = x * n + y + 1$。

代码:
```Java
class Solution {
    int N = 550;
    int[] p = new int[N * N];
    int m, n;
    int[][] g;
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    void union(int a, int b) {
        p[find(a)] = find(b);
    }
    public int numEnclaves(int[][] grid) {
        g = grid;
        m = g.length; n = g[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                p[getIdx(i, j)] = getIdx(i, j);
            }
        }
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
                    if (g[i][j] != 1 || query(getIdx(i, j), 0)) continue;
                    dfs(i, j);
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1 && !query(getIdx(i, j), 0)) ans++;
            }
        }
        return ans;
    }
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    void dfs(int x, int y) {
        union(getIdx(x, y), 0);
        for (int[] d : dirs) {
            int nx = x + d[0], ny = y + d[1];
            if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
            if (g[nx][ny] != 1 || query(getIdx(nx, ny), 0)) continue;
            dfs(nx, ny);
        }
    }
    int getIdx(int x, int y) {
        return x * n + y + 1;
    }
}
```
* 时间复杂度:初始化并查集复杂度为 $O(m * n)$;使用 `DFS` 对边缘陆地连通块进行标记复杂度为 $O(m * n)$;统计答案复杂度为 $O(m * n)$。整体复杂度为 $O(m * n)$
* 空间复杂度:$O(m * n)$

---

### 多源 BFS

也可以使用「多源 `BFS`」进行求解。

将所有「边缘陆地」看做与超级源点相连,起始将所有「边缘陆地」进行入队(等价于只将超级源点入队,然后取出超级源点并进行拓展)。

然后是常规的 `BFS` 过程,所有能够出队/入队的陆地格子,都代表与「边缘陆地」联通,都不属于「飞地」,对其进行标记。

最后遍历整个棋盘,统计所有未被标记的「陆地」格子数量即是答案。

不熟悉「多源 `BFS`」的同学可以看前置 🧀 :[多源 BFS 入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487179&idx=1&sn=e30a662c03fba3861254dbcf3fb9d6f2&chksm=fd9ca5d4caeb2cc205804fd17a2ce86b25d0408adc3417e73154f59d37e7cb17e02374f5122c&token=1222269277&lang=zh_CN#rd)。

代码:
```Java
class Solution {
    public int numEnclaves(int[][] g) {
        int m = g.length, n = g[0].length;
        boolean[][] vis = new boolean[m][n];
        Deque<int[]> d = new ArrayDeque<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 || j == 0 || i == m - 1 || j == n - 1) {
                    if (g[i][j] == 0) continue;
                    vis[i][j] = true;
                    d.addLast(new int[]{i, j});
                }
            }
        }
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int x = poll[0], y = poll[1];
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                if (g[nx][ny] != 1) continue;
                if (vis[nx][ny]) continue;
                vis[nx][ny] = true;
                d.addLast(new int[]{nx, ny});
            }
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1 && !vis[i][j]) ans++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(m * n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1011` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/1021-1030/1021. 删除最外层的括号（简单）.md

1021. 删除最外层的括号

Question:
有效括号字符串为空 `""`、`"(" + A + ")"` 或 `A + B` ,其中 `A` 和 `B` 都是有效的括号字符串,`+` 代表字符串的连接。

例如,`""`,`"()"`,`"(())()"` 和 `"(()(()))"` 都是有效的括号字符串。

如果有效字符串 `s` 非空,且不存在将其拆分为 `s = A + B` 的方法,我们称其为原语(`primitive`),其中 `A` 和 `B` 都是非空有效括号字符串。

给出一个非空有效字符串 `s`,考虑将其进行原语化分解,使得:`s = P_1 + P_2 + ... + P_k`,其中 `P_i` 是有效括号字符串原语。

对 `s` 进行原语化分解,删除分解中每个原语字符串的最外层括号,返回 s 。

示例 1:
```
输入:s = "(()())(())"

输出:"()()()"

解释:
输入字符串为 "(()())(())",原语化分解得到 "(()())" + "(())",
删除每个部分中的最外层括号后得到 "()()" + "()" = "()()()"。
```
示例 2:
```
输入:s = "(()())(())(()(()))"

输出:"()()()()(())"

解释:
输入字符串为 "(()())(())(()(()))",原语化分解得到 "(()())" + "(())" + "(()(()))",
删除每个部分中的最外层括号后得到 "()()" + "()" + "()(())" = "()()()()(())"。
```
示例 3:
```
输入:s = "()()"

输出:""

解释:
输入字符串为 "()()",原语化分解得到 "()" + "()",
删除每个部分中的最外层括号后得到 "" + "" = ""。
```

提示:
* $1 <= s.length <= 10^5$
* `s[i]` 为 `'('` 或 `')'`
* `s` 是一个有效括号字符串

Solution:
### 双指针模拟

和其他「有效括号」问题一样,我们将 `(` 记为 $1$ 分,将 `)` 记为 $-1$。

由于 `s` 本身为有效字符串,因此我们可以从前往后进行处理,每一次找到符合 $socre = 0$ 的连续段 $[i, j)$,然后再结合题意,删掉外层括号,即将 $[i + 1, j - 1)$ 加入答案。

代码:
```Java
class Solution {
    public String removeOuterParentheses(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length;
        StringBuilder sb = new StringBuilder();
        for (int i = 0, score = 0; i < n; ) {
            int j = i;
            while (i == j || score != 0) score += cs[j++] == '(' ? 1 : -1;
            sb.append(s.substring(i + 1, j - 1));
            i = j;
            score = 0;
        }
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1021` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


LeetCode/1021-1030/1022. 从根到叶的二进制数之和（简单）.md

1022. 从根到叶的二进制数之和

Question:
给出一棵二叉树,其上每个结点的值都是 $0$ 或 $1$ 。每一条从根到叶的路径都代表一个从最高有效位开始的二进制数。

例如,如果路径为 `0 -> 1 -> 1 -> 0 -> 1`,那么它表示二进制数 `01101`,也就是 $13$ 。
对树上的每一片叶子,我们都要找出从根到该叶子的路径所表示的数字。

返回这些数字之和。题目数据保证答案是一个 $32$ 位 整数。

示例 1:

```
输入:root = [1,0,1,0,1,0,1]

输出:22

解释:(100) + (101) + (110) + (111) = 4 + 5 + 6 + 7 = 22
```
示例 2:
```
输入:root = [0]

输出:0
```

提示:
* 树中的节点数在 $[1, 1000]$ 范围内
* `Node.val` 仅为 $0$ 或 $1$

Solution:
### 递归

容易想到「递归」进行求解,在 `DFS` 过程中记录下当前的值为多少,假设遍历到当前节点 $x$ 前,记录的值为 $cur$,那么根据题意,我们需要先将 $cur$ 进行整体左移(腾出最后一位),然后将节点 $x$ 的值放置最低位来得到新的值,并继续进行递归。

递归有使用「函数返回值」和「全局变量」两种实现方式。

代码:
```Java
class Solution {
    public int sumRootToLeaf(TreeNode root) {
        return dfs(root, 0);
    }
    int dfs(TreeNode root, int cur) {
        int ans = 0, ncur = (cur << 1) + root.val;
        if (root.left != null) ans += dfs(root.left, ncur);
        if (root.right != null) ans += dfs(root.right, ncur);
        return root.left == null && root.right == null ? ncur : ans;
    }
}
```

-

```Java
class Solution {
    int ans = 0;
    public int sumRootToLeaf(TreeNode root) {
        dfs(root, 0);
        return ans;
    }
    void dfs(TreeNode root, int cur) {
        int ncur = (cur << 1) + root.val;
        if (root.left != null) dfs(root.left, ncur);
        if (root.right != null) dfs(root.right, ncur);
        if (root.left == null && root.right == null) ans += ncur;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 迭代

自然也可以使用「迭代」进行求解。

为了不引入除「队列」以外的其他数据结构,当我们可以把某个节点 $x$ 放出队列时,先将其的值修改为当前遍历路径对应的二进制数。

代码:
```Java
class Solution {
    public int sumRootToLeaf(TreeNode root) {
        int ans = 0;
        Deque<TreeNode> d = new ArrayDeque<>();
        d.addLast(root);
        while (!d.isEmpty()) {
            TreeNode poll = d.pollFirst();
            if (poll.left != null) {
                poll.left.val = (poll.val << 1) + poll.left.val;
                d.addLast(poll.left);
            }
            if (poll.right != null) {
                poll.right.val = (poll.val << 1) + poll.right.val;
                d.addLast(poll.right);
            }
            if (poll.left == null && poll.right == null) ans += poll.val;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1022` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
设计一个算法:接收一个字符流,并检查这些字符的后缀是否是字符串数组 `words` 中的一个字符串。

例如,`words = ["abc", "xyz"]` 且字符流中逐个依次加入 $4$ 个字符 `'a'`、`'x'`、`'y'` 和 `'z'` ,你所设计的算法应当可以检测到 `"axyz"` 的后缀 `"xyz"` 与 `words` 中的字符串 `"xyz"` 匹配。

按下述要求实现 `StreamChecker` 类:

* `StreamChecker(String[] words)` :构造函数,用字符串数组 `words` 初始化数据结构。
* `boolean query(char letter)`:从字符流中接收一个新字符,如果字符流中的任一非空后缀能匹配 `words` 中的某一字符串,返回 `true`;否则,返回 `false`。

示例:
```
输入:
["StreamChecker", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query", "query"]
[[["cd", "f", "kl"]], ["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"], ["j"], ["k"], ["l"]]

输出:
[null, false, false, false, true, false, true, false, false, false, false, false, true]

解释:
StreamChecker streamChecker = new StreamChecker(["cd", "f", "kl"]);
streamChecker.query("a"); // 返回 False
streamChecker.query("b"); // 返回 False
streamChecker.query("c"); // 返回n False
streamChecker.query("d"); // 返回 True ,因为 'cd' 在 words 中
streamChecker.query("e"); // 返回 False
streamChecker.query("f"); // 返回 True ,因为 'f' 在 words 中
streamChecker.query("g"); // 返回 False
streamChecker.query("h"); // 返回 False
streamChecker.query("i"); // 返回 False
streamChecker.query("j"); // 返回 False
streamChecker.query("k"); // 返回 False
streamChecker.query("l"); // 返回 True ,因为 'kl' 在 words 中
```

提示:
* $1 <= words.length <= 2000$
* $1 <= words[i].length <= 200$
* `words[i]` 由小写英文字母组成
* `letter` 是一个小写英文字母
* 最多调用查询 $4 \times 10^4$ 次

Solution:
### Trie + 枚举

先考虑最为简单的做法:将给定的所有 $words[i]$ 顺序插入字典树,根据数据范围可知这一步计算量为 $2000 \times 200$,其中最大的 $words[i]$ 长度只有 $200$。

然后利用$words[i]$ 长度只有 $200$ 这一条件,直接使用「枚举」的方式来实现 `query`。

具体的,我们可以先使用一个字符串 `s` 来记录 `query` 操作产生的数据流,然后实现一个 `boolean query(int start, int end)` 方法,该方法会检查字典树中是否存在 $s[i...j]$ 子串。

由于 $words[i]$ 长度只有 $200$(假设当前 `s` 的长度为 $n$),因此我们只需要枚举「$\max(0, n - 200)$ 作为子串左端点,$n - 1$ 作为子串右端点」是否存在字典树中(是否存在 $words[i]$ 中)即可,最坏情况下,单次 `query` 操作计算量为 $200 \times 200$。

> 一些细节:为了避免每个样例都 `new` 大数组,我们可以使用 `static` 优化。

Java 代码:
```Java
class StreamChecker {
    static int N = 2010 * 200, idx = 0;
    static int[][] tr = new int[N][26];
    static boolean[] isEnd = new boolean[N * 26];
    StringBuilder sb = new StringBuilder();
    void add(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    boolean query(int start, int end) {
        int p = 0;
        for (int i = start; i <= end; i++) {
            int u = sb.charAt(i) - 'a';
            if (tr[p][u] == 0) return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
    public StreamChecker(String[] words) {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(tr[i], 0);
            isEnd[i] = false;
        }
        idx = 0;
        for (String s : words) add(s);
    }
    public boolean query(char c) {
        sb.append(c);
        int n = sb.length(), min = Math.max(0, n - 200);
        for (int i = n - 1; i >= min; i--) {
            if (query(i, n - 1)) return true;
        }
        return false;
    }
}
```
C++ 代码:
```C++
class StreamChecker {
public:
    static const int N = 2010 * 200;
    int tr[N][26];
    bool isEnd[N];
    string sb;
    int idx = 0;
    void add(const string &s) {
        int p = 0;
        for (char c : s) {
            int u = c - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    bool query(int start, int end) {
        int p = 0;
        for (int i = start; i <= end; i++) {
            int u = sb[i] - 'a';
            if (tr[p][u] == 0) return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
    StreamChecker(const vector<string>& words) {
        memset(tr, 0, sizeof(tr));
        memset(isEnd, 0, sizeof(isEnd));
        for (const string &s : words) add(s);
    }
    bool query(char c) {
        sb.push_back(c);
        int n = sb.length(), min = max(0, n - 200);
        for (int i = n - 1; i >= min; i--) {
            if (query(i, n - 1)) return true;
        }
        return false;
    }
};
```
* 时间复杂度:`StreamChecker` 初始化复杂度为 $O(n)$,其中 $n$ 为 `words` 字符总数;`query` 操作复杂度为 $O(m^2)$,其中 $m = 200$ 为最大 `words[i]` 长度
* 空间复杂度:$O(n \times C)$,其中 $n$ 为 `words` 字符总数,$C = 26$ 为字符集大小

---

### Trie(优化)

初始化将所有的 $words[i]$ 存入 `Trie` 是必然的,我们只能考虑如何优化 `query` 操作。

在解法一中,我们需要对新数据流对应的字符串的每个后缀进行搜索,同时每次搜索是相互独立的,即本次匹配不会对下一次匹配产生贡献。

**实际上,我们可以通过「倒序建 `Trie`」的方式,将「枚举检查多个后缀」的操作变为「匹配一次后缀」操作。**

具体的,我们可以在初始化 `StreamChecker` 时,将每个 $words[i]$ 翻转(倒序)加入 `Trie` 中;然后在 `query` 操作时(假设当前数据流对应的字符串为 `s`,长度为 $n$),从 `s` 的尾部开始在 `Trie` 中进行检索(即从 $s[n - 1]$ 开始往回找)。

若在某个位置 `idx` 时匹配成功,意味着 $s[idx ... (n-1)]$ 的翻转子串在字典树中,同时我们又是将每个 `words[i]` 进行倒序插入,即意味着 $s[idx ... (n - 1)]$ 的正向子串在 `words` 中,即满足 `s` 的某个后缀出现在 `words` 中。

同理,我们可以利用最大的 `words[i]` 长度为 $200$ 来控制从 $s[n - 1]$ 开始往回找的最远距离,同时利用当某个短后缀不在 `Trie` 中,则其余长度更大的后缀必然不在 `Trie` 中进行剪枝操作。

Java 代码:
```Java
class StreamChecker {
    static int N = 2010 * 200, idx = 0;
    static int[][] tr = new int[N][26];
    static boolean[] isEnd = new boolean[N * 26];
    StringBuilder sb = new StringBuilder();
    void add(String s) {
        int p = 0;
        for (int i = s.length() - 1; i >= 0; i--) {
            int u = s.charAt(i) - 'a';
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    public StreamChecker(String[] words) {
        for (int i = 0; i <= idx; i++) {
            Arrays.fill(tr[i], 0);
            isEnd[i] = false;
        }
        idx = 0;
        for (String s : words) add(s);
    }
    public boolean query(char c) {
        sb.append(c);
        int n = sb.length(), min = Math.max(0, n - 200), p = 0;
        for (int i = n - 1; i >= min; i--) {
            if (isEnd[p]) return true;
            int u = sb.charAt(i) - 'a';
            if (tr[p][u] == 0) return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
}
```
C++ 代码:
```C++
class StreamChecker {
public:
    static const int N = 2010 * 200;
    static const int ALPHABET_SIZE = 26;
    vector<vector<int>> tr;
    vector<bool> isEnd;
    string sb;
    int idx = 0;
    void add(const string &s) {
        int p = 0;
        for (int i = s.length() - 1; i >= 0; i--) {
            int u = s[i] - 'a';
            if (tr[p].size() <= u) tr[p].resize(u + 1, 0);
            if (tr[p][u] == 0) tr[p][u] = ++idx;
            p = tr[p][u];
        }
        isEnd[p] = true;
    }
    StreamChecker(const vector<string>& words) {
        tr.resize(N);
        isEnd.resize(N);
        fill(isEnd.begin(), isEnd.end(), false);
        for (const auto &s : words) {
            add(s);
        }
    }
    bool query(char c) {
        sb.push_back(c);
        int n = sb.length(), min = max(0, n - 200), p = 0;
        for (int i = n - 1; i >= min; i--) {
            if (isEnd[p]) return true;
            int u = sb[i] - 'a';
            if (tr[p].size() <= u || tr[p][u] == 0) return false;
            p = tr[p][u];
        }
        return isEnd[p];
    }
};
```
* 时间复杂度:`StreamChecker` 初始化复杂度为 $O(n)$,其中 $n$ 为 `words` 字符总数;`query` 操作复杂度为 $O(m)$,其中 $m = 200$ 为最大 `words[i]` 长度
* 空间复杂度:$O(n \times C)$,其中 $n$ 为 `words` 字符总数,$C = 26$ 为字符集大小

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1032` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
给你一个大小为 `m x n` 的整数矩阵 `grid`,表示一个网格。另给你三个整数 `row`、`col` 和 `color` 。网格中的每个值表示该位置处的网格块的颜色。

当两个网格块的颜色相同,而且在四个方向中任意一个方向上相邻时,它们属于同一 连通分量 。

连通分量的边界 是指连通分量中的所有与不在分量中的网格块相邻(四个方向上)的所有网格块,或者在网格的边界上(第一行/列或最后一行/列)的所有网格块。

请你使用指定颜色 `color` 为所有包含网格块 `grid[row][col]` 的 连通分量的边界 进行着色,并返回最终的网格 `grid` 。

示例 1:
```
输入:grid = [[1,1],[1,2]], row = 0, col = 0, color = 3

输出:[[3,3],[3,2]]
```
示例 2:
```
输入:grid = [[1,2,2],[2,3,2]], row = 0, col = 1, color = 3

输出:[[1,3,3],[2,3,3]]
```
示例 3:
```
输入:grid = [[1,1,1],[1,1,1],[1,1,1]], row = 1, col = 1, color = 2

输出:[[2,2,2],[2,1,2],[2,2,2]]
```

提示:
* $m == grid.length$
* $n == grid[i].length$
* $1 <= m, n <= 50$
* $1 <= grid[i][j], color <= 1000$
* $0 <= row < m$
* $0 <= col < n$

Solution:
### 基本分析

这是一道结合「阅读理解」的常规图论搜索题。

基本题意为:从题目给定的 $(row, col)$ 进行出发,如果遍历到「**连通分量的边界**」格子,则使用 $color$ 进行上色。

同一「连通分量」的「非边界」格子满足:**当前格子的四联通方向均存在相邻格子,且当前格子与四联通相邻格子颜色一致。**

也就是说,我们从 $(row, col)$ 进行出发,遍历 $(row, col)$ 所在的「连通分量」,如果遍历到的「连通分量」格子不满足上述条件(边界格子),则进行上色。

---

### BFS

具体的,我们可以使用 `BFS` 进行求解:

* 构造 $ans$ 矩阵作为答案,同时 $ans$ 也作为判重数组使用(通过判断 $ans[i][j]$ 是否为 $0$ 来得知是否被处理);

* 起始时,将 $(row, col)$ 位置进行入队,每次从队列中取出元素进行「四联通拓展」:

    * 拓展格子必须与起点格子处于同一「连通分量」,即满足两者起始颜色相同;

    * 进行「四联通拓展」的同时,记录当前出队是否为边界格子。若为边界格子,则使用 $color$ 进行上色;

* 跑完 `BFS` 后,对 $ans$ 进行遍历,将未上色($ans[i][j] = 0$)的位置使用原始色($grid[i][j]$)进行上色。

代码:
```Java
class Solution {
    public int[][] colorBorder(int[][] grid, int row, int col, int color) {
        int m = grid.length, n = grid[0].length;
        int[][] ans = new int[m][n];
        int[][] dirs = new int[][]{{1,0}, {-1,0}, {0,1}, {0,-1}};
        Deque<int[]> d = new ArrayDeque<>();
        d.addLast(new int[]{row, col});
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int x = poll[0], y = poll[1], cnt = 0;
            for (int[] di : dirs) {
                int nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
                if (grid[x][y] != grid[nx][ny]) continue;
                else cnt++;
                if (ans[nx][ny] != 0) continue;
                d.addLast(new int[]{nx, ny});
            }
            ans[x][y] = cnt == 4 ? grid[x][y] : color;
        }
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (ans[i][j] == 0) ans[i][j] = grid[i][j];
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(m * n)$

---

### DFS

同理,可以使用 `DFS` 进行求解。

由于使用 `DFS`  搜索时,我们使用「栈帧压栈/弹栈」作为拓展联通节点的容器,且仅在出队时进行上色。为防止「重复入队」问题,我们需要先在对节点 $(nx, ny)$ 入队时,先设置将 $ans[nx][ny]$ 设置为 $-1$ 标识位,以作为判重依据。

代码:
```Java
class Solution {
    int m, n, c;
    int[][] grid, ans;
    int[][] dirs = new int[][]{{1,0}, {-1,0}, {0,1}, {0,-1}};
    public int[][] colorBorder(int[][] _grid, int row, int col, int color) {
        grid = _grid; c = color;
        m = grid.length; n = grid[0].length;
        ans = new int[m][n];
        dfs(row, col);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (ans[i][j] == 0) ans[i][j] = grid[i][j];
            }
        }
        return ans;
    }
    void dfs(int x, int y) {
        int cnt = 0;
        for (int[] di : dirs) {
            int nx = x + di[0], ny = y + di[1];
            if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue;
            if (grid[x][y] != grid[nx][ny]) continue;
            else cnt++;
            if (ans[nx][ny] != 0) continue;
            ans[nx][ny] = -1;
            dfs(nx, ny);
        }
        ans[x][y] = cnt == 4 ? grid[x][y] : c;
    }
}
```
* 时间复杂度:$O(m * n)$
* 空间复杂度:$O(m * n)$

---

### 其他「图论搜索」相关内容

题太简单?不如来学习热乎的 [常规 BFS 搜索题(二维转一维)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489621&idx=1&sn=5d43fb97bc167a50a7aeb4ae2068571c&chksm=fd9cb34acaeb3a5c7e1e2e2a88d460ae2418a3cef615e1abf017b5d58aa1e7f490856d67f800&token=2136593799&lang=zh_CN#rd)

* [常规 BFS 搜索题(二维转一维)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489621&idx=1&sn=5d43fb97bc167a50a7aeb4ae2068571c&chksm=fd9cb34acaeb3a5c7e1e2e2a88d460ae2418a3cef615e1abf017b5d58aa1e7f490856d67f800&token=2136593799&lang=zh_CN#rd)
* [多源 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247487179&idx=1&sn=e30a662c03fba3861254dbcf3fb9d6f2&chksm=fd9ca5d4caeb2cc205804fd17a2ce86b25d0408adc3417e73154f59d37e7cb17e02374f5122c&scene=178&cur_album_id=1917113998693449732#rd)
* [双向 BFS](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489502&idx=1&sn=dc863d4bc71c4739a4799b9a4558bd01&chksm=fd9cbcc1caeb35d749d0d72f485485527482c27b608c8f4062c29a997ede97a09ce598b58c7f&scene=178&cur_album_id=1917113998693449732#rd)
* [双向 BFS II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247486981&idx=1&sn=045ea6c880080fea1ce807794ccff69b&chksm=fd9ca51acaeb2c0c83d13e3b2a5196895d1a1b44f8981cc3efad9d6a2af158267010646cc262&scene=178&cur_album_id=1917113998693449732#rd)
*  [灵活运用多种搜索方式(含启发式搜索)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489560&idx=2&sn=bb966d868c18d656620a20d31a425b23&chksm=fd9cb307caeb3a11424428f0a88e7f0cb86bb53b3e5a2b9e28683a24bcb3ac151655d2b6419e&scene=178&cur_album_id=1917113998693449732#rd)
*  [灵活运用多种搜索方式 II(含启发式搜索)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489588&idx=1&sn=479e4c0627247ab7e20af7909f2a8b64&chksm=fd9cb32bcaeb3a3d4f0bd73f023a92a165edabf212af1db9672a55bed1af7d4e32e8af9964c3&scene=178&cur_album_id=1917113998693449732#rd)

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1034` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。


Question:
在两条独立的水平线上按给定的顺序写下 `s1` 和 `s2` 中的整数。

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

*  $s1[i] = s2[j]$
* 且绘制的直线不与任何其他连线(非水平线)相交。

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

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

示例 1:

<img src="https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/04/28/142.png" style="zoom:18%;" />

```
输入:s1 = [1,4,2], s2 = [1,2,4]

输出:2

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

输出:3
```
示例 3:
```
输入:s1 = [1,3,7,1,7,5], s2 = [1,9,2,5,1]

输出:2
```

提示:
* $1 <= s1.length <= 500$
* $1 <= s2.length <= 500$
* $1 <= s1[i], s2[i] <= 2000$

Solution:
### 动态规划

这是一道「[最长公共子序列(LCS)](https://leetcode-cn.com/problems/longest-common-subsequence/solution/gong-shui-san-xie-zui-chang-gong-gong-zi-xq0h/)」的轻度变形题。

对于这类题都使用如下「状态定义」即可:

**$f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 的字符,形成的最长公共子序列长度。**

然后不失一般性的考虑 $f[i][j]$ 如何转移。

**由于我们的「状态定义」只是说「考虑前 $i$ 个和考虑前 $j$ 个字符」,并没有说「一定要包含第 $i$ 个或者第 $j$ 个字符」(这也是「最长公共子序列 LCS」与「最长上升子序列 LIS」状态定义上的最大不同)。**

我们需要考虑「不包含 $s1[i]$,不包含 $s2[j]$」、「不包含 $s1[i]$,包含 $s2[j]$」「包含 $s1[i]$,不包含 $s2[j]$」、「包含 $s1[i]$,包含 $s2[j]$」四种情况:

 * 不包含 $s1[i]$,不包含 $s2[j]$:结合状态定义,可以使用 $f[i - 1][j - 1]$ 进行精确表示。
 * 包含 $s1[i]$,包含 $s2[j]$:前提是 $s1[i] = s2[j]$,可以使用 $f[i - 1][j - 1] + 1$ 进行精确表示。
 * 不包含 $s1[i]$,包含 $s2[j]$:结合状态定义,我们无法直接将该情况表示出来。
    **注意 $f[i - 1][j]$ 只是表示「必然不包含 $s1[i]$,但可能包含$s2[j]$」的情况,也就是说 $f[i - 1][j]$ 其实是该情况与情况 $1$ 的合集**。
    但是由于我们求的是「最大值」,只需要确保「不漏」即可保证答案的正确(某些情况被重复参与比较不影响正确性),因此这里直接使用 $f[i - 1][j]$ 进行表示没有问题。
* 包含 $s1[i]$,不包含 $s2[j]$:与情况 $3$ 同理,直接使用 $f[i][j - 1]$ 表示没有问题。

$f[i][j]$ 就是在上述所有情况中取 $max$ 而来,由于情况 $1$ 被 情况 $3$ 和 情况 $4$ 所包含,因此我们只需要考虑 $f[i - 1][j]$、$f[i][j -1]$ 和 $f[i - 1][j - 1] + 1$ 三种状态即可,其中最后一种状态需要满足 $s1[i] = s2[j]$ 前提条件。

因此我们最后的状态转移方程为:

$$
f[i][j]=\begin{cases}
\max(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1] + 1) & s1[i] = s2[j] \\
\max(f[i - 1][j], f[i][j - 1]) & s1[i] \neq s2[j] \\
\end{cases}
$$

上述分析过程建议加深理解,估计很多同学能 AC 但其实并不知道 LCS 问题的状态转移是包含了「重复状态比较」的。

Java 代码:
```Java
class Solution {
    public int maxUncrossedLines(int[] s1, int[] s2) {
        int n = s1.length, m = s2.length;
        int[][] f = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
                if (s1[i - 1] == s2[j - 1]) {
                    f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
                }
            }
        }
        return f[n][m];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxUncrossedLines(vector<int>& s1, vector<int>& s2) {
        int n = s1.size(), m = s2.size();
        vector<vector<int>> f(n + 1, vector<int>(m + 1, 0));
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                f[i][j] = max(f[i - 1][j], f[i][j - 1]);
                if (s1[i - 1] == s2[j - 1]) {
                    f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
                }
            }
        }
        return f[n][m];
    }
};
```
Python 代码:
```Python
class Solution:
    def maxUncrossedLines(self, s1: List[int], s2: List[int]) -> int:
        n, m = len(s1), len(s2)
        f = [[0] * (m + 1) for _ in range(n + 1)]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                f[i][j] = max(f[i - 1][j], f[i][j - 1])
                if s1[i - 1] == s2[j - 1]:
                    f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1)
        return f[n][m]
```
TypeScript 代码:
```TypeScript
function maxUncrossedLines(s1: number[], s2: number[]): number {
    const n = s1.length, m = s2.length;
    const f = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));
    for (let i = 1; i <= n; ++i) {
        for (let j = 1; j <= m; ++j) {
            f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
            if (s1[i - 1] === s2[j - 1]) {
                f[i][j] = Math.max(f[i][j], f[i - 1][j - 1] + 1);
            }
        }
    }
    return f[n][m];
};
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

这是我们「刷穿 LeetCode」系列文章的第 `No.1035` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。