Question:
在一个 $10^6 \times 10^6$ 的网格中,每个网格上方格的坐标为 $(x, y)$ 。

现在从源方格 $source = [s_x, s_y]$ 开始出发,意图赶往目标方格 $target = [t_x, t_y]$ 。

数组 $blocked$ 是封锁的方格列表,其中每个 $blocked[i] = [x_i, y_i]$ 表示坐标为 $(x_i, y_i)$ 的方格是禁止通行的。

每次移动,都可以走到网格中在四个方向上相邻的方格,只要该方格 不 在给出的封锁列表 $blocked$ 上。同时,不允许走出网格。

只有在可以通过一系列的移动从源方格 $source$ 到达目标方格 $target$ 时才返回 $true$。否则,返回 $false$。

示例 1:
```
输入:blocked = [[0,1],[1,0]], source = [0,0], target = [0,2]

输出:false

解释:
从源方格无法到达目标方格,因为我们无法在网格中移动。
无法向北或者向东移动是因为方格禁止通行。
无法向南或者向西移动是因为不能走出网格。
```
示例 2:
```
输入:blocked = [], source = [0,0], target = [999999,999999]

输出:true

解释:
因为没有方格被封锁,所以一定可以到达目标方格。
```

提示:
* $0 <= blocked.length <= 200$
* $blocked[i].length == 2$
* $0 <= xi, yi < 10^6$
* $source.length == target.length == 2$
* $0 <= sx, sy, tx, ty < 10^6$
* $source != target$
* 题目数据保证 $source$ 和 $target$ 不在封锁列表内

Solution:
### BFS + 给定障碍物所能围成的最大面积

为了方便,我们用 $s$ 代指 $source$,用 $t$ 代指 $target$,用 $n$ 来代指 $blocked$ 大小。

整理题意为:在一个足够大的空间里,有少数的障碍物,问两点是否连通。

当两点相隔较远时,常规的 `BFS` 做法可能会搜完整个棋盘,而棋盘大小为 $10^6 \times 10^6$,会 `TLE`。

考虑什么情况下两点会不连通?

当两个点中的任意一点被障碍物围住时,两点将无法连通。

一个很容易想到的思路是:**从 $s$ 跑一遍 `BFS`,然后从 $t$ 跑一遍 `BFS`,同时设定一个最大访问点数量 `MAX`,若从两者出发能够访问的点数量都能超过 `MAX`,说明两点均没有被围住,最终必然会联通。**

考虑如何敲定 `MAX`  的取值范围?直观感受,`MAX` 应该是一个与 $blocked$ 大小相关的数。

但第一反应还是想从单秒计算量上界进行反推,两边 `BFS`  的复杂度均为 $O(\max)$,因此直接设定 `MAX = 1e5` 应该是比较合适的。

更小的 `MAX` 需要证明:**在给定数量障碍物的前提下,障碍物所能围成的最大面积为多少。**

首先,容易想到:**任何一条封闭图形的直边都可以通过调整为斜边来围成更大的面积:**

**即组成封闭图形的边不可能有直边,同时由于是封闭图形,因此斜边直接必然是单点衔接,而不可能是平行(无法封闭)。**

同时,**想要达到最大面积,应当尽可能利用边界作为围成图形的某些边。**

利用边界所能围成的最大封面图形 **可以是**「由边界提供两边,障碍物提供一边的三角形」。

如果不是该形状,则可以通过调整障碍物的直边为一条完整的斜边,来组成封闭三角形,围成面积不会变小:

即给定 $n$ 的情况下,根据「等差数列求和」可知,最大所能围成的面积为 $1 + 2 + ... + n - 1 = \frac{n \times (n - 1)}{2}$。

因此如果从 $s$ 和 $t$ 出发,能够访问的点数超过 $\frac{n \times (n - 1)}{2}$ 个,那么两点并没有被围住,必然联通。

最后,为了在 `BFS` 过程中记录某些点被访问过,可以通过计算某个位置哈希值(数值)来实现。

**代码(感谢 [@🍭可乐可乐吗QAQ](/u/littletime_cc/) 和 [@Benhao](/u/himymben/) 同学提供的其他语言版本):**
```Java
class Solution {
    int EDGE = (int)1e6, MAX = (int)1e5;
    long BASE = 131L;
    Set<Long> set = new HashSet<>();
    int[][] dir = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    public boolean isEscapePossible(int[][] blocked, int[] s, int[] t) {
        for (int[] p : blocked) set.add(p[0] * BASE + p[1]);
        int n = blocked.length;
        MAX = n * (n - 1) / 2; // 可直接使用 1e5
        return check(s, t) && check(t, s);
    }
    boolean check(int[] a, int[] b) {
        Set<Long> vis = new HashSet<>();
        Deque<int[]> d = new ArrayDeque<>();
        d.addLast(a);
        vis.add(a[0] * BASE + a[1]);
        while (!d.isEmpty() && vis.size() <= MAX) {
            int[] poll = d.pollFirst();
            int x = poll[0], y = poll[1];
            if (x == b[0] && y == b[1]) return true;
            for (int[] di : dir) {
                int nx = x + di[0], ny = y + di[1];
                if (nx < 0 || nx >= EDGE || ny < 0 || ny >= EDGE) continue;
                long hash = nx * BASE + ny;
                if (set.contains(hash)) continue;
                if (vis.contains(hash)) continue;
                d.addLast(new int[]{nx, ny});
                vis.add(hash);
            }
        }
        return vis.size() > MAX;
    }
}
```

-

```C++
class Solution {
public:
    int EDGE = 1e6, MAX = 1e5;
    long long BASE = 13331;
    unordered_set<long long> set;
    int dir[4][2] = { {1, 0}, {-1, 0}, {0, 1}, {0, -1} };
    bool isEscapePossible(vector<vector<int>>& blocked, vector<int>& s, vector<int>& t) {
        for(auto& p : blocked) set.insert(p[0] * BASE + p[1]);
        int n = blocked.size();
        MAX = n * (n - 1) / 2; // 可直接使用 1e5
        return check(s, t) and check(t, s);
    }
    bool check(vector<int>& a, vector<int>& b){
        unordered_set<long long> vis;
        queue< pair<int,int> > q;
        q.push( {a[0], a[1]});
        vis.insert(a[0] * BASE + a[1]);
        while(q.size() and vis.size() <= MAX){
            auto t = q.front();
            q.pop();
            int x = t.first, y = t.second;
            if(x == b[0] and y == b[1]) return true;
            for(int i = 0; i < 4; i++){
                int nx = x + dir[i][0], ny = y + dir[i][1];
                if(nx < 0 or nx >= EDGE or ny < 0 or ny >= EDGE) continue;
                if(set.count(nx * BASE + ny)) continue;
                if(vis.count(nx * BASE + ny)) continue;
                q.push( {nx, ny} );
                vis.insert(nx * BASE + ny);
            }
        }
        return vis.size() > MAX;
    }
};
```

-

```Python
EDGE, MAX, BASE, DIR = int(1e6), int(1e5), 131, [(1, 0), (-1, 0), (0, 1), (0, -1)]
class Solution:
    def isEscapePossible(self, blocked: List[List[int]], source: List[int], target: List[int]) -> bool:
        block = {p[0] * BASE + p[1] for p in blocked}
        n = len(blocked)
        MAX = n * (n-1)//2 # 可直接使用 1e5
        def check(a, b):
            vis = {a[0] * BASE + a[1]}
            d = deque([a])
            while len(d) and len(vis) <= MAX:
                x, y = d.popleft()
                if x == b[0] and y == b[1]:
                    return True
                for dx, dy in DIR:
                    nx, ny = x + dx, y + dy
                    if nx < 0 or nx >= EDGE or ny < 0 or ny >= EDGE:
                        continue
                    h = nx * BASE + ny
                    if h in block or h in vis:
                        continue
                    d.append((nx, ny))
                    vis.add(h)
            return len(vis) > MAX
        return check(source, target) and check(target, source)
```
* 时间复杂度:令 $n$ 为 $blocked$ 大小,两次 `BFS` 的最大访问点数为 $\frac{n \times (n - 1)}{2}$。整体复杂度为 $O(n^2)$
* 空间复杂度:两次 `BFS` 的最大访问点数为 $\frac{n \times (n - 1)}{2}$。整体复杂度为 $O(n^2)$

### 最后

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

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

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

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


1037. 有效的回旋镖

Question:
给定一个数组 `points`,其中 $points[i] = [x_i, y_i]$ 表示 `X-Y` 平面上的一个点,如果这些点构成一个 回旋镖 则返回 `true`。

回旋镖 定义为一组三个点,这些点 各不相同 且 不在一条直线上 。

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

输出:true
```
示例 2:
```
输入:points = [[1,1],[2,2],[3,3]]

输出:false
```

提示:
* $points.length == 3$
* $points[i].length == 2$
* $0 <= x_i, y_i <= 100$

Solution:
### 计算几何

共三个点,分别使用两个点计算向量,随后判断向量叉积是否为 $0$。

代码:
```Java
class Solution {
    public boolean isBoomerang(int[][] ps) {
        return (ps[1][0] - ps[0][0]) * (ps[2][1] - ps[0][1]) != (ps[2][0] - ps[0][0]) * (ps[1][1] - ps[0][1]);
    }
}
```
* 时间复杂度:$O(1)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1031-1040/1038. 从二叉搜索树到更大和树（中等）.md

1038. 从二叉搜索树到更大和树

Question:
给定一个二叉搜索树 `root` (BST),请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。

提醒一下, 二叉搜索树满足下列约束条件:

* 节点的左子树仅包含键小于节点键的节点。
* 节点的右子树仅包含键大于节点键的节点。
* 左右子树也必须是二叉搜索树。

示例 1:

```
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]

输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
```
示例 2:
```
输入:root = [0,null,1]

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

提示:
* 树中的节点数在 $[1, 100]$ 范围内。
* $0 <= Node.val <= 100$
* 树中的所有值均不重复 。

Solution:
### 中序遍历

利用 **`BST` 的中序遍历是有序** 的特性,我们可以通过两次遍历 `BST` 来求解问题。

首先,通过一次遍历,计算出整棵树的节点总和 `tot`,然后在中序遍历过程中,不断对 `tot` 进行更新,将其作为当前未遍历到的节点的总和,用于给当前节点赋值。

假设当前遍历到的节点为 `x`(起始节点值为 `t`),那么将节点更新为当前节点 `tot` 后,更新 `tot = tot - t`。

这是常规的中序遍历做法,更进一步,如果将其中序遍历的顺序进行翻转(从「左中右」调整为「右中左」),则可实现一次遍历。

Java 代码:
```Java
class Solution {
    int tot = 0;
    public TreeNode bstToGst(TreeNode root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode root) {
        if (root == null) return ;
        dfs(root.right);
        tot += root.val;
        root.val = tot;
        dfs(root.left);
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int tot = 0;
    TreeNode* bstToGst(TreeNode* root) {
        dfs(root);
        return root;
    }
    void dfs(TreeNode* root) {
        if (root == nullptr) return;
        dfs(root->right);
        tot += root->val;
        root->val = tot;
        dfs(root->left);
    }
};
```
Python 代码:
```Python
class Solution:
    def bstToGst(self, root: TreeNode) -> TreeNode:
        tot = 0
        def dfs(root):
            nonlocal tot
            if not root: return
            dfs(root.right)
            tot += root.val
            root.val = tot
            dfs(root.left)
        dfs(root)
        return root
```
TypeScript 代码:
```TypeScript
function bstToGst(root: TreeNode | null): TreeNode | null {
    let tot = 0;
    const dfs = function(root: TreeNode | null): void {
        if (!root) return ;
        dfs(root.right);
        tot += root.val;
        root.val = tot;
        dfs(root.left);
    }
    dfs(root);
    return root;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1041-1050/1041. 困于环中的机器人（中等）.md

1041. 困于环中的机器人

Question:
在无限的平面上,机器人最初位于 $(0, 0)$ 处,面朝北方。注意:

* 北方向 是 `y` 轴的正方向。
* 南方向 是 `y` 轴的负方向。
* 东方向 是 `x` 轴的正方向。
* 西方向 是 `x` 轴的负方向。

机器人可以接受下列三条指令之一:

* `"G"`:直走 $1$ 个单位
* `"L"`:左转 $90$ 度
* `"R"`:右转 $90$ 度

机器人按顺序执行指令 `instructions`,并一直重复它们。

只有在平面中存在环使得机器人永远无法离开时,返回 `true`。否则,返回 `false`。

示例 1:
```
输入:instructions = "GGLLGG"

输出:true

解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“G”:移动一步。位置:(0,2).方向:北。
“L”:逆时针旋转90度。位置:(0,2).方向:西。
“L”:逆时针旋转90度。位置:(0,2)方向:南。
“G”:移动一步。位置:(0,1)方向:南。
“G”:移动一步。位置:(0,0)方向:南。
重复指令,机器人进入循环:(0,0)——>(0,1)——>(0,2)——>(0,1)——>(0,0)。
在此基础上,我们返回true。
```
示例 2:
```
输入:instructions = "GG"

输出:false

解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“G”:移动一步。位置:(0,2).方向:北。
重复这些指示,继续朝北前进,不会进入循环。
在此基础上,返回false。
```
示例 3:
```
输入:instructions = "GL"

输出:true

解释:机器人最初在(0,0)处,面向北方。
“G”:移动一步。位置:(0,1)方向:北。
“L”:逆时针旋转90度。位置:(0,1).方向:西。
“G”:移动一步。位置:(- 1,1)方向:西。
“L”:逆时针旋转90度。位置:(- 1,1)方向:南。
“G”:移动一步。位置:(- 1,0)方向:南。
“L”:逆时针旋转90度。位置:(- 1,0)方向:东方。
“G”:移动一步。位置:(0,0)方向:东方。
“L”:逆时针旋转90度。位置:(0,0)方向:北。
重复指令,机器人进入循环:(0,0)——>(0,1)——>(- 1,1)——>(- 1,0)——>(0,0)。
在此基础上,我们返回true。
```

提示:
* $1 <= instructions.length <= 100$
* `instructions[i]` 仅包含 `'G'`, `'L'`, `'R'`

Solution:
### 模拟

为了方便,将 `instructions` 记为 `s`,“北西南东”四个方向分别记为“上左下右”四个逆时针方向。

起始位置在 $(0,0)$,方向为上,我们可以将「位置 + 方向」统称为「状态」。

所谓“循环”,则是指执行若干次的 `s` 后,会回到相同的状态。

我们可以按 `s` 执行一遍,假设执行完所在位置为 $(x, y)$,所在位置为 $k$,先根据 **位置** 分情况讨论:

1. $(x, y)$ 为 $(0, 0)$,此时无论执行一遍后的方向为何值,必然能在若干次执行后回到起始状态。

   即只需要确保 `(n * k) % 4` 为 $0$ 即可,机器人会陷入循环;

2. $(x, y)$ 不为 $(0, 0)$,再根据 **方向** 进一步分情况讨论:

   * 方向为上:每执行一遍 `s` 指令,位置变化为 $(x, y)$,方向不变。

     那么执行 $n$ 遍后位置为 $(n \times x, n \times y)$,其中 $n$ 为正整数,并且 $x$ 和 $y$ 不同时为 $0$,因此随着执行次数增加,位置会离 $(0, 0)$ 越来越远,机器人不会陷入循环;

   * 方向为下:每执行一遍 `s` 指令,位置变化为 $(x, y)$,方向翻转。

     如果再执行一遍,由于再次执行时的方向与起始方向相反,因此位置变化为 $(-x, -y)$,同时方向再次翻转(与起始方向一致)。即执行偶数次后,会回到起始状态,机器人会陷入循环;

   * 方向为左:每执行一遍 `s` 指令,位置变化为 $(x, y)$,方向为逆时针 $90$ 度。

     如果执行第二次,位置变化为 $(-y, x)$,方向再逆时针 $90$ 度(与起始方向相反);执行第三次,位置变化为 $(-x, -y)$,方向再逆时针 $90$ 度(与起始方向呈顺时针 $90$ 度),该次变化会和首次执行相互抵消;执行第四次,位置变化为 $(y, -x)$,方向再逆时针 $90$ 度(与起始方向相同),该次变化与第二次执行相互抵消。总的位置变化为 $(0, 0)$,同时方向与起始方向一致,机器人会陷入循环;

   * 方向为右:与「方向为左」同理,机器人会陷入循环。

综上,只要执行一遍 `s` 后所在位置为 $(0, 0)$ 或方向不为上,均可确保循环发生。

Java 代码:
```Java
class Solution {
    public boolean isRobotBounded(String s) {
        int x = 0, y = 0, d = 0;
        int[][] dirs = new int[][]{{0,1}, {-1,0}, {0,-1}, {1,0}};
        for (char c : s.toCharArray()) {
            if (c == 'G') {
                x += dirs[d][0]; y += dirs[d][1];
            } else if (c == 'L') {
                d = (d + 1) % 4;
            } else {
                d = ((d - 1) % 4 + 4) % 4;
            }
        }
        return (x == 0 && y == 0) || d != 0;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool isRobotBounded(string s) {
        int x = 0, y = 0, d = 0;
        int dirs[4][2] = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}};
        for (char c : s) {
            if (c == 'G') {
                x += dirs[d][0]; y += dirs[d][1];
            } else if (c == 'L') {
                d = (d + 1) % 4;
            } else {
                d = ((d - 1) % 4 + 4) % 4;
            }
        }
        return (x == 0 && y == 0) || d != 0;
    }
};
```
Python 代码:
```Python
class Solution:
    def isRobotBounded(self, s: str) -> bool:
        x, y, d = 0, 0, 0
        dirs = [[0, 1], [-1, 0], [0, -1], [1, 0]]
        for c in s:
            if c == 'G':
                x += dirs[d][0]
                y += dirs[d][1]
            elif c == 'L':
                d = (d + 1) % 4
            else:
                d = ((d - 1) % 4 + 4) % 4
        return (x == 0 and y == 0) or d != 0
```
Go 代码:
```Go
func isRobotBounded(s string) bool {
    x, y, d := 0, 0, 0
    dirs := [][]int{{0, 1}, {-1, 0}, {0, -1}, {1, 0}}
    for _, c := range s {
        if c == 'G' {
            x += dirs[d][0]
            y += dirs[d][1]
        } else if c == 'L' {
            d = (d + 1) % 4
        } else {
            d = ((d - 1) % 4 + 4) % 4
        }
    }
    return (x == 0 && y == 0) || d != 0
}
```
TypeScript 代码:
```TypeScript
function isRobotBounded(s: string): boolean {
    let x = 0, y = 0, d = 0;
    const dirs: number[][] = [[0, 1], [-1, 0], [0, -1], [1, 0]];
    for (const c of s) {
        if (c === 'G') {
            x += dirs[d][0];
            y += dirs[d][1];
        } else if (c === 'L') {
            d = (d + 1) % 4;
        } else {
            d = ((d - 1) % 4 + 4) % 4;
        }
    }
    return (x === 0 && y === 0) || d !== 0;
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1044. 最长重复子串

Question:
给你一个字符串 `s` ,考虑其所有 重复子串 :即 `s` 的连续子串,在 `s` 中出现 $2$ 次或更多次。这些出现之间可能存在重叠。

返回 任意一个 可能具有最长长度的重复子串。如果 `s` 不含重复子串,那么答案为 `""` 。

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

输出:"ana"
```
示例 2:
```
输入:s = "abcd"

输出:""
```

提示:
* $2 <= s.length <= 3 * 10^4$
* `s` 由小写英文字母组成

Solution:
### 字符串哈希 + 二分

题目要求得「能取得最大长度的任一方案」,首先以「最大长度」为分割点的数轴具有「二段性」:

* 小于等于最大长度方案均存在(考虑在最大长度方案上做删减);
* 大于最大长度的方案不存在。

二分范围为 $[0, n]$,关键在于如何 `check` 函数,即实现「检查某个长度 $len$ 作为最大长度,是否存在合法方案」。

对于常规做法而言,可枚举每个位置作为起点,得到长度为 $len$ 的子串,同时使用 `Set<String>` 容器记录已被处理过子串有哪些,当容器中出现过当前子串,说明存在合法方案。

但是该做法实现的 `check` 并非线性,子串的生成和存入容器的时执行的哈希函数执行均和子串长度相关,复杂度是 $O(n * len)$。

我们可以通过「字符串哈希」进行优化,对「字符串哈希」不熟悉的同学可以看前置 🧀 [字符串哈希入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489813&idx=1&sn=7f3bc18ca390d85b17655f7164d8e660&chksm=fd9cb20acaeb3b1cc78abf05d6fea6d093098998ce877f799ac478247604bd267fbee6fcd989&token=1342991619&lang=zh_CN#rd)。

具体的,在二分前先通过 $O(n)$ 的复杂度预处理出哈希数组,从而确保能够在 `check` 时能够 $O(1)$ 得到某个子串的哈希值,最终将 `check` 的复杂度降为 $O(n)$。

代码:
```Java
class Solution {
    long[] h, p;
    public String longestDupSubstring(String s) {
        int P = 1313131, n = s.length();
        h = new long[n + 10]; p = new long[n + 10];
        p[0] = 1;
        for (int i = 0; i < n; i++) {
            p[i + 1] = p[i] * P;
            h[i + 1] = h[i] * P + s.charAt(i);
        }
        String ans = "";
        int l = 0, r = n;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            String t = check(s, mid);
            if (t.length() != 0) l = mid;
            else r = mid - 1;
            ans = t.length() > ans.length() ? t : ans;
        }
        return ans;
    }
    String check(String s, int len) {
        int n = s.length();
        Set<Long> set = new HashSet<>();
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            long cur = h[j] - h[i - 1] * p[j - i + 1];
            if (set.contains(cur)) return s.substring(i - 1, j);
            set.add(cur);
        }
        return "";
    }
}
```
* 时间复杂度:令 $n$ 为字符串 `s` 的长度,预处理出哈希数组的复杂度为 $O(n)$;二分最大长度的复杂度为 $O(n\log{n})$;整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

---

### 后缀数组

另外一个较为进阶的做法是使用「后缀数组」,后缀数组有基于基数排序的倍增实现,复杂度为 $O(n\log{n})$,也有基于 `DC3` 的 $O(n)$ 做法。

`DC3` 的做法已经写不出来了,写一个基于「基数排序」的倍增做法。

后缀数组包含几个核心数组:

* `sa` 数组:字典序排名为 $i$ 的后缀是第几个;
* `rk` 数组:第 $i$ 个后缀的排名是多少(不难发现 `rk` 与 `sa` 满足 $sa[rk[i]] = rk[sa[i]] = i$);
* `height` 数组:存储 $sa[i]$ 与 $sa[i - 1]$ 的 `LCP(最长公共前缀)` 为何值。

代码:
```Java
class Solution {
    int N = 30010;
    int[] x = new int[N], y = new int[N], c = new int[N];
    int[] sa = new int[N], rk = new int[N], height = new int[N];
    void swap(int[] a, int[] b) {
        int n = a.length;
        int[] c = a.clone();
        for (int i = 0; i < n; i++) a[i] = b[i];
        for (int i = 0; i < n; i++) b[i] = c[i];
    }
    public String longestDupSubstring(String s) {
        int n = s.length(), m = 128;
        s = " " + s;
        char[] cs = s.toCharArray();
        // sa:两遍基数排序,板子背不下来也可以直接使用 sort,复杂度退化到 n \log^2 n
        for (int i = 1; i <= n; i++) {
            x[i] = cs[i]; c[x[i]]++;
        }
        for (int i = 2; i <= m; i++) c[i] += c[i - 1];
        for (int i = n; i > 0; i--) sa[c[x[i]]--] = i;
        for (int k = 1; k <= n; k <<= 1) {
            int cur = 0;
            for (int i = n - k + 1; i <= n; i++) y[++cur] = i;
            for (int i = 1; i <= n; i ++) {
                if (sa[i] > k) y[++cur] = sa[i] - k;
            }
            for (int i = 1; i <= m; i++) c[i] = 0;
            for (int i = 1; i <= n; i++) c[x[i]]++;
            for (int i = 2; i <= m; i++) c[i] += c[i - 1];
            for (int i = n; i > 0; i--) {
                sa[c[x[y[i]]]--] = y[i];
                y[i] = 0;
            }
            swap(x, y);
            x[sa[1]] = cur = 1;
            for (int i = 2; i <= n; i++) {
                if (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = cur;
                else x[sa[i]] = ++cur;
            }
            if (cur == n) break;
            m = cur;
        }
        // height
        int k = 0;
        for (int i = 1; i <= n; i ++) rk[sa[i]] = i;
        for (int i = 1; i <= n; i++) {
            if (rk[i] == 1) continue;
            int j = sa[rk[i] - 1];
            k = k > 0 ? k - 1 : k;
            while (i + k <= n && j + k <= n && cs[i + k] == cs[j + k]) k++;
            height[rk[i]] = k;
        }
        int idx = -1, max = 0;
        for (int i = 1; i <= n; i++) {
            if (height[i] > max) {
                max = height[i];
                idx = sa[i];
            }
        }
        return max == 0 ? "" : s.substring(idx, idx + max);
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1041-1050/1047. 删除字符串中的所有相邻重复项（简单）.md

1047. 删除字符串中的所有相邻重复项

Question:
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:
```
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
```

提示:
* 1 <= S.length <= 20000
* S 仅由小写英文字母组成。

Solution:
### (自带) 栈解法

```java
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        Deque<Character> d = new ArrayDeque<>();
        for (char c : cs) {
            if (!d.isEmpty() && d.peekLast().equals(c)) {
                d.pollLast();
            } else {
                d.addLast(c);
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) sb.append(d.pollLast());
        sb.reverse();
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### (数组模拟) 栈解法

```java
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        char[] d = new char[s.length()];
        int hh = 0, tt = -1;
        for (char c : cs) {
            if (hh <= tt && d[tt] == c) {
                tt--;
            } else {
                d[++tt] = c;
            }
        }
        StringBuilder sb = new StringBuilder();
        while (hh <= tt) sb.append(d[tt--]);
        sb.reverse();
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### (自带) 双端队列解法

```java
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        Deque<Character> d = new ArrayDeque<>();
        for (char c : cs) {
            if (!d.isEmpty() && d.peekLast().equals(c)) {
                d.pollLast();
            } else {
                d.addLast(c);
            }
        }
        StringBuilder sb = new StringBuilder();
        while (!d.isEmpty()) sb.append(d.pollFirst());
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### (数组模拟) 双端队列解法

```java
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        char[] d = new char[s.length()];
        int hh = 0, tt = -1;
        for (char c : cs) {
            if (hh <= tt && d[tt] == c) {
                tt--;
            } else {
                d[++tt] = c;
            }
        }
        StringBuilder sb = new StringBuilder();
        while (hh <= tt) sb.append(d[hh++]);
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

***

### 纯数组解法

```java
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        char[] d = new char[s.length()];
        int hh = 0, tt = -1;
        for (char c : cs) {
            if (hh <= tt && d[tt] == c) {
                tt--;
            } else {
                d[++tt] = c;
            }
        }
        return new String(d, 0, tt + 1);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/1041-1050/1049. 最后一块石头的重量 II（中等）.md

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

Question:
有一堆石头,用整数数组 `stones` 表示。

其中 `stones[i]` 表示第 `i` 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 `x` 和 `y`,且 `x <= y`。那么粉碎的可能结果如下:
* 如果 `x == y`,那么两块石头都会被完全粉碎;
* 如果 `x != y`,那么重量为 `x` 的石头将会完全粉碎,而重量为 `y` 的石头新重量为 `y-x`。

最后,最多只会剩下一块 石头。返回此石头最小的可能重量 。

如果没有石头剩下,就返回 `0`。

示例 1:
```
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
```
示例 2:
```
输入:stones = [31,26,33,21,40]
输出:5
```
示例 3:
```
输入:stones = [1,2]
输出:1
```

提示:
* $1 <= stones.length <= 30$
* $1 <= stones[i] <= 100$

Solution:
### 基本分析

看到标题,心里咯噔了一下 🤣

一般性的石子合并问题通常是只能操作相邻的两个石子,要么是「区间 DP」要么是「四边形不等式」,怎么到 LeetCode 就成了中等难度的题目(也太卷了 🤣

仔细看了一下题目,可对任意石子进行操作,重放回的重量也不是操作石子的总和,而是操作石子的差值。

哦,那没事了 ~ 🤣

也是基于此启发,我们可以这样进行分析。

假设想要得到最优解,我们需要按照如下顺序操作石子:$[(sa, sb), (sc, sd), ... ,(si, sj), (sp, sq)]$。

其中 $abcdijpq$ 代表了石子编号,**字母顺序不代表编号的大小关系**。

**如果不考虑「有放回」的操作的话,我们可以划分为两个石子堆(正号堆/负号堆):**

* **将每次操作中「重量较大」的石子放到「正号堆」,代表在这次操作中该石子重量在「最终运算结果」中应用 $+$ 运算符**
* **将每次操作中「重量较少/相等」的石子放到「负号堆」,代表在这次操作中该石子重量在「最终运算结果」中应用 $-$ 运算符**

**这意味我们最终得到的结果,可以为原来 $stones$ 数组中的数字添加 $+/-$ 符号,所形成的「计算表达式」所表示。**

那有放回的石子重量如何考虑?

**其实所谓的「有放回」操作,只是触发调整「某个原有石子」所在「哪个堆」中,并不会真正意义上的产生「新的石子重量」。**

什么意思呢?

假设有起始石子 $a$ 和 $b$,且两者重量关系为 $a \geq b$,那么首先会将 $a$ 放入「正号堆」,将 $b$ 放入「负号堆」。重放回操作可以看作产生一个新的重量为 $a - b$ 的“虚拟石子”,将来这个“虚拟石子”也会参与某次合并操作,也会被添加 $+/-$ 符号:

* 当对“虚拟石子”添加 $+$ 符号,即可 $+(a - b)$,展开后为 $a - b$,即起始石子 $a$ 和 $b$ 所在「石子堆」不变
* 当对“虚拟石子”添加 $-$ 符号,即可 $-(a - b)$,展开后为 $b - a$,即起始石子 $a$ 和 $b$ 所在「石子堆」交换

**因此所谓不断「合并」&「重放」,本质只是在构造一个折叠的计算表达式,最终都能展开扁平化为非折叠的计算表达式。**

**综上,即使是包含「有放回」操作,最终的结果仍然可以使用「为原来 $stones$ 数组中的数字添加 $+/-$ 符号,形成的“计算表达式”」所表示。**

---

### 动态规划

有了上述分析后,问题转换为:**为 $stones$ 中的每个数字添加 $+/-$,使得形成的「计算表达式」结果绝对值最小。**

与[(题解)494. 目标和](https://leetcode-cn.com/problems/target-sum/solution/gong-shui-san-xie-yi-ti-si-jie-dfs-ji-yi-et5b/) 类似,需要考虑正负号两边时,其实只需要考虑一边就可以了,使用总和 $sum$ 减去决策出来的结果,就能得到另外一边的结果。

同时,由于想要「计算表达式」结果绝对值,因此我们需要将石子划分为差值最小的两个堆。

其实就是对「计算表达式」中带 $-$ 的数值提取公因数 $-1$,进一步转换为两堆石子相减总和,绝对值最小。

这就将问题彻底切换为 01 背包问题:**从 $stones$ 数组中选择,凑成总和不超过 $\frac{sum}{2}$ 的最大价值。**

其中「成本」&「价值」均为数值本身。

整理一下:

定义 $f[i][j]$ 代表考虑前 $i$ 个物品(数值),凑成总和不超过 $j$ 的最大价值。

每个物品都有「选」和「不选」两种决策,转移方程为:

$$f[i][j] = \max(f[i - 1][j], f[i - 1][j - stones[i - 1]] + stones[i - 1])$$

与完全背包不同,01 背包的几种空间优化是不存在时间复杂度上的优化,因此写成 朴素二维、滚动数组、一维优化 都可以。

建议直接上手写「一维空间优化」版本,是其他背包问题的基础。

代码:
```Java
class Solution {
    public int lastStoneWeightII(int[] ss) {
        int n = ss.length;
        int sum = 0;
        for (int i : ss) sum += i;
        int t = sum / 2;
        int[][] f = new int[n + 1][t + 1];
        for (int i = 1; i <= n; i++) {
            int x = ss[i - 1];
            for (int j = 0; j <= t; j++) {
                f[i][j] = f[i - 1][j];
                if (j >= x) f[i][j] = Math.max(f[i][j], f[i - 1][j - x] + x);
            }
        }
        return Math.abs(sum - f[n][t] - f[n][t]);
    }
}
```
```Java
class Solution {
    public int lastStoneWeightII(int[] ss) {
        int n = ss.length;
        int sum = 0;
        for (int i : ss) sum += i;
        int t = sum / 2;
        int[][] f = new int[2][t + 1];
        for (int i = 1; i <= n; i++) {
            int x = ss[i - 1];
            int a = i & 1, b = (i - 1) & 1;
            for (int j = 0; j <= t; j++) {
                f[a][j] = f[b][j];
                if (j >= x) f[a][j] = Math.max(f[a][j], f[b][j - x] + x);
            }
        }
        return Math.abs(sum - f[n&1][t] - f[n&1][t]);
    }
}
```
```Java
class Solution {
    public int lastStoneWeightII(int[] ss) {
        int n = ss.length;
        int sum = 0;
        for (int i : ss) sum += i;
        int t = sum / 2;
        int[] f = new int[t + 1];
        for (int i = 1; i <= n; i++) {
            int x = ss[i - 1];
            for (int j = t; j >= x; j--) {
                f[j] = Math.max(f[j], f[j - x] + x);
            }
        }
        return Math.abs(sum - f[t] - f[t]);
    }
}
```
* 时间复杂度:$O(n \times \sum_{i = 0}^{n - 1} stones[i])$
* 空间复杂度:$O(n \times \sum_{i = 0}^{n - 1} stones[i])$

### 最后

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

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

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

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


Question:
学校打算为全体学生拍一张年度纪念照。根据要求,学生需要按照 非递减 的高度顺序排成一行。

排序后的高度情况用整数数组 `expected` 表示,其中 `expected[i]` 是预计排在这一行中第 `i` 位的学生的高度(下标从 $0$ 开始)。

给你一个整数数组 `heights`,表示 当前学生站位 的高度情况。`heights[i]` 是这一行中第 `i` 位学生的高度(下标从 $0$ 开始)。

返回满足 `heights[i] != expected[i]` 的 下标数量 。

示例:
```
输入:heights = [1,1,4,2,1,3]

输出:3

解释:
高度:[1,1,4,2,1,3]
预期:[1,1,1,2,3,4]
下标 2 、4 、5 处的学生高度不匹配。
```
示例 2:
```
输入:heights = [5,1,2,3,4]

输出:5

解释:
高度:[5,1,2,3,4]
预期:[1,2,3,4,5]
所有下标的对应学生高度都不匹配。
```
示例 3:
```
输入:heights = [1,2,3,4,5]

输出:0

解释:
高度:[1,2,3,4,5]
预期:[1,2,3,4,5]
所有下标的对应学生高度都匹配。
```

提示:
* $1 <= heights.length <= 100$
* $1 <= heights[i] <= 100$

Solution:
### 排序

先排序得到目标序列,再将目标序列和原序列进行逐一对比,得到答案。

排序部分,可以使用复杂度为 $O(n\log{n})$ 的双轴快排,也可以利用数据范围为 $100$,利用计数排序思想来构建目标序列,复杂度为 $O(C + n)$。

代码(快排 $P1$,计数排序 $P2$):
```Java
class Solution {
    public int heightChecker(int[] heights) {
        int[] t = heights.clone();
        Arrays.sort(t);
        int n = heights.length, ans = 0;
        for (int i = 0; i < n; i++) {
            if (t[i] != heights[i]) ans++;
        }
        return ans;
    }
}
```

-

```Java
class Solution {
    public int heightChecker(int[] heights) {
        int[] cnts = new int[110];
        for (int i : heights) cnts[i]++;
        int n = heights.length, ans = 0;
        int[] t = new int[n];
        for (int i = 0, j = 0; i < 110; i++) {
            while (cnts[i]-- > 0) t[j++] = i;
        }
        for (int i = 0; i < n; i++) {
            if (t[i] != heights[i]) ans++;
        }
        return ans;
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$(快排)或者为 $O(C + n)$(计数排序),统计答案复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$(快排)或 $O(C + n)$(计数排序)
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


LeetCode/1051-1060/1052. 爱生气的书店老板（中等）.md

1052. 爱生气的书店老板

Question:
今天,书店老板有一家店打算试营业 $customers.length$ 分钟。每分钟都有一些顾客($customers[i]$)会进入书店,所有这些顾客都会在那一分钟结束后离开。

在某些时候,书店老板会生气。 如果书店老板在第 $i$ 分钟生气,那么 $grumpy[i] = 1$,否则 $grumpy[i] = 0$。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。

书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 $X$ 分钟不生气,但却只能使用一次。

请你返回这一天营业下来,最多有多少客户能够感到满意的数量。

示例:
```
输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3

输出:16

解释:
书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.
```

提示:
* $1 <= X <= customers.length == grumpy.length <= 20000$
* $0 <= customers[i] <= 1000$
* $0 <= grumpy[i] <= 1$

Solution:
### 滑动窗口

由于「技巧」只会将情绪将「生气」变为「不生气」,不生气仍然是不生气。

1. 我们可以先将原本就满意的客户加入答案,同时将对应的 $customers[i]$ 变为 $0$。

2. 之后的问题转化为:在 $customers$ 中找到连续一段长度为 $minutes$ 的子数组,使得其总和最大。这部分就是我们应用技巧所得到的客户。

代码:
```Java
class Solution {
    public int maxSatisfied(int[] customers, int[] grumpy, int minutes) {
        int n = customers.length, ans = 0;
        for (int i = 0; i < n; i++) {
            if (grumpy[i] == 0) {
                ans += customers[i];
                customers[i] = 0;
            }
        }
        int cur = 0, max = 0;
        for (int i = 0; i < n; i++) {
            cur += customers[i];
            if (i >= minutes) cur -= customers[i - minutes];
            max = Math.max(max, cur);
        }
        return ans + max;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/1071-1080/1074. 元素和为目标值的子矩阵数量（困难）.md

1074. 元素和为目标值的子矩阵数量

Question:
给出矩阵 matrix 和目标值 target,返回元素总和等于目标值的非空子矩阵的数量。

子矩阵 x1, y1, x2, y2 是满足 x1 <= x <= x2 且 y1 <= y <= y2 的所有单元 matrix[x][y] 的集合。

如果 (x1, y1, x2, y2) 和 (x1', y1', x2', y2') 两个子矩阵中部分坐标不同(如:x1 != x1'),那么这两个子矩阵也不同。

示例 1:

```
输入:matrix = [[0,1,0],[1,1,1],[0,1,0]], target = 0
输出:4
解释:四个只含 0 的 1x1 子矩阵。
```
示例 2:
```
输入:matrix = [[1,-1],[-1,1]], target = 0
输出:5
解释:两个 1x2 子矩阵,加上两个 2x1 子矩阵,再加上一个 2x2 子矩阵。
```
示例 3:
```
输入:matrix = [[904]], target = 0
输出:0
```

提示:
* 1 <= matrix.length <= 100
* 1 <= matrix[0].length <= 100
* -1000 <= matrix[i] <= 1000
* -$10^8$ <= target <= $10^8$

Solution:
### 朴素二维前缀和

从题面来看显然是一道「二维前缀和」的题目,如果你还不了解「二维前缀和」,可以看看 [(题解)304. 二维区域和检索 - 矩阵不可变](https://leetcode-cn.com/problems/range-sum-query-2d-immutable/solution/xia-ci-ru-he-zai-30-miao-nei-zuo-chu-lai-ptlo/)。本题预处理前缀和的复杂度为 $O(m * n)$。

搜索所有子矩阵需要枚举「矩形左上角」和「矩形右下角」,复杂度是 $O(m^2 * n^2)$。

因此,如果把本题当做二维前缀和模板题来做的话,整体复杂度是 $O(m^2 * n^2)$。

数据范围是 $10^2$,对应的计算量是 $10^8$,处于超时边缘,但当我们枚举「矩阵右下角」$(i,j)$ 的时候,我们只需要搜索位于 $(i,j)$ 的左上方的点 $(p,q)$ 作为「矩阵左上角」,所以其实我们是取不满 $m^2 * n^2$ 的,但仍然具有超时风险(2021/05/29 Java 测试可通过)。

代码:
```Java
class Solution {
    public int numSubmatrixSumTarget(int[][] mat, int t) {
        int n = mat.length, m = mat[0].length;
        int[][] sum = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                for (int p = 1; p <= i; p++) {
                    for (int q = 1; q <= j; q++) {
                        if (sum[i][j] - sum[p - 1][j] - sum[i][q - 1] + sum[p - 1][q - 1] == t) ans++;
                    }
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m^2 * n^2)$
* 空间复杂度:$O(m * n)$

---

### 优化枚举 + 哈希表

上述分析是从「点」上来确定一个子矩阵,在 $n$ 和 $m$ 同阶的情况下,复杂度是 $O(n^4)$ 的。

事实上,我们能从「边」的角度来确定一个子矩阵,通过枚举三条边,然后加速找第四条边的过程,这样可以将复杂度降到 $O(n^3)$。

**这个分析思路在 [(题解)363. 矩形区域不超过 K 的最大数值和](https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/solution/gong-shui-san-xie-you-hua-mei-ju-de-ji-b-dh8s/) 详细讲过,没有印象的同学可以翻翻。这道题一定程度上是那道题的简化版,在本题我们只需要找到矩阵和为 $target$ 的值,因此只需要使用「哈希表」来记录出现过的值即可,而在 [(原题)363. 矩形区域不超过 K 的最大数值和](https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/) 中,我们需要找到和不超过 $k$ 的最大子矩阵,因此还涉及「有序集合 + 二分」。**

具体的,我们仍然需要先预处理出一个二维前缀和。**然后通过枚举确定「子矩阵的上下边界」,在将「子矩阵右边界」不断右移的过程中,把「子矩阵右边界」到「原矩阵左边界」行程的矩阵和存入哈希表(因为要统计数量,存储格式为 {"面积”:"出现次数"} ),然后通过容斥原理来找到目标的「子矩阵左边界」。**

假设当前我们「子矩阵的上下边界」已经固定,当枚举到某个「子矩阵右边界」时,我们先通过二维前缀和计算出「子矩阵右边界」和「原矩阵左边界」形成的矩阵和 $cur$,由于我们希望找到矩阵和为 $target$ 的子矩阵,即希望找到一个「子矩阵左边界」使得矩阵和满足要求,这等价于从「哈希表」中找到一个 $x$,使得 $cur - x = target$,这是一个 $O(1)$ 操作。

代码:
```Java
class Solution {
    public int numSubmatrixSumTarget(int[][] mat, int t) {
        int n = mat.length, m = mat[0].length;
        int[][] sum = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = 0;
        for (int top = 1; top <= n; top++) {
            for (int bot = top; bot <= n; bot++) {
                int cur = 0;
                Map<Integer, Integer> map = new HashMap<>();
                for (int r = 1; r <= m; r++) {
                    cur = sum[bot][r] - sum[top - 1][r];
                    if (cur == t) ans++;
                    if (map.containsKey(cur - t)) ans += map.get(cur - t);
                    map.put(cur, map.getOrDefault(cur, 0) + 1);
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(m * n^2)$
* 空间复杂度:$O(m * n)$

---

### 进阶

1. 【时间复杂度】在上述解法中,我们采用的是**固定上下边界,移动右边界,通过哈希表找左边界**的做法,复杂度为 $O(m * n^2)$;自然也能改为**固定左右边界,移动下边界,通过哈希表找上边界**做法,复杂度为 $O(m^2 * n)$。那么我们要如何调整代码,才能最大化「哈希表」带来的优化效果呢?此时的复杂度又是多少?

2. 【空间复杂度】我们空间复杂度的瓶颈在「二维前缀和」上,但注意到无论我们采取「固定上下边界」还是「固定左右边界」的做法,扫描原矩阵的方向都是固定的,那么是否可以不预处理「二维前缀和」呢?从而将空间复杂度降低到 $O(\max(n, m))$ 呢?

上述两问,你都可以在 [(题解)363. 矩形区域不超过 K 的最大数值和](https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/solution/gong-shui-san-xie-you-hua-mei-ju-de-ji-b-dh8s/) 中找到答案。

### 最后

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

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

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

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


Question:
给出第一个词 `first` 和第二个词 `second`,考虑在某些文本 `text` 中可能以 `"first second third"` 形式出现的情况,其中 `second` 紧随 `first` 出现,`third` 紧随 `second` 出现。

对于每种这样的情况,将第三个词 `"third"` 添加到答案中,并返回答案。

示例 1:
```
输入:text = "alice is a good girl she is a good student", first = "a", second = "good"

输出:["girl","student"]
```
示例 2:
```
输入:text = "we will we will rock you", first = "we", second = "will"

输出:["we","rock"]
```

提示:
* $1 <= text.length <= 1000$
* `text` 由小写英文字母和空格组成
* `text` 中的所有单词之间都由 单个空格字符 分隔
* $1 <= first.length, second.length <= 10$
* `first` 和 `second` 由小写英文字母组成

Solution:
### 模拟

好像每次起晚都是简单题?🤣

根据题意进行模拟。

代码:
```Java
class Solution {
    public String[] findOcurrences(String text, String a, String b) {
        String[] ss = text.split(" ");
        int n = ss.length;
        List<String> list = new ArrayList<>();
        for (int i = 0; i + 2 < n; i++) {
            if (ss[i].equals(a) && ss[i + 1].equals(b)) list.add(ss[i + 2]);
        }
        return list.toArray(new String[list.size()]);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个长度固定的整数数组 `arr`,请你将该数组中出现的每个零都复写一遍,并将其余的元素向右平移。

注意:请不要在超过该数组长度的位置写入元素。

要求:请对输入的数组 就地 进行上述修改,不要从函数返回任何东西。

示例 1:
```
输入:[1,0,2,3,0,4,5,0]

输出:null

解释:调用函数后,输入的数组将被修改为:[1,0,0,2,3,0,0,4]
```
示例 2:
```
输入:[1,2,3]

输出:null

解释:调用函数后,输入的数组将被修改为:[1,2,3]
```

提示:
* $1 <= arr.length <= 10000$
* $0 <= arr[i] <= 9$

Solution:
### 双指针

这道题挺有意思的,建议大家在看文字的同时,在纸上推导一遍。

我们可以假设已经通过原数组 `arr` 处理出最终的目标数组 `ans`,起始使用指针 $i$ 指向原数组的开头位置,使用指针 $j$ 指向目标数组的开头位置。

然后开始同时从前往后处理,默认情况下,两个指针每次往后移动一位,当在原数组中出现 $arr[i] = 0$ 时,根据题意,我们需要在目标数组 $ans[j]$ 后面复写多一位,因此此时让 $j$ 多走一位。

当 $j$ 走到的位置已超出数组长度,此时 $i$ 也停在了该被截断的位置的下一位。

此时我们先将 $i$ 和 $j$ 同时往回走一步,确保 $i$ 落在真正的截断后的位置,但需要注意,由于 $j$ 在之前的往前走逻辑中可能会走两步,因此 $j$ 在往回走一步后仍可能在数组以外的位置。

然后考虑如何在 $i$ 和 $j$ 所在位置开始「往回」赋值:每次将 $arr[i]$ 赋值给 $arr[j]$(需要确保 $j$ 下标合法),由于 $i$ 必然不会出现在 $j$ 的后面(即不会出现 $j < i$),因此不会出现值被提前覆盖的问题。因此通常情况下,我们可以不断的同时减少 $i$ 和 $j$,并将 $arr[i]$ 赋值给 $arr[j]$。当出现 $arr[i] = 0$ 时,我们需要额外让 $j$ 往前多走一步,并将该位置置为 $0$,来实现目标数组「复写零」的效果。

当 $i$ 指针越过开头的位置,说明已完成从 `arr` 到 `ans` 的转换。

> 这个往回赋值的过程,十分建议大家在纸上推导一遍。

代码:
```Java
class Solution {
    public void duplicateZeros(int[] arr) {
        int n = arr.length, i = 0, j = 0;
        while (j < n) {
            if (arr[i] == 0) j++;
            i++; j++;
        }
        i--; j--;
        while (i >= 0) {
            if (j < n) arr[j] = arr[i];
            if (arr[i] == 0 && --j >= 0) arr[j] = 0;
            i--; j--;
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1092. 最短公共超序列

Question:
给出两个字符串 `str1` 和 `str2`,返回同时以 `str1` 和 `str2` 作为子序列的最短字符串。如果答案不止一个,则可以返回满足条件的任意一个答案。

(如果从字符串 `T` 中删除一些字符(也可能不删除,并且选出的这些字符可以位于 `T` 中的 任意位置),可以得到字符串 `S`,那么 `S` 就是 `T` 的子序列)

示例:
```
输入:str1 = "abac", str2 = "cab"

输出:"cabac"

解释:
str1 = "abac" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 的第一个 "c"得到 "abac"。
str2 = "cab" 是 "cabac" 的一个子串,因为我们可以删去 "cabac" 末尾的 "ac" 得到 "cab"。
最终我们给出的答案是满足上述属性的最短字符串。
```

提示:
* $1 <= str1.length, str2.length <= 1000$
* `str1` 和 `str2` 都由小写英文字母组成。

Solution:
### LCS 求具体方案 + 构造

为了方便,我们令 `str1` 为 `s1`,`str2` 为 `s2`,并将两者长度记为 $n$ 和 $m$。

容易想到最终的方案必然是由三部分组成:两字符串的公共子序列(且必然是最长公共子序列)+ 两者特有的字符部分。

基于此,我们可以先使用对两者求 `LCS`,并在求具体方案时遵循:属于最长公共子序列的字符只添加一次,而特有于 `s1` 或 `s2` 的字符则独自添加一次。

求解 `LCS` 部分我们定义 **$f[i][j]$ 代表考虑 $s1$ 的前 $i$ 个字符、考虑 $s2$ 的前 $j$ 的字符,形成的最长公共子序列长度(为了方便,令下标从 $1$ 开始)。**

当有了「状态定义」之后,基本上「转移方程」就是呼之欲出:

* `s1[i]==s2[j]` : $f[i][j]=f[i-1][j-1]+1$。代表**必然使用 $s1[i]$ 与 $s2[j]$ 时** `LCS` 的长度。
* `s1[i]!=s2[j]` : $f[i][j]=max(f[i-1][j], f[i][j-1])$。代表**必然不使用 $s1[i]$(但可能使用$s2[j]$)时** 和 **必然不使用 $s2[j]$(但可能使用$s1[i]$)时** `LCS` 的长度。

> **不了解 LCS 的同学可以看前置 🧀 : [LCS 模板题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247492097&idx=1&sn=f51f29d86df809d8ac43a40a1369b3d6)**

当预处理出动规数组 `f` 之后,我们使用「双指针」和「通用 `DP` 求具体方案」的做法进行构造:使用 `i` 变量指向 `s1` 的尾部(即起始有 $i = n$),使用 `j` 变量指向 `s2` 的尾部(即起始有 $j = m$),根据 `i` 和 `j` 当前所在位置以及 $f[i][j]$ 从何值转移而来:

1. 若 `i` 或 `j` 其一走完(`i = 0` 或 `j = 0`),将剩余字符追加到答案中;
2. 当 $f[i][j] = f[i - 1][j - 1] + 1$ 且 $s1[i] = s2[j]$ 时(可简化为 $s1[i] = s2[j]$ 判断),此时 `i` 指向的字符和 `j` 指向的字符为相同,且为 `LCS` 中的字符,将其追加到具体方案,并让 `i` 和 `j` 同时后移;
3. 当 $f[i][j] = f[i - 1][j]$,将 `s1[i]` 追加到答案中,令 `i` 后移;
4. 当 $f[i][j] = f[i][j - 1]$,将 `s2[j]` 追加到答案中,令 `j` 后移。

当条件 `3` 和 `4` 同时满足时,由于只需要输出任一具体方案,我们任取其一即可。

最后,由于我们是从后往前进行构造,在返回时需要再进行一次翻转。

Java 代码:
```Java
class Solution {
    public String shortestCommonSupersequence(String str1, String str2) {
        int n = str1.length(), m = str2.length();
        str1 = " " + str1; str2 = " " + str2;
        char[] s1 = str1.toCharArray(), s2 = str2.toCharArray();
        int[][] f = new int[n + 10][m + 10];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1;
                else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);
            }
        }
        StringBuilder sb = new StringBuilder();
        int i = n, j = m;
        while (i > 0 || j > 0) {
            if (i == 0) sb.append(s2[j--]);
            else if (j == 0) sb.append(s1[i--]);
            else {
                if (s1[i] == s2[j]) {
                    sb.append(s1[i]);
                    i--; j--;
                } else if (f[i][j] == f[i - 1][j]) {
                    sb.append(s1[i--]);
                } else {
                    sb.append(s2[j--]);
                }
            }
        }
        return sb.reverse().toString();
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string shortestCommonSupersequence(string str1, string str2) {
        int n = str1.size(), m = str2.size();
        str1 = " " + str1; str2 = " " + str2;
        vector<vector<int>> f(n + 10, vector<int>(m + 10));
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= m; ++j) {
                if (str1[i] == str2[j])
                    f[i][j] = f[i - 1][j - 1] + 1;
                else
                    f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            }
        }
        string res;
        int i = n, j = m;
        while (i > 0 || j > 0) {
            if (i == 0) res += str2[j--];
            else if (j == 0) res += str1[i--];
            else {
                if (str1[i] == str2[j]) {
                    res += str1[i];
                    i--; j--;
                }
                else if (f[i][j] == f[i - 1][j]) res += str1[i--];
                else res += str2[j--];
            }
        }
        reverse(res.begin(), res.end());
        return res;
    }
};
```
Python 代码:
```Python
class Solution:
    def shortestCommonSupersequence(self, s1: str, s2: str) -> str:
        n, m = len(s1), len(s2)
        s1 = " " + s1
        s2 = " " + s2
        f = [[0] * (m + 10) for _ in range(n + 10)]
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                f[i][j] = f[i - 1][j - 1] + 1 if s1[i] == s2[j] else max(f[i - 1][j], f[i][j - 1])
        ans = ""
        i, j = n, m
        while i > 0 or j > 0:
            if i == 0:
                ans += s2[j]
                j -= 1
            elif j == 0:
                ans += s1[i]
                i -= 1
            else:
                if s1[i] == s2[j]:
                    ans += s1[i]
                    i -= 1
                    j -= 1
                elif f[i][j] == f[i - 1][j]:
                    ans += s1[i]
                    i -= 1
                else:
                    ans += s2[j]
                    j -= 1
        return ans[::-1]
```
TypeScript 代码:
```TypeScript
function shortestCommonSupersequence(s1: string, s2: string): string {
    const n = s1.length, m = s2.length
    s1 = " " + s1; s2 = " " + s2
    const f = new Array<Array<number>>()
    for (let i = 0; i < n + 10; i++) f.push(new Array<number>(m + 10).fill(0))
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= m; j++) {
            if (s1[i] == s2[j]) f[i][j] = f[i - 1][j - 1] + 1
            else f[i][j] = Math.max(f[i - 1][j], f[i][j - 1])
        }
    }
    let ans = ""
    let i = n, j = m
    while (i > 0 || j > 0) {
        if (i == 0) ans += s2[j--]
        else if (j == 0) ans += s1[i--]
        else {
            if (s1[i] == s2[j]) {
                ans += s1[i]
                i--; j--
            } else if (f[i][j] == f[i - 1][j]) {
                ans += s1[i--]
            } else {
                ans += s2[j--]
            }
        }
    }
    return ans.split('').reverse().join('')
};
```
* 时间复杂度:`LCS` 复杂度为 $O(n \times m)$;构造答案复杂度为 $O(n \times m)$。整体复杂度为 $O(n \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


Question:
车上最初有 `capacity` 个空座位,车只能向一个方向行驶(不允许掉头或改变方向)。

给定整数 `capacity` 和一个数组 `trips`,  $trip[i] = [numPassengers_{i}, from_{i}, to_{i}]$ 表示第 `i` 次旅行有 $numPassengers_{i}$ 乘客,接他们和放他们的位置分别是 $from_{i}$ 和 $to_{i}$ 。

这些位置是从汽车的初始位置向东的公里数。

当且仅当你可以在所有给定的行程中接送所有乘客时,返回 `true`,否则请返回 `false`。

示例 1:
```
输入:trips = [[2,1,5],[3,3,7]], capacity = 4

输出:false
```
示例 2:
```
输入:trips = [[2,1,5],[3,3,7]], capacity = 5

输出:true
```

提示:
* $1 <= trips.length <= 1000$
* $trips[i].length = 3$
* $1 <= numPassengers_{i} <= 100$
* $0 <= from_{i} < to_{i} <= 1000$
* $1 <= capacity <= 10^5$

Solution:
### 差分

从朴素的想法开始:创建一个数组 `cnt`,用于存储从某个站点出发时,车上的乘客数量。

例如 $cnt[x] = c$ 含义为在站点 $x$ 出发时(在该站点的下车和上车均完成),车上乘客数为 $c$ 个。

对于每个 $trips[i] = (c, a, b)$,我们需要对 $[a, b)$ 范围内的 $cnt[j]$ 进行加 $c$ 操作。

处理完 `trips` 后,检查所有站点的乘客人数,根据是否满足 `capacity` 限制返回答案。

因此,这是一个关于「区间修改,单点查询」的经典问题,可使用「差分」求解。

所谓“差分”,是指 **原数组中每个元素与前一元素之差所形成的数组**,与之相对应的是“前缀和”。

我们知道,对原数组进行诸位累加(前缀计算操作),所得到的数组为前缀和数组。差分数组,则是对其执行前缀计算后,能够得到原数组的那个数组 🤣 。

关于「差分数组 - 原数组 - 前缀和数组」三者关系如图所示:

前缀和数组的主要作用,是利用「容斥原理」快速求解某段之和。例如要查询原数组 `nums` 中下标范围 $[l, r]$ 的和,可通过 $sum[r] - sum[l - 1]$ 快速求解。

差分数组的主要作用,是帮助快速修改某段区间。

由于差分数组执行「前缀计算」后得到的是原数组,因此在差分数组上修改某个值,会对原数组某段后缀产生相同的影响。

因此,**当我们想要对原数组的 $[l, r]$ 进行整体修改时,只需要对差分数组的 $l$ 和 $r + 1$ 位置执行相应操作即可**。

举个 🌰,假设想对原数组 `nums` 的 $[l, r]$ 进行整体“加一”操作,那么可转换为对差分数组 `c[l]` 的加一操作(等价对原数组的 $[l, n - 1]$ 进行加一),以及对差分数组 `c[r + 1]` 的减一操作(等价于对原数组的 $[r + 1, n - 1]$ 进行减一,最终只有 $[l, r]$ 有加一效果)。

至此,我们完成了对「差分」的基本学习:**将原数组的区间修改等价为差分数组的特定位置修改**。

回到本题,起始先用 `nums` 来作为差分数组,对于 $trips[i] = (c, a, b)$,有 $c$ 个乘客在 $a$ 点上车,在 $b$ 点下车,因此对 $[a, b)$ 进行整体加 $c$ 操作,对应差分数组操作 `nums[a] += c; nums[b] -= c`。

处理完 `trips` 后,对差分数组 `nums` 进行前缀计算(可直接复用 `nums`,进行原地计算),便可得到各个站点的乘客数量,与 `capacity` 比较得出答案。

一些细节:为了方便,人为规定站点编号从 $1$ 开始。

Java 代码:

```Java
class Solution {
    public boolean carPooling(int[][] trips, int capacity) {
        int[] nums = new int[1010];
        for (int[] t : trips) {
            int c = t[0], a = t[1], b = t[2];
            nums[a + 1] += c; nums[b + 1] -= c;
        }
        for (int i = 1; i <= 1000; i++) {
            nums[i] += nums[i - 1];
            if (nums[i] > capacity) return false;
        }
        return true;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        vector<int> nums(1010, 0);
        for (const auto& t : trips) {
            int c = t[0], a = t[1], b = t[2];
            nums[a + 1] += c; nums[b + 1] -= c;
        }
        for (int i = 1; i <= 1000; i++) {
            nums[i] += nums[i - 1];
            if (nums[i] > capacity) return false;
        }
        return true;
    }
};
```
Python 代码:
```Python
class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        nums = [0] * 1010
        for t in trips:
            c, a, b = t[0], t[1], t[2]
            nums[a + 1] += c
            nums[b + 1] -= c
        for i in range(1, 1001):
            nums[i] += nums[i - 1]
            if nums[i] > capacity: return False
        return True
```
TypeScript 代码:
```TypeScript
function carPooling(trips: number[][], capacity: number): boolean {
    const nums = new Array(1010).fill(0);
    for (const t of trips) {
        const c = t[0], a = t[1], b = t[2];
        nums[a + 1] += c; nums[b + 1] -= c;
    }
    for (let i = 1; i <= 1000; i++) {
        nums[i] += nums[i - 1];
        if (nums[i] > capacity) return false;
    }
    return true;
};
```
* 时间复杂度:$O(n + m)$,其中 $n$ 为数组 `trips` 大小;$m = 1000$ 为位置值域大小
* 空间复杂度:$O(m)$

### 最后

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

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

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

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


Question:
给你 $n$ 个非负整数 $a_1$,$a_2$,...,$a_n$,每个数代表坐标中的一个点 $(i, a_i)$ 。

在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 $(i, a_i)$ 和 $(i, 0)$ 。

找出其中的两条线,使得它们与 `x` 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器。

示例 1:

```
输入:[1,8,6,2,5,4,8,3,7]

输出:49

解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
```
示例 2:
```
输入:height = [1,1]

输出:1
```
示例 3:
```
输入:height = [4,3,2,1,4]

输出:16
```
示例 4:
```
输入:height = [1,2,1]

输出:2
```

提示:
* $n = height.length$
* $2 <= n <= 3 \times 10^4$
* $0 <= height[i] <= 3 \times 10^4$

Solution:
### 双指针 + 贪心

先用两个指针 `i` 和 `j` 指向左右边界,然后考虑指针应该怎么移动。

由于构成矩形的面积,取决于 `i` 和 `j` 之间的距离(记为 `w`) 和 `i` 和 `j` 下标对应的高度的最小值(记为 `h`)。

首先无论是 `i` 指针往右移动还是 `j` 指针往左移动都会导致 `w` 变小,所以想要能够枚举到更大的面积,我们应该让 `h` 在指针移动后变大。

不妨假设当前情况是 `height[i] < heigth[j]`(此时矩形的高度为 `height[i]`),然后分情况讨论:

* 让 `i` 和 `j` 两者高度小的指针移动,即 `i` 往右移动:
  * 移动后,i 指针对应的高度变小,即 `height[i] > height[i + 1]`:`w` 和 `h` 都变小了,面积一定变小
  * 移动后,i 指针对应的高度不变,即 `height[i] = height[i + 1]`:`w` 变小,`h` 不变,面积一定变小
  * 移动后,i 指针对应的高度变大,即 `height[i] < height[i + 1]`:`w` 变小,`h` 变大,面积可能会变大

* 让 `i` 和 `j` 两者高度大的指针移动,即 `j` 往左移动:
   * 移动后,j 指针对应的高度变小,即 `height[j] > height[j - 1]`:`w` 变小,`h` 可能不变或者变小(当 `height[j - 1] >= height[i]` 时,`h` 不变;当 `height[j - 1] < height[i]` 时,`h` 变小),面积一定变小
    * 移动后,j 指针对应的高度不变,即 `height[j] = height[j - 1]`:`w` 变小,`h` 不变,面积一定变小
    * 移动后,j 指针对应的高度变大,即 `height[j] < height[j - 1]`:`w` 变小,`h` 不变,面积一定变小

综上所述,我们只有将高度小的指针往内移动,才会枚举到更大的面积:

代码:
```Java
class Solution {
    public int maxArea(int[] height) {
        int n = height.length, ans = 0;
        int i = 0, j = n - 1;
        while (i < j) {
            ans = Math.max(ans, (j - i) * Math.min(height[i], height[j]));
            if (height[i] < height[j]) i++;
            else j--;
        }
        return ans;
    }
}
```
* 时间复杂度:会对整个数组扫描一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
```
字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000
```
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

* I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
* X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
* C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个整数,将其转为罗马数字。输入确保在 1 到 3999 的范围内。

示例 1:
```
输入: 3
输出: "III"
```
示例 2:
```
输入: 4
输出: "IV"
```
示例 3:
```
输入: 9
输出: "IX"
```
示例 4:
```
输入: 58
输出: "LVIII"
解释: L = 50, V = 5, III = 3.
```
示例 5:
```
输入: 1994
输出: "MCMXCIV"
解释: M = 1000, CM = 900, XC = 90, IV = 4.
```

提示:
* $1 <= num <= 3999$

Solution:
### 模拟

现实中每一个「阿拉伯数字」固定对应一个「罗马数字」,不存在说一个「阿拉伯数字」能够对应出一个较长或者较短的「罗马数字」。

因此这是一个按照固定规则进行转换的「模拟」过程。

根据题意,我们可以列举出有限个罗马字符和其对应的数值。

然后从左到右构建罗马数字,优先构建数值高的罗马字符(如果有足够的分值,尽量尝试构建 "M",直到分值不够,再尽量尝试构建 "CM" ... )

代码:
```Java
class Solution {
    int[] val = new int[]{1000,900,500,400,100,90,50,40,10,9,5,4,1};
    String[] str = new String[]{"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
    public String intToRoman(int x) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < val.length && x > 0; i++) {
            int cv = val[i];
            String cs = str[i];
            while (x >= cv) {
                sb.append(cs);
                x -= cv;
            }
        }
        return sb.toString();
    }
}
```
* 时间复杂度:通过映射对应关系,我们发现一位「阿拉伯数字」(数值 0-9)最多用 4 个字母表示(8 = VIII),因此「罗马数字」的长度最多不超过「阿拉伯数字」长度的 $4$ 倍(同一数量级内)。对应输入值 $n$ 而言,长度约为 $\log{n}$,因此「罗马数字」的长度不超过 $4 * \log{n}$。复杂度为 $O(\log{n})$
* 空间复杂度:虽然使用了两个数组记录罗马字符和数值,但消耗的空间固定,不随着样本大小而变化。复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
```
字符          数值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000
```
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做  XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

* I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
* X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
* C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。

给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。

示例 1:
```
输入: "III"
输出: 3
```
示例 2:
```
输入: "IV"
输出: 4
```
示例 3:
```
输入: "IX"
输出: 9
```
示例 4:
```
输入: "LVIII"
输出: 58
解释: L = 50, V= 5, III = 3.
```
示例 5:
```
输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
```

提示:
* $1 <= s.length <= 15$
* `s` 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
* 题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内
* 题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。
* IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。

Solution:
### 模拟

与 [12. 整数转罗马数字](https://leetcode-cn.com/problems/integer-to-roman/solution/gong-shui-san-xie-12-zheng-shu-zhuan-luo-b9kw/) 类似,现实中每一个「罗马数字」固定对应一个「阿拉伯数字」。

因此这是一个按照固定规则进行转换的「模拟」过程。

根据题意,我们可以列举出有限个罗马字符和其对应的数值。

然后从左到右匹配罗马数字,优先匹配数值高的罗马字符即可。

代码:
```Java
class Solution {
    int[] val = new int[]{1000,900,500,400,100,90,50,40,10,9,5,4,1};
    String[] str = new String[]{"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
    public int romanToInt(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0, j = 0; i < str.length && j < n; i++) {
            int cv = val[i];
            String cs = str[i];
            int size = cs.length();
            while (j + size <= n && s.substring(j, j + size).equals(cs)) {
                ans += cv;
                j += size;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:通过映射对应关系,我们发现每一位「罗马数字字符」都对应特定的数值,将对应数值添加到答案的复杂度为 $O(1)$。复杂度为 $O(n)$
* 空间复杂度:虽然使用了两个数组记录罗马字符和数值,但消耗的空间固定,不随着样本大小而变化。复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 `""`。

示例 1:
```
输入:strs = ["flower","flow","flight"]
输出:"fl"
```
示例 2:
```
输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。
```

提示:
* $0 <= strs.length <= 200$
* $0 <= strs[i].length <= 200$
* `strs[i]` 仅由小写英文字母组成

Solution:
### 朴素解法

对每个字符串进行逐位检查,直到不满足公共前缀。

代码:
```Java
class Solution {
    public String longestCommonPrefix(String[] ss) {
        String ans = "";
        if (ss.length == 0) return ans;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String s = ss[0];
            if (i >= s.length()) return ans;
            char c = ss[0].charAt(i);
            for (String item : ss) {
                if (i >= item.length() || item.charAt(i) != c) return ans;
            }
            ans += String.valueOf(c);
        }
        return ans;
    }
}
```
说明:判断条件不一定要写成 `i < Integer.MAX_VALUE`,题目给出了范围是 200 以内,写成 `i <= 200` 也可以。不影响执行效率。
* 时间复杂度:对于 $n$ 个字符串,都需要遍历到公共前缀长度 m。复杂度为 $O(n \times m)$
* 空间复杂度:需要存储公共前缀作为答案返回。复杂度为 $O(m)$

### 最后

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

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

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

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


Question:
给你一个包含 $n$ 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 `a`,`b`,`c` ,使得 `a + b + c = 0` ?

请你找出所有和为 $0$ 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:
```
输入:nums = [-1,0,1,2,-1,-4]

输出:[[-1,-1,2],[-1,0,1]]
```
示例 2:
```
输入:nums = []

输出:[]
```
示例 3:
```
输入:nums = [0]

输出:[]
```

提示:
* $0 <= nums.length <= 3000$
* $-10^5 <= nums[i] <= 10^5$

Solution:
### 排序 + 双指针

对数组进行排序,使用三个指针 `i`、`j` 和 `k` 分别代表要找的三个数。

1. 通过枚举 `i` 确定第一个数,另外两个指针 `j`,`k` 分别从左边 `i + 1` 和右边 `n - 1` 往中间移动,找到满足 `nums[i] + nums[j] + nums[k] == 0` 的所有组合。

2. `j` 和 `k` 指针的移动逻辑,分情况讨论 `sum = nums[i] + nums[j] + nums[k]` :
    * `sum` > 0:`k` 左移,使 `sum` 变小
    * `sum` < 0:`j` 右移,使 `sum` 变大
    * `sum` = 0:找到符合要求的答案,存起来

由于题目要求答案不能包含重复的三元组,所以在确定第一个数和第二个数的时候,要跳过数值一样的下标(在三数之和确定的情况下,确保第一个数和第二个数不会重复,即可保证三元组不重复)。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = n - 1;
            while (j < k) {
                while (j > i + 1 && j < n && nums[j] == nums[j - 1]) j++;
                if (j >= k) break;
                int sum = nums[i] + nums[j] + nums[k];
                if (sum == 0) {
                    ans.add(Arrays.asList(nums[i], nums[j], nums[k]));
                    j++;
                } else if (sum > 0) {
                    k--;
                } else if (sum < 0) {
                    j++;
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<vector<int>> ans;
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = n - 1;
            while (j < k) {
                while (j > i + 1 && j < n && nums[j] == nums[j - 1]) j++;
                if (j >= k) break;
                int sum = nums[i] + nums[j] + nums[k];
                if (sum == 0) {
                    ans.push_back({nums[i], nums[j], nums[k]});
                    j++;
                } else if (sum > 0) {
                    k--;
                } else if (sum < 0) {
                    j++;
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        ans = []
        for i in range(n):
            if i > 0 and nums[i] == nums[i - 1]: continue
            j, k = i + 1, n - 1
            while j < k:
                while j > i + 1 and j < n and nums[j] == nums[j - 1]:
                    j += 1
                if j >= k: break
                _sum = nums[i] + nums[j] + nums[k]
                if _sum == 0:
                    ans.append([nums[i], nums[j], nums[k]])
                    j += 1
                elif _sum > 0:
                    k -= 1
                elif _sum < 0:
                    j += 1
        return ans
```
TypeScript 代码:
```TypeScript
function threeSum(nums: number[]): number[][] {
    nums.sort((a, b) => a - b);
    const n = nums.length;
    const ans = [];
    for (let i = 0; i < n; i++) {
        if (i > 0 && nums[i] === nums[i - 1]) continue;
        let j = i + 1, k = n - 1;
        while (j < k) {
            while (j > i + 1 && j < n && nums[j] === nums[j - 1]) j++;
            if (j >= k) break;
            const sum = nums[i] + nums[j] + nums[k];
            if (sum === 0) {
                ans.push([nums[i], nums[j], nums[k]]);
                j++;
            } else if (sum > 0) {
                k--;
            } else if (sum < 0) {
                j++;
            }
        }
    }
    return ans;
};
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$,对于每个 `i` 而言,最坏的情况 `j` 和 `k` 都要扫描一遍数组的剩余部分,复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(\log{n})$

### 最后

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

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

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

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


16. 最接近的三数之和

Question:
给定一个包括 $n$ 个整数的数组 `nums` 和 一个目标值 `target`。

找出 `nums` 中的三个整数,使得它们的和与 `target` 最接近。

返回这三个数的和。

每组输入只存在唯一答案。

示例:
```
输入:nums = [-1,2,1,-4], target = 1

输出:2

解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
```

提示:
* $3 <= nums.length <= 10^3$
* $-10^3 <= nums[i] <= 10^3$
* $-10^4 <= target <= 10^4$

Solution:
### 排序 + 双指针

对数组进行排序,使用三个指针 `i`、`j` 和 `k` 分别代表要找的三个数。

1. 通过枚举 `i` 确定第一个数,另外两个指针 `j`,`k` 分别从左边 `i + 1` 和右边 `n - 1` 往中间移动,找到满足 `nums[i] + nums[j] + nums[k]` 最接近 `target` 的唯一解。

2. `j` 和 `k` 指针的移动逻辑,分情况讨论 `sum = nums[i] + nums[j] + nums[k]` :
    * `sum` > `target`:`k` 左移,使 `sum` 变小
    * `sum` < `target`:`j` 右移,使 `sum` 变大
    * `sum` = `target`:找到最符合要求的答案,直接返回

为了更快找到答案,对于相同的 `i`,可以直接跳过下标。

Java 代码:
```Java
class Solution {
    public int threeSumClosest(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length, ans = nums[0] + nums[1] + nums[2];
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = n - 1;
            while (j < k) {
                int sum = nums[i] + nums[j] + nums[k];
                if (Math.abs(sum - target) < Math.abs(ans - target)) ans = sum;
                if (ans == target) {
                    return target;
                } else if (sum > target) {
                    k--;
                } else if (sum < target) {
                    j++;
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int threeSumClosest(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end());
        int n = nums.size(), ans = nums[0] + nums[1] + nums[2];
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            int j = i + 1, k = n - 1;
            while (j < k) {
                int sum = nums[i] + nums[j] + nums[k];
                if (abs(sum - target) < abs(ans - target)) ans = sum;
                if (ans == target) {
                    return target;
                } else if (sum > target) {
                    k--;
                } else if (sum < target) {
                    j++;
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        n, ans = len(nums), nums[0] + nums[1] + nums[2]
        for i in range(n):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            j, k = i + 1, n - 1
            while j < k:
                s = nums[i] + nums[j] + nums[k]
                if abs(s - target) < abs(ans - target):
                    ans = s
                if ans == target:
                    return target
                elif s > target:
                    k -= 1
                elif s < target:
                    j += 1
        return ans
```
TypeScript 代码:
```TypeScript
function threeSumClosest(nums: number[], target: number): number {
    nums.sort((a, b) => a - b);
    let n = nums.length, ans = nums[0] + nums[1] + nums[2];
    for (let i = 0; i < n; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        let j = i + 1, k = n - 1;
        while (j < k) {
            const sum = nums[i] + nums[j] + nums[k];
            if (Math.abs(sum - target) < Math.abs(ans - target)) ans = sum;
            if (ans == target) {
                return target;
            } else if (sum > target) {
                k--;
            } else if (sum < target) {
                j++;
            }
        }
    }
    return ans;
};
```
* 时间复杂度:排序的复杂度为 $O(n\log{n})$,对于每个 `i` 而言,最坏的情况 `j` 和 `k` 都要扫描一遍数组的剩余部分,复杂度为 $O(n^2)$。整体复杂度为 $O(n^2)$
* 空间复杂度:$O(n ^ 2)$

### 最后

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

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

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

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


17. 电话号码的字母组合

Question:
给定一个仅包含数字 `2-9` 的字符串,返回所有它能表示的字母组合。

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

给出数字到字母的映射如下(与电话按键相同)。注意 $1$ 不对应任何字母。

示例 1:

```
输入:digits = "23"

输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
```
示例 2:
```
输入:digits = ""

输出:[]
```
示例 3:
```
输入:digits = "2"

输出:["a","b","c"]
```

提示:
* $0 <= digits.length <= 4$
* $digits[i]$ 是范围 `['2','9']` 的一个数字。

Solution:
### 回溯算法

对于字符串 `ds` 中的每一位数字,都有其对应的字母映射数组。

在 DFS 中决策每一位数字应该对应哪一个字母,当决策的位数 `i == n`,代表整个 `ds` 字符串都被决策完毕,将决策结果添加到结果集:

代码:
```Java
class Solution {
    Map<String, String[]> map = new HashMap<>(){{
        put("2", new String[]{"a", "b", "c"});
        put("3", new String[]{"d", "e", "f"});
        put("4", new String[]{"g", "h", "i"});
        put("5", new String[]{"j", "k", "l"});
        put("6", new String[]{"m", "n", "o"});
        put("7", new String[]{"p", "q", "r", "s"});
        put("8", new String[]{"t", "u", "v"});
        put("9", new String[]{"w", "x", "y", "z"});
    }};
    public List<String> letterCombinations(String ds) {
        int n = ds.length();
        List<String> ans = new ArrayList<>();
        if (n == 0) return ans;
        StringBuilder sb = new StringBuilder();
        dfs(ds, 0, n, sb, ans);
        return ans;
    }
    void dfs(String ds, int i, int n, StringBuilder sb, List<String> ans) {
        if (i == n) {
            ans.add(sb.toString());
            return;
        }
        String key = ds.substring(i, i + 1);
        String[] all = map.get(key);
        for (String item : all) {
            sb.append(item);
            dfs(ds, i + 1, n, sb, ans);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}
```
* 时间复杂度:`n` 代表字符串 `ds` 的长度,一个数字最多对应 4 个字符(7 对应 “pqrs"),即每个数字最多有 4 个字母需要被决策。复杂度为 $O(4^n)$
* 空间复杂度:有多少种方案,就需要多少空间来存放答案。复杂度为 $O(4^n)$

### 最后

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

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

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

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


Question:
给定一个包含 $n$ 个整数的数组 `nums` 和一个目标值 `t`,判断 `nums` 中是否存在四个元素 `a`,`b`,`c` 和 `d` ,使得 `a + b + c + d` 的值与 `t` 相等?

找出所有满足条件且不重复的四元组。

注意:答案中不可以包含重复的四元组。

示例 1:
```
输入:nums = [1,0,-1,0,-2,2], t = 0

输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
```
示例 2:
```
输入:nums = [], t = 0

输出:[]
```

提示:
* $0 <= nums.length <= 200$
* $-10^9 <= nums[i] <= 10^9$
* $-10^9 <= t<= 10^9$

Solution:
### 排序 + 双指针

对数组进行排序,使用四个指针 `i`、`j` 、`k` 和 `p` 分别代表要找的四个数。

1. 通过枚举 `i` 确定第一个数,枚举 `j` 确定第二个数,另外两个指针 `k` 和 `p` 分别从左边 `j + 1` 和右边 `n - 1` 往中间移动,找到满足 `nums[i] + nums[j] + nums[k] + nums[p] == t` 的所有组合。

2. `k` 和 `p` 指针的移动逻辑,分情况讨论 `sum = nums[i] + nums[j] + nums[k] + nums[p]` :
    * `sum` > `t`:`p` 左移,使 `sum` 变小
    * `sum` < `t`:`k` 右移,使 `sum` 变大
    * `sum` = `t`:将组合加入结果集,`k` 右移继续进行检查

题目要求不能包含重复元素,所以我们要对 `i`、`j` 和 `k` 进行去重,去重逻辑是对于相同的数,只使用第一个。

Java 代码:
```Java
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int t) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        for (int i = 0; i < n; i++) { // 确定第一个数
            if (i > 0 && nums[i] == nums[i - 1]) continue; // 对第一个数进行去重(相同的数只取第一个)
            for (int j = i + 1; j < n; j++) { // 确定第二个数
                if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 对第二个数进行去重(相同的数只取第一个)
                // 确定第三个数和第四个数
                int k = j + 1, p = n - 1;
                while (k < p) {

                    // 对第三个数进行去重(相同的数只取第一个)
                    while (k > j + 1 && k < n && nums[k] == nums[k - 1]) k++;
                    // 如果 k 跳过相同元素之后的位置超过了 p,本次循环结束
                    if (k >= p) break;

                    long sum = 0L + nums[i] + nums[j] + nums[k] + nums[p];
                    if (sum == t) {
                        ans.add(Arrays.asList(nums[i], nums[j], nums[k], nums[p]));
                        k++;
                    } else if (sum > t) {
                        p--;
                    } else if (sum < t) {
                        k++;
                    }
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int t) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<vector<int>> ans;
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < n; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;
                int k = j + 1, p = n - 1;
                while (k < p) {
                    while (k > j + 1 && k < n && nums[k] == nums[k - 1]) k++;
                    if (k >= p) break;
                    long sum = 0L + nums[i] + nums[j] + nums[k] + nums[p];
                    if (sum == t) {
                        ans.push_back({nums[i], nums[j], nums[k], nums[p]});
                        k++;
                    } else if (sum > t) {
                        p--;
                    } else if (sum < t) {
                        k++;
                    }
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def fourSum(self, nums: List[int], t: int) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        ans = []

        for i in range(n):
            if i > 0 and nums[i] == nums[i - 1]: continue
            for j in range(i + 1, n):
                if j > i + 1 and nums[j] == nums[j - 1]: continue
                k, p = j + 1, n - 1
                while k < p:
                    while k > j + 1 and k < n and nums[k] == nums[k - 1]:
                        k += 1
                    if k >= p: break

                    _sum = nums[i] + nums[j] + nums[k] + nums[p]
                    if _sum == t:
                        ans.append([nums[i], nums[j], nums[k], nums[p]])
                        k += 1
                    elif _sum > t:
                        p -= 1
                    elif _sum < t:
                        k += 1
        return ans
```
TypeScript 代码:
```TypeScript
function fourSum(nums: number[], t: number): number[][] {
    nums.sort((a, b) => a - b);
    const n = nums.length;
    const ans = [];
    for (let i = 0; i < n; i++) {
        if (i > 0 && nums[i] === nums[i - 1]) continue;
        for (let j = i + 1; j < n; j++) {
            if (j > i + 1 && nums[j] === nums[j - 1]) continue;
            let k = j + 1, p = n - 1;
            while (k < p) {
                while (k > j + 1 && k < n && nums[k] === nums[k - 1]) k++;
                if (k >= p) break;
                const sum = nums[i] + nums[j] + nums[k] + nums[p];
                if (sum === t) {
                    ans.push([nums[i], nums[j], nums[k], nums[p]]);
                    k++;
                } else if (sum > t) {
                    p--;
                } else if (sum < t) {
                    k++;
                }
            }
        }
    }
    return ans;
};
```
* 时间复杂度:`i` 和 `j` 是直接枚举确定,复杂度为 $O(n^2)$,当确定下来 `i` 和 `j` 之后,通过双指针确定 `k` 和 `p` ,也就是对于每一组 `i` 和 `j` 而言复杂度为 $O(n)$。总的复杂度为 $O(n^3)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/11-20/19. 删除链表的倒数第 N 个结点（中等）.md

19. 删除链表的倒数第 N 个结点

Question:
给你一个链表,删除链表的倒数第 `n` 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

```
输入:head = [1,2,3,4,5], n = 2

输出:[1,2,3,5]
```
示例 2:
```
输入:head = [1], n = 1

输出:[]
```
示例 3:
```
输入:head = [1,2], n = 1

输出:[1]
```

提示:
* 链表中结点的数目为 `sz`
* $1 <= sz <= 30$
* $0 <= Node.val <= 100$
* $1 <= n <= sz$

Solution:
### 快慢指针

删除链表的倒数第 `n` 个结点,首先要确定倒数第 `n` 个节点的位置。

我们可以设定两个指针,分别为 `slow` 和 `fast`,刚开始都指向 head。

然后先让 `fast` 往前走 `n` 步,`slow` 指针不动,这时候两个指针的距离为 `n`。

再让 `slow` 和 `fast` 同时往前走(保持两者距离不变),直到 `fast` 指针到达结尾的位置。

这时候 `slow` 会停在待删除节点的前一个位置,让 `slow.next = slow.next.next` 即可。

但这里有一个需要注意的边界情况是:如果链表的长度是 `L`,而我们恰好要删除的是倒数第 `L` 个节点(删除头节点),这时候 `fast` 往前走 `n` 步之后会变为 null,此时我们只需要让 `head = slow.next` 即可删除。

代码:
```Java
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if (head.next == null) return null;
        ListNode slow = head, fast = head;
        while (n-- > 0) fast = fast.next;
        if (fast == null) {
            head = slow.next;
        } else {
            while (fast.next != null) {
                slow = slow.next; fast = fast.next;
            }
            slow.next = slow.next.next;
        }
        return head;
    }
}
```
* 时间复杂度:需要扫描的长度为链表的长度。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个只包括 `'(',')','{','}','[',']'` 的字符串 `s` ,判断字符串是否有效。

有效字符串需满足:

1. 左括号必须用相同类型的右括号闭合。

2. 左括号必须以正确的顺序闭合。

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

输出:true
```
示例 2:
```
输入:s = "()[]{}"

输出:true
```
示例 3:
```
输入:s = "(]"

输出:false
```
示例 4:
```
输入:s = "([)]"

输出:false
```
示例 5:
```
输入:s = "{[]}"

输出:true
```

提示:
* $1 <= s.length <= 10^4$
* `s` 仅由括号 `'()[]{}'` 组成

Solution:
### 栈 + 哈希表

这是道模拟题,同一类型的括号,一个右括号要对应一个左括号。

不难发现可以直接使用 `栈` 来解决:

代码:
```Java
class Solution {
    HashMap<Character, Character> map = new HashMap<Character, Character>(){{
        put(']', '[');
        put('}', '{');
        put(')', '(');
    }};
    public boolean isValid(String s) {
        Deque<Character> d = new ArrayDeque<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(' || c == '{' || c == '[') {
                d.addLast(c);
            } else {
                if (!d.isEmpty() && d.peekLast() == map.get(c)) {
                    d.pollLast();
                } else {
                    return false;
                }
            }
        }
        return d.isEmpty();
    }
}
```
* 时间复杂度:对字符串 `s` 扫描一遍。复杂度为 $O(n)$
* 空间复杂度:使用的哈希表空间固定,不随着样本数量变大而变大。复杂度为 $O(1)$

**注意:三叶使用了 `Deque` 双端队列来充当栈,而不是 `Stack`,这也是 JDK 推荐的做法。建议所有的 Java 同学都采用 `Deque` 作为栈。**

**不使用 `Stack` 的原因是 `Stack` 继承自 `Vector`,拥有了动态数组的所有公共 API,并不安全,而且 `Stack` 还犯了面向对象设计的错误:将组合关系当成了继承关系。**

---

### 栈 + ASCII 差值

我们也可以利用 `"()"`、`"{}"` 和 `"[]"` 的左右部分在 ASCII 值上比较接近的事实。

`(` 和 `)` 分别对应 -7 和 -8;`[` 和 `]` 分别对应 43 和 45;`{` 和 `}` 分别对应 75 和 77。

也就是同类型的左右括号,相差不超过 2 ,同时不同类型的左右括号,相差大于 2。

利用此特性,我们可以节省一个哈希表:

代码:
```Java
class Solution {
    public boolean isValid(String s) {
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            int u = c - '0';
            if (c == '(' || c == '{' || c == '[') {
                d.addLast(u);
            } else {
                if (!d.isEmpty() && Math.abs(d.peekLast() - u) <= 2) {
                    d.pollLast();
                } else {
                    return false;
                }
            }
        }
        return d.isEmpty();
    }
}
```
* 时间复杂度:对字符串 `s` 扫描一遍。复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
在一棵无限的二叉树上,每个节点都有两个子节点,树中的节点 逐行 依次按 “之” 字形进行标记。

如下图所示,在奇数行(即,第一行、第三行、第五行......)中,按从左到右的顺序进行标记;

而偶数行(即,第二行、第四行、第六行......)中,按从右到左的顺序进行标记。

给你树上某一个节点的标号 label,请你返回从根节点到该标号为 label 节点的路径,该路径是由途经的节点标号所组成的。

示例 1:
```
输入:label = 14

输出:[1,3,4,14]
```
示例 2:
```
输入:label = 26

输出:[1,2,6,10,26]
```

提示:
* 1 <= label <= $10^6$

Solution:
### 模拟

一个朴素的做法是根据题意进行模拟。

利用从根节点到任意一层都是满二叉树,我们可以先确定 `label` 所在的层级 `level`,然后计算出当前层起始节点值(最小值)和结束节点值(最大值)。

再利用「每层节点数量翻倍」&「隔层奇偶性翻转」,寻址出上一层的节点下标(令每层下标均「从左往右」计算,并从 $1$ 开始),直到构造出答案(寻址到根节点)。

代码:
```Java
class Solution {
    // 第 level 层的起始节点值
    int getStart(int level) {
        return (int)Math.pow(2, level - 1);
    }
    // 第 level 层的结束节点值
    int getEnd(int level) {
        int a = getStart(level);
        return a + a - 1;
    }
    public List<Integer> pathInZigZagTree(int n) {
        // 计算 n 所在层级
        int level = 1;
        while (getEnd(level) < n) level++;

        int[] ans = new int[level];
        int idx = level - 1, cur = n;
        while (idx >= 0) {
            ans[idx--] = cur;
            int tot = (int)Math.pow(2, level - 1);
            int start = getStart(level), end = getEnd(level);
            if (level % 2 == 0) {
                // 当前层为偶数层,则当前层节点「从右往左」数值递增,相应计算上一层下标也应该「从右往左」
                int j = tot / 2;
                for (int i = start; i <= end; i += 2, j--) {
                    if (i == cur || (i + 1) == cur) break;
                }
                int prevStart = getStart(level - 1);
                while (j-- > 1) prevStart++;
                cur = prevStart;
            } else {
                // 当前层为奇数层,则当前层节点「从左往右」数值递增,相应计算上一层下标也应该「从左往右」
                int j = 1;
                for (int i = start; i <= end; i += 2, j++) {
                    if (i == cur || (i + 1) == cur) break;
                }
                int prevEnd = getEnd(level - 1);
                while (j-- > 1) prevEnd--;
                cur = prevEnd;
            }
            level--;
        }
        List<Integer> list = new ArrayList<>();
        for (int i : ans) list.add(i);
        return list;
    }
}
```
* 时间复杂度:确定 $n$ 所在层级复杂度为 $O(\log{n})$;构造答案最坏情况下每个节点会被遍历一次,复杂度为 $O(n)$
* 空间复杂度:$O(1)$

---

### 数学

上述解法复杂度上界取决于「由当前行节点位置确定上层位置」的线性遍历。

如果二叉树本身不具有奇偶性翻转的话,显然某个节点 $x$ 的父节点为 $\left \lfloor x / 2 \right \rfloor$,但事实上存在奇偶性翻转,而在解法一中我们已经可以 $O(1)$ 计算某一层的起始值和结束值,有了「起始值 & 结算值」和「当前节点所在层的相对位置」,只需要利用“对称性”找到父节点在上层的相应位置,然后根据相应位置算出父节点值即可。

代码:
```Java
class Solution {
    int getStart(int level) {
        return (int)Math.pow(2, level - 1);
    }
    int getEnd(int level) {
        int a = getStart(level);
        return a + a - 1;
    }
    public List<Integer> pathInZigZagTree(int n) {
        int level = 1;
        while (getEnd(level) < n) level++;
        int[] ans = new int[level];
        int idx = level - 1, cur = n;
        while (idx >= 0) {
            ans[idx--] = cur;
            int loc = ((1 << (level)) - 1 - cur) >> 1;
            cur = (1 << (level - 2)) + loc;
            level--;
        }
        List<Integer> list = new ArrayList<>();
        for (int i : ans) list.add(i);
        return list;
    }
}
```
* 时间复杂度:复杂度上界取决于确定 $n$ 所在层级。复杂度为 $O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


1106. 解析布尔表达式

Question:
给你一个以字符串形式表述的 布尔表达式 `(boolean) expression`,返回该式的运算结果。

有效的表达式需遵循以下约定:

* `"t"`,运算结果为 `True`
* `"f"`,运算结果为 `False`
* `"!(expr)"`,运算过程为对内部表达式 `expr` 进行逻辑 非的运算(`NOT`)
* `"&(expr1,expr2,...)"`,运算过程为对 `2` 个或以上内部表达式 `expr1, expr2, ...` 进行逻辑 与的运算(`AND`)
* `"|(expr1,expr2,...)"`,运算过程为对 `2` 个或以上内部表达式 `expr1, expr2, ...` 进行逻辑 或的运算(`OR`)

示例 1:
```
输入:expression = "!(f)"

输出:true
```
示例 2:
```
输入:expression = "|(f,t)"

输出:true
```
示例 3:
```
输入:expression = "&(t,f)"

输出:false
```
示例 4:
```
输入:expression = "|(&(t,f,t),!(t))"

输出:false
```

提示:
* $1 <= expression.length <= 20000$
* `expression[i]` 由 `{'(', ')', '&', '|', '!', 't', 'f', ','}` 中的字符组成。
* `expression` 是以上述形式给出的有效表达式,表示一个布尔值。

Solution:
### 双栈

为了方便,我们令 `expression` 为 `s`。

我们可以将 `t` 和 `f` 看做操作数,而 `|`、`&` 和 `!` 看做操作符,创建两个栈 `nums` 和 `ops` 分别对其进行存储。

剩余的 `()` 和 `,` 则只是优先级和分隔符,无须额外关注。

从前往后处理 `s`,根据当前处理的字符为何值进行分情况讨论:

* `,`:分隔符,直接跳过;
* `t` 或 `f`:操作数,添加到 `nums` 栈中;
* `|`、`&` 或 `!`:操作符,添加到 `ops` 栈中;
* `(`:子表达式的左端点,为了在我们从「双栈」中取出数值和符号计算时,可以知道某个子表达式计算完成,需要记录一下。往 `nums` 追加一个占位符号 `-` 来代指;
* `)`:子表达式的右端点,代表一个子表达式的结束。可从「双栈」中取出符号和数组进行计算(在 `ops` 中仅取栈顶元素,代表当前子表达式的操作符;而在 `nums` 中则取到代表左端点的占位元素 `-` 为止),并将结果重新放入 `nums` 中。

最后考虑如何计算最简表达式,考虑实现一个 `char calc(char a, char b, char op)` 函数,代表对操作数 `a` 和 `b` 执行 `op` 操作并进行结果返回。

实际上,在 `calc` 函数我们只区分 `|` 操作和其他操作即可。也就是说 `&` 和 `!` 均当做 `&` 来做,`!` 操作在计算完整个表达式后再翻转。

Java 代码:
```Java
class Solution {
    public boolean parseBoolExpr(String s) {
        Deque<Character> nums = new ArrayDeque<>(), ops = new ArrayDeque<>();
        for (char c : s.toCharArray()) {
            if (c == ',') continue;
            if (c == 't' || c == 'f') nums.addLast(c);
            if (c == '|' || c == '&' || c == '!') ops.addLast(c);
            if (c == '(') nums.addLast('-');
            if (c == ')') {
                char op = ops.pollLast(), cur = ' ';
                while (!nums.isEmpty() && nums.peekLast() != '-') {
                    char top = nums.pollLast();
                    cur = cur == ' ' ? top : calc(top, cur, op);
                }
                if (op == '!') cur = cur == 't' ? 'f' : 't';
                nums.pollLast(); nums.addLast(cur);
            }
        }
        return nums.peekLast() == 't';
    }
    char calc(char a, char b, char op) {
        boolean x = a == 't', y = b == 't';
        boolean ans = op == '|' ? x | y : x & y;
        return ans ? 't' : 'f';
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    bool parseBoolExpr(string s) {
        deque<char> nums, ops;
        for (char c : s) {
            if (c == ',') continue;
            if (c == 't' || c == 'f') nums.push_back(c);
            if (c == '|' || c == '&' || c == '!') ops.push_back(c);
            if (c == '(') nums.push_back('-');
            if (c == ')') {
                char op = ops.back(); ops.pop_back();
                char cur = ' ';
                while (!nums.empty() && nums.back() != '-') {
                    char top = nums.back(); nums.pop_back();
                    cur = cur == ' ' ? top : calc(top, cur, op);
                }
                if (op == '!') cur = cur == 't' ? 'f' : 't';
                nums.pop_back(); nums.push_back(cur);
            }
        }
        return nums.back() == 't';
    }
    char calc(char a, char b, char op) {
        bool x = a == 't', y = b == 't';
        bool ans = op == '|' ? x | y : x & y;
        return ans ? 't' : 'f';
    }
};
```
Python 代码:
```Python
class Solution:
    def parseBoolExpr(self, s: str) -> bool:
        def calc(a, b, op):
            x, y = a == 't', b == 't'
            ans = x | y if op == '|' else x & y
            return 't' if ans else 'f'
        nums, ops = [], []
        for c in s:
            if c == ',':
                continue
            if c == 't' or c == 'f':
                nums.append(c)
            if c == '|' or c == '&' or c == '!':
                ops.append(c)
            if c == '(':
                nums.append('-')
            if c == ')':
                op, cur = ops.pop(), ' '
                while nums and nums[-1] != '-':
                    top = nums.pop()
                    cur = top if cur == ' ' else calc(cur, top, op)
                if op == '!':
                    cur = 't' if cur == 'f' else 'f'
                nums.pop()
                nums.append(cur)
        return nums[-1] == 't'
```
TypeScript 代码:
```TypeScript
function parseBoolExpr(s: string): boolean {
    function calc(a: string, b: string, op: string): string {
        const x = a == 't', y = b == 't'
        const ans = op == '|' ? x || y : x && y
        return ans ? 't' : 'f'
    }
    const nums = new Array<string>(s.length).fill(''), ops = new Array<string>(s.length).fill('')
    let idx1 = 0, idx2 = 0
    for (const c of s) {
        if (c == ',') continue
        if (c == 't' || c == 'f') nums[idx1++] = c
        if (c == '|' || c == '&' || c == '!') ops[idx2++] = c
        if (c == '(') nums[idx1++] = '-'
        if (c == ')') {
            let op = ops[--idx2], cur = ' '
            while (idx1 > 0 && nums[idx1 - 1] != '-') {
                const top = nums[--idx1]
                cur = cur == ' ' ? top : calc(top, cur, op)
            }
            if (op == '!') cur = cur == 't' ? 'f' : 't'
            idx1--; nums[idx1++] = cur
        }
    }
    return nums[idx1 - 1] == 't'
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个有效的 `IPv4` 地址 `address`,返回这个 `IP` 地址的无效化版本。

所谓无效化 `IP` 地址,其实就是用 `"[.]"` 代替了每个 `"."`。

示例 1:
```
输入:address = "1.1.1.1"

输出:"1[.]1[.]1[.]1"
```
示例 2:
```
输入:address = "255.100.50.0"

输出:"255[.]100[.]50[.]0"
```

提示:
* 给出的 `address` 是一个有效的 `IPv4` 地址

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    public String defangIPaddr(String s) {
        StringBuilder sb = new StringBuilder();
        int n = s.length(), idx = -1;
        while (++idx < n) {
            char c = s.charAt(idx);
            if (c == '.') sb.append('[');
            sb.append(c);
            if (c == '.') sb.append(']');
        }
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


1109. 航班预订统计

Question:
这里有 $n$ 个航班,它们分别从 $1$ 到 $n$ 进行编号。

有一份航班预订表 `bookings`,表中第 $i$ 条预订记录 $bookings[i] = [first_i, last_i, seats_i]$ 意味着在从 $first_i$ 到 $last_i$ (包含 $first_i$ 和 $last_i$ )的 每个航班 上预订了 $seats_i$ 个座位。

请你返回一个长度为 $n$ 的数组 `answer`,其中 $answer[i]$ 是航班 $i$ 上预订的座位总数。

示例 1:
```
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5

输出:[10,55,45,25,25]

解释:
航班编号        1   2   3   4   5
预订记录 1 :   10  10
预订记录 2 :       20  20
预订记录 3 :       25  25  25  25
总座位数:      10  55  45  25  25
因此,answer = [10,55,45,25,25]
```
示例 2:
```
输入:bookings = [[1,2,10],[2,2,15]], n = 2

输出:[10,25]

解释:
航班编号        1   2
预订记录 1 :   10  10
预订记录 2 :       15
总座位数:      10  25
因此,answer = [10,25]
```
提示:
* $1 <= n <= 2 * 10^4$
* $1 <= bookings.length <= 2 * 10^4$
* $bookings[i].length == 3$
* $1 <= first_i <= last_i <= n$
* $1 <= seats_i <= 10^4$

Solution:
### 基本分析

本题只涉及「区间修改 + 单点查询」,属于「区间求和」问题中的入门难度。

对于各类「区间求和」问题,该用什么方式进行求解,之前在 [这里](https://leetcode-cn.com/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/) 提到过。

此处可以再总结一下(加粗字体为最佳方案):

* 数组不变,区间查询:**前缀和**、树状数组、线段树;
* 数组单点修改,区间查询:**树状数组**、线段树;
* 数组区间修改,单点查询:**差分**、线段树;
* 数组区间修改,区间查询:**线段树**。

> 注意:上述总结是对于一般性而言的(能直接解决的),对标的是模板问题。
但存在经过一些经过“额外”操作,对问题进行转化,从而使用别的解决方案求解的情况。
例如某些问题,我们可以先对原数组进行差分,然后使用树状数组,也能解决区间修改问题。
或者使用多个树状数组来维护多个指标,从而实现类似线段树的持久化标记操作。
但这些不属于一般性,所以就不添加到题解了。

---

### 差分

本题只涉及「区间修改 + 单点查询」,因此是一道「差分」的模板题。

「差分」可以看做是求「前缀和」的逆向过程。

对于一个「将区间 $[l, r]$ 整体增加一个值 $v$」操作,我们可以对差分数组 $c$ 的影响看成两部分:

* 对 $c[l] += v$:由于差分是前缀和的逆向过程,这个操作对于将来的查询而言,带来的影响是对于所有的下标大于等于 $l$ 的位置都增加了值 $v$;
* 对 $c[r + 1] -= v$:由于我们期望只对 $[l, r]$ 产生影响,因此需要对下标大于 $r$ 的位置进行减值操作,从而抵消“影响”。

对于最后的构造答案,可看做是对每个下标做“单点查询”操作,只需要对差分数组求前缀和即可。

代码:
```Java
class Solution {
    public int[] corpFlightBookings(int[][] bs, int n) {
        int[] c = new int[n + 1];
        for (int[] bo : bs) {
            int l = bo[0] - 1, r = bo[1] - 1, v = bo[2];
            c[l] += v;
            c[r + 1] -= v;
        }
        int[] ans = new int[n];
        ans[0] = c[0];
        for (int i = 1; i < n; i++) {
            ans[i] = ans[i - 1] + c[i];
        }
        return ans;
    }
}
```
* 时间复杂度:令 `bs` 长度为 $m$,预处理差分数组的复杂度为 $O(m)$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(m + n)$
* 空间复杂度:$O(n)$

---

### 线段树

在「基本分析」中,我们发现几乎所有的「区间求和」问题都可以使用线段树解决。

那么是否无脑写线段树呢?答案并不是,恰好相反。

线段树代码很长,且常数很大,实际表现不算很好。只有不得不写「线段树」的时候,我们才考虑线段树。

回到本题,由于涉及「区间修改」操作,因此我们需要对线段树进行持久化标记(懒标记),从而确保操作仍为 $\log$ 级别的复杂度。

代码:
```Java
class Solution {
    class Node {
        int l, r, v, add;
        Node(int _l, int _r) {
            l = _l; r = _r;
        }
    }
    int N = 20009;
    Node[] tr = new Node[N * 4];
    void pushup(int u) {
        tr[u].v = tr[u << 1].v + tr[u << 1 | 1].v;
    }
    void pushdown(int u) {
        int add = tr[u].add;
        tr[u << 1].v += add;
        tr[u << 1].add += add;
        tr[u << 1 | 1].v += add;
        tr[u << 1 | 1].add += add;
        tr[u].add = 0;
    }
    void build(int u, int l, int r) {
        tr[u] = new Node(l, r);
        if (l != r) {
            int mid = l + r >> 1;
            build(u << 1, l, mid);
            build(u << 1 | 1, mid + 1, r);
        }
    }
    void update(int u, int l, int r, int v) {
        if (l <= tr[u].l && tr[u].r <= r) {
            tr[u].v += v;
            tr[u].add += v;
        } else {
            pushdown(u);
            int mid = tr[u].l + tr[u].r >> 1;
            if (l <= mid) update(u << 1, l, r, v);
            if (r > mid) update(u << 1 | 1, l, r, v);
            pushup(u);
        }
    }
    int query(int u, int l, int r) {
        if (l <= tr[u].l && tr[u].r <= r) {
            return tr[u].v;
        } else {
            pushdown(u);
            int mid = tr[u].l + tr[u].r >> 1;
            int ans = 0;
            if (l <= mid) ans += query(u << 1, l, r);
            if (r > mid) ans += query(u << 1 | 1, l, r);
            return ans;
        }
    }
    public int[] corpFlightBookings(int[][] bs, int n) {
        build(1, 1, n);
        for (int[] bo : bs) {
            update(1, bo[0], bo[1], bo[2]);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = query(1, i + 1, i + 1);
        }
        return ans;
    }
}
```
* 时间复杂度:线段树建树复杂度为 $O(n)$,其余操作复杂度为 $O(\log{n})$。对于本题,令 `bs` 长度为 $m$,整体复杂度为 $O(m\log{n} + n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你二叉树的根节点 `root` 和一个整数目标和 `targetSum`,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

```
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22

输出:[[5,4,11,2],[5,8,4,5]]
```
示例 2:

```
输入:root = [1,2,3], targetSum = 5

输出:[]
```
示例 3:
```
输入:root = [1,2], targetSum = 0

输出:[]
```

提示:
* 树中节点总数在范围 $[0, 5000]$ 内
* $-1000 <= Node.val <= 1000$
* $-1000 <= targetSum <= 1000$

Solution:
### 树的遍历 - DFS

定义 `DFS` 函数 `void dfs(TreeNode root, int cur, int t, List<Integer> path)` 。

其中 `root` 为当前遍历到的节点;`cur` 为当前路径总和;`t` 为目标总和;`path` 为当前路径方案。

剩下的为常规的树的遍历,当 `root` 为叶子节点(既没有左子树,也没有右子树)时,根据 `cur` 是否与 `t` 相等,决定是否要把 `path` 添加到答案中。

代码:
```Java
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> pathSum(TreeNode root, int t) {
        if (root != null) dfs(root, root.val, t, new ArrayList<>(){{add(root.val);}});
        return ans;
    }
    void dfs(TreeNode root, int cur, int t, List<Integer> path) {
        if (root.left == null && root.right == null) {
            if (cur == t) ans.add(new ArrayList<>(path));
            return ;
        }
        List<TreeNode> list = new ArrayList<>();
        if (root.left != null) list.add(root.left);
        if (root.right != null) list.add(root.right);
        for (TreeNode child : list) {
            path.add(child.val);
            dfs(child, cur + child.val, t, path);
            path.remove(path.size() - 1);
        }
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个字符串 `s` 和一个字符串 `t` ,计算在 `s` 的子序列中 `t` 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,`"ACE"` 是 `"ABCDE"` 的一个子序列,而 `"AEC"` 不是)

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

示例 1:
```
输入:s = "rabbbit", t = "rabbit"

输出:3

解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
```
示例 2:
```
输入:s = "babgbag", t = "bag"

输出:5

解释:
如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。
(上箭头符号 ^ 表示选取的字母)
babgbag
^^ ^
babgbag
^^    ^
babgbag
^    ^^
babgbag
  ^  ^^
babgbag
    ^^^
```

提示:
* $0 <= s.length, t.length <= 1000$
* `s` 和 `t` 由英文字母组成

Solution:
### 基本思路

有两个字符串 `s` 和 `t`,长度数量级都为 $10^3$。

一个朴素的想法是,找出所有 `s` 的子序列,与 `t` 进行比较,找所有子序列的复杂度是 $O(2^n)$,肯定会超时。

因此,我们放弃这种朴素思路。

字符串匹配也不具有二段性质,不可能有 $log$ 级别的算法,那么复杂度再往下优化就是 $O(n * m)$ 的递推 DP 做法了。

***

### 动态规划

DP 的状态定义猜测通常是一门经验学科。

**但是,对于两个字符串匹配,一个非常通用的状态定义如下:**

**定义 $f[i][j]$ 为考虑 `s` 中 $[0,i]$ 个字符,`t` 中 $[0,j]$ 个字符的匹配个数。**

那么显然对于某个 $f[i][j]$ 而言,从「最后一步」的匹配进行分析,包含两类决策:

* 不让 `s[i]` 参与匹配,也就是需要让 `s` 中 $[0,i-1]$ 个字符去匹配 `t` 中的 $[0,j]$ 字符。此时匹配值为 $f[i-1][j]$
* 让 `s[i]` 参与匹配,这时候只需要让 `s` 中 $[0,i-1]$ 个字符去匹配 `t` 中的 $[0,j-1]$ 字符即可,同时满足 `s[i]=t[j]`。此时匹配值为 $f[i-1][j-1]$

最终 $f[i][j]$ 就是两者之和。

代码:
```java []
class Solution {
    public int numDistinct(String s, String t) {
        // 技巧:往原字符头部插入空格,这样得到 char 数组是从 1 开始
        // 同时由于往头部插入相同的(不存在的)字符,不会对结果造成影响,而且可以使得 f[i][0] = 1,可以将 1 这个结果滚动下去
        int n = s.length(), m = t.length();
        s = " " + s;
        t = " " + t;
        char[] cs = s.toCharArray(), ct = t.toCharArray();
        // f(i,j) 代表考虑「s 中的下标为 0~i 字符」和「t 中下标为 0~j 字符」是否匹配
        int[][] f = new int[n + 1][m + 1];
        // 原字符只有小写字符,当往两个字符插入空格之后,f[i][0] = 1 是一个显而易见的初始化条件
        for (int i = 0; i <= n; i++) f[i][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 包含两种决策:
                // 不使用 cs[i] 进行匹配,则有 f[i][j] = f[i - 1][j]
                f[i][j] = f[i - 1][j];
                // 使用 cs[i] 进行匹配,则要求 cs[i] == ct[j],然后有  f[i][j] += f[i - 1][j - 1]
                if (cs[i] == ct[j]) {
                    f[i][j] += f[i - 1][j - 1];
                }
            }
        }
        return f[n][m];
    }
}
```
* 时间复杂度:$O(n \times m)$
* 空间复杂度:$O(n \times m)$

*PS. 需要说明的是,由于中间结果会溢出,CPP 中必须使用 long long,而 Java 不用。由于 Java 中 int 的存储机制,只要在运算过程中只要不涉及取 min、取 max 或者其他比较操作的话,中间结果溢出不会影响最终结果。*

***

### 总结

1. 关于字符串匹配,通常有两种(你也可以理解为一种)通用的状态定义:

* $f[i][j]$ 表示「第一个字符 `s` 中 $[0,i]$ 个字符」与「第二个字符 `t` 中 $[0,j]$ 个字符」的匹配结果
* $f[i][j]$ 表示「第一个字符 `s` 中 $[0,i]$ 个字符」与「第二个字符 `t` 中 $[0,j]$ 个字符」且 「最后一个字符为 `t[j]`」的匹配结果

2. 往两个字符串的头部追加「不存在」的字符,目的是为了能够构造出可以滚动(被累加)下去的初始化值

***

### 进阶

事实上,关于字符串匹配问题,还有一道稍稍不同的变形的题目。

也是利用了类似的「通用思路」(状态定义) &「技巧」,然后对匹配过程中的字符进行分情况讨论,学有余力的同学可以看看:

[10. 正则表达式匹配](https://leetcode-cn.com/problems/regular-expression-matching/) : [如何利用的「等差」性质降低「正则字符串匹配」算法复杂度 ...](https://leetcode-cn.com/problems/regular-expression-matching/solution/shua-chuan-lc-dong-tai-gui-hua-jie-fa-by-zn9w/)

### 最后

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

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

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

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