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

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

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

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

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

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

Solution:
### 回溯解法(Set)

由于是求所有的方案,而且数据范围只有 $10$,可以直接用爆搜来做。

同时由于答案中不能包含相同的方案,因此我们可以先对原数组进行排序,从而确保所有爆搜出来的方案,都具有单调性,然后配合 `Set` 进行去重。

代码:
```Java
class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        Set<List<Integer>> ans = new HashSet<>();
        List<Integer> cur = new ArrayList<>();
        dfs(nums, 0, cur, ans);
        return new ArrayList<>(ans);
    }

    /**
     * @param nums 原输入数组
     * @param u 当前决策到原输入数组中的哪一位
     * @param cur 当前方案
     * @param ans 最终结果集
     */
    void dfs(int[] nums, int u, List<Integer> cur, Set<List<Integer>> ans) {
        // 所有位置都决策完成,将当前方案放入结果集
        if (nums.length == u) {
            ans.add(new ArrayList<>(cur));
            return;
        }

        // 选择当前位置的元素,往下决策
        cur.add(nums[u]);
        dfs(nums, u + 1, cur, ans);

        // 不选当前位置的元素(回溯),往下决策
        cur.remove(cur.size() - 1);
        dfs(nums, u + 1, cur, ans);
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,爆搜复杂度为 $(2^n)$,每个方案通过深拷贝存入答案,复杂度为 $O(n)$。整体复杂度为 $(n \times 2^n)$
* 空间复杂度:总共有 $2^n$ 个方案,每个方案最多占用 $O(n)$ 空间,整体复杂度为 $(n \times 2^n)$

---

### 回溯解法

我们知道使用 `Set` 虽然是 $O(1)$ 操作,但是只是均摊 $O(1)$。

因此我们来考虑不使用 `Set` 的做法。

我们使用 `Set` 的目的是为了去重,那什么时候会导致的重复呢?

**其实就是相同的元素,不同的决策方案对应同样的结果。**

举个🌰,[1,1,1] 的数据,只选择第一个和只选择第三个(不同的决策方案),结果是一样的。

**因此如果我们希望去重的话,不能单纯的利用「某个下标是否被选择」来进行决策,而是要找到某个数值的连续一段,根据该数值的选择次数类进行决策。**

还是那个🌰,[1,1,1] 的数据,我们可以需要找到数值为 1 的连续一段,然后决策选择 0 次、选择 1 次、选择 2 次 ... 从而确保不会出现重复

**也就是说,将决策方案从「某个下标是否被选择」修改为「相同的数值被选择的个数」。这样肯定不会出现重复,因为 [1,1,1] 不会因为只选择第一个和只选择第三个产生两个 [1] 的方案,只会因为 1 被选择一次,产生一个 [1] 的方案。**

代码:
```Java
class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();
        List<Integer> cur = new ArrayList<>();
        dfs(nums, 0, cur, ans);
        return ans;
    }

    /**
     * @param nums 原输入数组
     * @param u 当前决策到原输入数组中的哪一位
     * @param cur 当前方案
     * @param ans 最终结果集
     */
    void dfs(int[] nums, int u, List<Integer> cur, List<List<Integer>> ans) {
        // 所有位置都决策完成,将当前方案放入结果集
        int n = nums.length;
        if (n == u) {
            ans.add(new ArrayList<>(cur));
            return;
        }

        // 记录当前位置是什么数值(令数值为 t),并找出数值为 t 的连续一段
        int t = nums[u];
        int last = u;
        while (last < n && nums[last] == nums[u]) last++;

        // 不选当前位置的元素,直接跳到 last 往下决策
        dfs(nums, last, cur, ans);

        // 决策选择不同个数的 t 的情况:选择 1 个、2 个、3 个 ... k 个
        for (int i = u; i < last; i++) {
            cur.add(nums[i]);
            dfs(nums, last, cur, ans);
        }

        // 回溯对数值 t 的选择
        for (int i = u; i < last; i++) {
            cur.remove(cur.size() - 1);
        }
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,爆搜复杂度为 $(2^n)$,每个方案通过深拷贝存入答案,复杂度为 $O(n)$。整体复杂度为 $(n \times 2^n)$
* 空间复杂度:总共有 $2^n$ 个方案,每个方案最多占用 $O(n)$ 空间,整体复杂度为 $(n \times 2^n)$

---

### 状态压缩(Set)

由于长度只有 10,我们可以使用一个 `int` 的后 $10$ 位来代表每位数组成员是否被选择。

同样,我们也需要先对原数组进行排序,再配合 `Set` 来进行去重。

代码:
```Java
class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        Set<List<Integer>> ans = new HashSet<>();
        List<Integer> cur = new ArrayList<>();

        // 枚举 i 代表,枚举所有的选择方案状态
        // 例如 [1,2],我们有 []、[1]、[2]、[1,2] 几种方案,分别对应了 00、10、01、11 几种状态
        for (int i = 0; i < (1 << n); i++) {
            cur.clear();
            // 对当前状态进行诸位检查,如果当前状态为 1 代表被选择,加入当前方案中
            for (int j = 0; j < n; j++) {
                int t = (i >> j) & 1;
                if (t == 1) cur.add(nums[j]);
            }
            // 将当前方案中加入结果集
            ans.add(new ArrayList<>(cur));
        }
        return new ArrayList<>(ans);
    }
}
```
* 时间复杂度:排序复杂度为 $O(n\log{n})$,爆搜复杂度为 $(2^n)$,每个方案通过深拷贝存入答案,复杂度为 $O(n)$。整体复杂度为 $(n \times 2^n)$
* 空间复杂度:总共有 $2^n$ 个方案,每个方案最多占用 $O(n)$ 空间,整体复杂度为 $(n \times 2^n)$

### 最后

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

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

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

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


811. 子域名访问计数

Question:
网站域名 `"discuss.leetcode.com"` 由多个子域名组成。顶级域名为 `"com"` ,二级域名为 `"leetcode.com"` ,最低一级为 `"discuss.leetcode.com"` 。

当访问域名 `"discuss.leetcode.com"` 时,同时也会隐式访问其父域名 `"leetcode.com"` 以及 `"com"` 。

计数配对域名 是遵循 `"rep d1.d2.d3"` 或 `"rep d1.d2"` 格式的一个域名表示,其中 `rep` 表示访问域名的次数,`d1.d2.d3` 为域名本身。

例如,`"9001 discuss.leetcode.com"` 就是一个 计数配对域名 ,表示 `discuss.leetcode.com` 被访问了 `9001` 次。

给你一个 计数配对域名 组成的数组 `cpdomains` ,解析得到输入中每个子域名对应的 计数配对域名 ,并以数组形式返回。可以按 任意顺序 返回答案。

示例 1:
```
输入:cpdomains = ["9001 discuss.leetcode.com"]

输出:["9001 leetcode.com","9001 discuss.leetcode.com","9001 com"]

解释:例子中仅包含一个网站域名:"discuss.leetcode.com"。
按照前文描述,子域名 "leetcode.com" 和 "com" 都会被访问,所以它们都被访问了 9001 次。
```
示例 2:
```
输入:cpdomains = ["900 google.mail.com", "50 yahoo.com", "1 intel.mail.com", "5 wiki.org"]

输出:["901 mail.com","50 yahoo.com","900 google.mail.com","5 wiki.org","5 org","1 intel.mail.com","951 com"]

解释:按照前文描述,会访问 "google.mail.com" 900 次,"yahoo.com" 50 次,"intel.mail.com" 1 次,"wiki.org" 5 次。
而对于父域名,会访问 "mail.com" 900 + 1 = 901 次,"com" 900 + 50 + 1 = 951 次,和 "org" 5 次。
```

提示:
* $1 <= cpdomain.length <= 100$
* $1 <= cpdomain[i].length <= 100$
* `cpdomain[i]` 会遵循 `"repi d1i.d2i.d3i"` 或 `"repi d1i.d2i"` 格式
* `repi` 是范围 $[1, 10^4]$ 内的一个整数
* $d1_i$、$d2_i$ 和 $d3_i$ 由小写英文字母组成

Solution:
### 哈希表

为了方便,我们令 `cpdomains` 为 `ss`。

根据题意进行模拟:使用哈希表记录每个域名的总访问次数,从前往后处理所有的 $ss[i]$。在处理某个 $ss[i]$ 时(记长度为 $n$,使用指针 `idx` 代指扫描到的游标位置),先通过指针扫描找到访问数字部分 `cnt = ss[i][0:idx]`,然后「从后往前」处理 $ss[i]$ 的 $[idx + 1, n - 1]$ 部分,按照域名层级「从小到大」的顺序进行截取,并累加访问次数 `cnt` 到当前域名。

最后根据哈希表构造答案。

Java 代码:
```Java
class Solution {
    public List<String> subdomainVisits(String[] ss) {
        Map<String, Integer> map = new HashMap<>();
        for (String s : ss) {
            int n = s.length(), idx = 0;
            while (idx < n && s.charAt(idx) != ' ') idx++;
            int cnt = Integer.parseInt(s.substring(0, idx));
            int start = idx + 1; idx = n - 1;
            while (idx >= start) {
                while (idx >= start && s.charAt(idx) != '.') idx--;
                String cur = s.substring(idx + 1);
                map.put(cur, map.getOrDefault(cur, 0) + cnt);
                idx--;
            }
        }
        List<String> ans = new ArrayList<>();
        for (String key : map.keySet()) ans.add(map.get(key) + " " + key);
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function subdomainVisits(ss: string[]): string[] {
    const map = new Map<string, number>()
    for (const s of ss) {
        let n = s.length, idx = 0
        while (idx < n && s[idx] != ' ') idx++
        const cnt = Number(s.substring(0, idx))
        const start = idx + 1; idx = n - 1
        while (idx >= start) {
            while (idx >= start && s[idx] != '.') idx--
            const cur = s.substring(idx + 1)
            if (!map.has(cur)) map.set(cur, 0)
            map.set(cur, map.get(cur) + cnt)
            idx--
        }
    }
    const ans = new Array<string>()
    for (const key of map.keys()) ans.push(map.get(key) + " " + key)
    return ans
};
```
Python 代码:
```Python
class Solution:
    def subdomainVisits(self, ss: List[str]) -> List[str]:
        mapping = defaultdict(int)
        for s in ss:
            n, idx = len(s), 0
            while idx < n and s[idx] != ' ':
                idx += 1
            cnt = int(s[:idx])
            start, idx = idx + 1, n - 1
            while idx >= start:
                while idx >= start and s[idx] != '.':
                    idx -= 1
                mapping[s[idx + 1:]] += cnt
                idx -= 1
        return [f'{v} {k}' for k, v in mapping.items()]
```
* 时间复杂度:$O(\sum_{i = 0}^{n - 1}len(ss[i]))$
* 空间复杂度:$O(\sum_{i = 0}^{n - 1}len(ss[i]))$

### 最后

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

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

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

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


812. 最大三角形面积

Question:
给定包含多个点的集合,从其中取三个点组成三角形,返回能组成的最大三角形的面积。

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

输出: 2

解释:
这五个点如下图所示。组成的橙色三角形是最大的,面积为2。
```

注意:
* $3 <= points.length <= 50.$
* 不存在重复的点。
* $-50 <= points[i][j] <= 50$
* 结果误差值在 $10^{-6}$ 以内都认为是正确答案。

Solution:
### 模拟

根据题意模拟即可:枚举三角形三个顶点并计算面积。

代码:
```Java
class Solution {
    public double largestTriangleArea(int[][] ps) {
        int n = ps.length;
        double ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    int cur = cross(ps[j][0] - ps[i][0], ps[j][1] - ps[i][1], ps[k][0] - ps[i][0], ps[k][1] - ps[i][1]);
                    ans = Math.max(ans, Math.abs(cur / 2.0));
                }
            }
        }
        return ans;
    }
    int cross(int a, int b, int c, int d) {
        return a * d - c * b;
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


813. 最大平均值和的分组

Question:
给定数组 `nums` 和一个整数 `m` 。

我们将给定的数组 `nums` 分成最多 `m` 个相邻的非空子数组,分数由每个子数组内的平均值的总和构成。

注意我们必须使用 `nums` 数组中的每一个数进行分组,并且分数不一定需要是整数。

返回我们所能得到的最大分数是多少。

答案误差在 $10^{-6}$ 内被视为是正确的。

示例 1:
```
输入: nums = [9,1,2,3,9], m = 3

输出: 20.00000

解释:
nums 的最优分组是[9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20.
我们也可以把 nums 分成[9, 1], [2], [3, 9].
这样的分组得到的分数为 5 + 2 + 6 = 13, 但不是最大值.
```
示例 2:
```
输入: nums = [1,2,3,4,5,6,7], m = 4

输出: 20.50000
```

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

Solution:
### 前缀和 + 序列 DP

题意可整理为一句话:将 $n$ 个元素划分为「最多」$m$ 个连续段,最大化连续段的平均值之和。

为了方便,我们令所有数组下标从 $1$ 开始。

定义 $f[i][j]$ 为考虑将前 $i$ 个元素划分成 $j$ 份的最大平均和,答案为 $f[n][k]$,其中 $1 \leq k \leq m$。

不失一般性考虑 $f[i][j]$ 该如何计算,由于划分出来的子数组不能是空集,因此我们可以根据 $j$ 的大小分情况讨论:

* 当 $j = 1$,此时有 $f[i][j] = \frac{\sum_{idx = 1}^{i} nums[idx - 1]}{i}$
* 当 $j > 1$,此时枚举最后一个子数组的起点 $k$,其中 $2 \leq k \leq i$,此时有平均值之和为 $f[k - 1][j - 1] + \frac{\sum_{idx = k}^{i} nums[idx]}{i - k + 1}$,最终 $f[i][j]$ 为枚举所有 $k$ 值的最大值

其中求解连续段之和可以用「前缀和」进行优化。同时,想要简化代码,还可以利用一个简单的数学结论:划分份数越多,平均值之和越大,因此想要取得最大值必然是恰好划分成 $m$ 份。

Java 代码:
```Java
class Solution {
    public double largestSumOfAverages(int[] nums, int m) {
        int n = nums.length;
        double[] sum = new double[n + 10];
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        double[][] f = new double[n + 10][m + 10];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= Math.min(i, m); j++) {
                if (j == 1) {
                    f[i][1] = sum[i] / i;
                } else {
                    for (int k = 2; k <= i; k++) {
                        f[i][j] = Math.max(f[i][j], f[k - 1][j - 1] + (sum[i] - sum[k - 1]) / (i - k + 1));
                    }
                }
            }
        }
        return f[n][m];
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    double largestSumOfAverages(vector<int>& nums, int m) {
        int n = nums.size();
        vector<double> sum(n + 10, 0);
        for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
        vector<vector<double>> f(n + 10, vector<double>(m + 10, 0));
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= min(i, m); j++) {
                if (j == 1) {
                    f[i][j] = sum[i] / i;
                } else {
                    for (int k = 2; k <= i; k++) {
                        f[i][j] = max(f[i][j], f[k - 1][j - 1] + (sum[i] - sum[k - 1]) / (i - k + 1));
                    }
                }
            }
        }
        return f[n][m];
    }
};
```
Python 代码:
```Python
class Solution:
    def largestSumOfAverages(self, nums: List[int], m: int) -> float:
        n = len(nums)
        psum = [0] * (n + 10)
        for i in range(1, n + 1):
            psum[i] = psum[i - 1] + nums[i - 1]
        f = [[0] * (m + 10) for _ in range(n + 10)]
        for i in range(1, n + 1):
            for j in range(1, min(i, m) + 1):
                if j == 1:
                    f[i][j] = psum[i] / i
                else:
                    for k in range(2, i + 1):
                        f[i][j] = max(f[i][j], f[k - 1][j - 1] + (psum[i] - psum[k - 1]) / (i - k + 1))
        return f[n][m]
```
TypeScript 代码:
```TypeScript
function largestSumOfAverages(nums: number[], m: number): number {
    const n = nums.length
    const sum = new Array<number>(n + 10).fill(0)
    for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1]
    const f = new Array<Array<number>>()
    for (let i = 0; i < n + 10; i++) f[i] = new Array<number>(m + 10).fill(0)
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= Math.min(i, m); j++) {
            if (j == 1) {
                f[i][j] = sum[i] / i
            } else {
                for (let k = 2; k <= i; k++) {
                    f[i][j] = Math.max(f[i][j], f[k - 1][j - 1] + (sum[i] - sum[k - 1]) / (i - k + 1))
                }
            }
        }
    }
    return f[n][m]
}
```
* 时间复杂度:$O(n^2 \times m)$
* 空间复杂度:$O(n \times m)$

### 最后

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

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

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

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


Question:
给你二叉树的根结点 `root`,此外树的每个结点的值要么是 $0$ ,要么是 $1$ 。

返回移除了所有不包含 $1$ 的子树的原二叉树。

节点 `node` 的子树为 `node` 本身加上所有 `node` 的后代。

示例 1:

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

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

解释:
只有红色节点满足条件“所有不包含 1 的子树”。 右图为返回的答案。
```
示例 2:

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

输出:[1,null,1,null,1]
```
示例 3:

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

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

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

Solution:
### 递归

根据题意,我们将原函数 `pruneTree` 作为递归函数,递归函数的含义为「将入参 `root` 中的所有不包含 $1$ 的子树移除,并返回新树头结点」。

不失一般性的考虑任意节点作为入参该如何处理:我们可以递归处理左右子树,并将新左右子树重新赋值给 `root`。由于当前节点 `root` 的左右子树可能为空树,因此我们要增加递归函数入参为空的边界处理。

当递归操作完成后,若左右节点任一值不为空(说明当前节点 `root` 不为叶子节点),我们可以直接返回 `root`,否则根据 `root` 的值是否为 $0$ 来决定返回空树还是 `root` 本身。

Java 代码:
```Java
class Solution {
    public TreeNode pruneTree(TreeNode root) {
        if (root == null) return null;
        root.left = pruneTree(root.left);
        root.right = pruneTree(root.right);
        if (root.left != null || root.right != null) return root;
        return root.val == 0 ? null : root;
    }
}
```
TypeScript 代码:
```TypeScript
function pruneTree(root: TreeNode | null): TreeNode | null {
    if (root == null) return null
    root.left = pruneTree(root.left)
    root.right = pruneTree(root.right)
    if (root.left != null || root.right != null) return root
    return root.val == 0 ? null : root
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
给你一个数组 `routes` ,表示一系列公交线路,其中每个 `routes[i]` 表示一条公交线路,第 `i` 辆公交车将会在上面循环行驶。

* 例如,路线 `routes[0] = [1, 5, 7]` 表示第 0 辆公交车会一直按序列 `1 -> 5 -> 7 -> 1 -> 5 -> 7 -> 1 -> ...` 这样的车站路线行驶。

现在从 `source` 车站出发(初始时不在公交车上),要前往 `target` 车站。 期间仅可乘坐公交车。

求出 最少乘坐的公交车数量 。如果不可能到达终点车站,返回 `-1` 。

示例 1:
```
输入:routes = [[1,2,7],[3,6,7]], source = 1, target = 6

输出:2

解释:最优策略是先乘坐第一辆公交车到达车站 7 , 然后换乘第二辆公交车到车站 6 。
```
示例 2:
```
输入:routes = [[7,12],[4,5,15],[6],[15,19],[9,12,13]], source = 15, target = 12

输出:-1
```

提示:
* 1 <= routes.length <= 500.
* 1 <= routes[i].length <= $10^5$
* routes[i] 中的所有值 互不相同
* sum(routes[i].length) <= $10^5$
* 0 <= routes[i][j] < $10^6$
* 0 <= source, target < $10^6$

Solution:
### 基本分析

为了方便,我们令每个公交站为一个「车站」,由一个「车站」可以进入一条或多条「路线」。

问题为从「起点车站」到「终点车站」,所进入的最少路线为多少。

抽象每个「路线」为一个点,当不同「路线」之间存在「公共车站」则为其增加一条边权为 $1$ 的无向边。

---

### 单向 BFS

由于是在边权为 $1$ 的图上求最短路,我们直接使用 `BFS` 即可。

起始时将「起点车站」所能进入的「路线」进行入队,每次从队列中取出「路线」时,查看该路线是否包含「终点车站」:

* 包含「终点车站」:返回进入该线路所花费的距离
* 不包含「终点车站」:遍历该路线所包含的车站,将由这些车站所能进入的路线,进行入队

一些细节:由于是求最短路,同一路线重复入队是没有意义的,因此将新路线入队前需要先判断是否曾经入队。

代码:
```Java
class Solution {
    int s, t;
    int[][] rs;
    public int numBusesToDestination(int[][] _rs, int _s, int _t) {
        rs = _rs; s = _s; t = _t;
        if (s == t) return 0;
        int ans = bfs();
        return ans;
    }
    int bfs() {
        // 记录某个车站可以进入的路线
        Map<Integer, Set<Integer>> map = new HashMap<>();
        // 队列存的是经过的路线
        Deque<Integer> d = new ArrayDeque<>();
        // 哈希表记录的进入该路线所使用的距离
        Map<Integer, Integer> m = new HashMap<>();
        int n = rs.length;
        for (int i = 0; i < n; i++) {
            for (int station : rs[i]) {
                // 将从起点可以进入的路线加入队列
                if (station == s) {
                    d.addLast(i);
                    m.put(i, 1);
                }
                Set<Integer> set = map.getOrDefault(station, new HashSet<>());
                set.add(i);
                map.put(station, set);
            }
        }
        while (!d.isEmpty()) {
            // 取出当前所在的路线,与进入该路线所花费的距离
            int poll = d.pollFirst();
            int step = m.get(poll);

            // 遍历该路线所包含的车站
            for (int station : rs[poll]) {
                // 如果包含终点,返回进入该路线花费的距离即可
                if (station == t) return step;

                // 将由该线路的车站发起的路线,加入队列
                Set<Integer> lines = map.get(station);
                if (lines == null) continue;
                for (int nr : lines) {
                    if (!m.containsKey(nr)) {
                        m.put(nr, step + 1);
                        d.add(nr);
                    }
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令路线的数量为 $n$,车站的数量为 $m$。建图的时间复杂度为 $O(\sum_{i=0}^{n-1} len(rs[i]))$;`BFS` 部分每个路线只会入队一次,最坏情况下每个路线都包含所有车站,复杂度为 $O(n * m)$。整体复杂度为 $O(n * m + \sum_{i=0}^{n-1} len(rs[i]))$。
* 空间复杂度:$O(n * m)$

---

### 双向 BFS(并查集预处理无解情况)

另外一个做法是使用双向 `BFS`。

首先建图方式不变,将「起点」和「终点」所能进入的路线分别放入两个方向的队列,如果「遇到公共的路线」或者「当前路线包含了目标位置」,说明找到了最短路径。

另外我们知道,双向 `BFS` 在无解的情况下不如单向 `BFS`。因此我们可以先使用「并查集」进行预处理,判断「起点」和「终点」是否连通,如果不联通,直接返回 $-1$,有解才调用双向 `BFS`。

由于使用「并查集」预处理的复杂度与建图是近似的,增加这样的预处理并不会越过我们时空复杂度的上限,因此这样的预处理是有益的。一定程度上可以最大化双向 `BFS` 减少搜索空间的效益。

代码:
```Java
class Solution {
    static int N = (int)1e6+10;
    static int[] p = new int[N];
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        p[find(a)] = p[find(b)];
    }
    boolean query(int a, int b) {
        return find(a) == find(b);
    }
    int s, t;
    int[][] rs;
    public int numBusesToDestination(int[][] _rs, int _s, int _t) {
        rs = _rs; s = _s; t = _t;
        if (s == t) return 0;
        for (int i = 0; i < N; i++) p[i] = i;
        for (int[] r : rs) {
            for (int loc : r) {
                union(loc, r[0]);
            }
        }
        if (!query(s, t)) return -1;
        int ans = bfs();
        return ans;
    }
    // 记录某个车站可以进入的路线
    Map<Integer, Set<Integer>> map = new HashMap<>();
    int bfs() {
        Deque<Integer> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        Map<Integer, Integer> m1 = new HashMap<>(), m2 = new HashMap<>();

        int n = rs.length;
        for (int i = 0; i < n; i++) {
            for (int station : rs[i]) {
                // 将从起点可以进入的路线加入正向队列
                if (station == s) {
                    d1.addLast(i);
                    m1.put(i, 1);
                }
                // 将从终点可以进入的路线加入反向队列
                if (station == t) {
                    d2.addLast(i);
                    m2.put(i, 1);
                }
                Set<Integer> set = map.getOrDefault(station, new HashSet<>());
                set.add(i);
                map.put(station, set);
            }
        }

        // 如果「起点所发起的路线」和「终点所发起的路线」有交集,直接返回 1
        Set<Integer> s1 = map.get(s), s2 = map.get(t);
        Set<Integer> tot = new HashSet<>();
        tot.addAll(s1);
        tot.retainAll(s2);
        if (!tot.isEmpty()) return 1;

        // 双向 BFS
        while (!d1.isEmpty() && !d2.isEmpty()) {
            int res = -1;
            if (d1.size() <= d2.size()) {
                res = update(d1, m1, m2);
            } else {
                res = update(d2, m2, m1);
            }
            if (res != -1) return res;
        }

        return 0x3f3f3f3f; // never
    }
    int update(Deque<Integer> d, Map<Integer, Integer> cur, Map<Integer, Integer> other) {
        int m = d.size();
        while (m-- > 0) {
            // 取出当前所在的路线,与进入该路线所花费的距离
            int poll = d.pollFirst();
            int step = cur.get(poll);

            // 遍历该路线所包含的车站
            for (int station : rs[poll]) {
                // 遍历将由该线路的车站发起的路线
                Set<Integer> lines = map.get(station);
                if (lines == null) continue;
                for (int nr : lines) {
                    if (cur.containsKey(nr)) continue;
                    if (other.containsKey(nr)) return step + other.get(nr);
                    cur.put(nr, step + 1);
                    d.add(nr);
                }
            }
        }
        return -1;
    }
}
```
* 时间复杂度:令路线的数量为 $n$,车站的个数为 $m$。并查集和建图的时间复杂度为 $O(\sum_{i=0}^{n-1} len(rs[i]))$;`BFS` 求最短路径的复杂度为 $O(n * m)$。整体复杂度为 $O(n * m + \sum_{i=0}^{n-1} len(rs[i]))$。
* 空间复杂度:$O(n * m + \sum_{i=0}^{n-1} len(rs[i]))$

### 最后

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

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

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

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


Question:
我们有一些二维坐标,如 `"(1, 3)"` 或 `"(2, 0.5)"`,然后我们移除所有逗号,小数点和空格,得到一个字符串 `S`。返回所有可能的原始字符串到一个列表中。

原始的坐标表示法不会存在多余的零,所以不会出现类似于 `"00", "0.0", "0.00", "1.0", "001", "00.01"` 或一些其他更小的数来表示坐标。

此外,一个小数点前至少存在一个数,所以也不会出现 `“.1”` 形式的数字。

最后返回的列表可以是任意顺序的。而且注意返回的两个数字中间(逗号之后)都有一个空格。

示例 1:
```
输入: "(123)"

输出: ["(1, 23)", "(12, 3)", "(1.2, 3)", "(1, 2.3)"]
```
示例 2:
```
输入: "(00011)"

输出:  ["(0.001, 1)", "(0, 0.011)"]

解释:
0.0, 00, 0001 或 00.01 是不被允许的。
```
示例 3:
```
输入: "(0123)"

输出: ["(0, 123)", "(0, 12.3)", "(0, 1.23)", "(0.1, 23)", "(0.1, 2.3)", "(0.12, 3)"]
```
示例 4:
```
输入: "(100)"

输出: [(10, 0)]

解释:
1.0 是不被允许的。
```

提示:
* $4 <= S.length <= 12$
* `S[0] = "(", S[S.length - 1] = ")"`, 且字符串 `S` 中的其他元素都是数字。

Solution:
### 枚举

我们先将原字符串 `s` 中的左右括号去掉,重新定义 `s` 为原字符串 $s[1...(n - 2)]$,重新定义后的 `s` 长度为 `n`。

随后枚举逗号的位置 `idx`,枚举范围为 $[0, n - 1)$,含义为在 $s[idx]$ 后面追加逗号。

此时左边部分字符串为 $s[0, idx]$,右边部分字符串为 $s[idx + 1, n - 1]$。

实现一个搜索函数 `List<String> search(int start, int end)`,该函数返回使用字符串 $s[start...end]$ 构造的具体数值集合。

假设左边字符串 $s[0, idx]$ 搜索结果为 `A`,右边字符串 $s[idx + 1, n - 1]$ 搜索结果为 `B`,根据「乘法原理」,可知所有实际方案为 `(x, y)` 其中 $x \in A$,$y \in B$。

考虑如何实现 `search` 函数(假设入参函数 `start` 和 `end` 对应的子串为 `sub`):枚举字符串追加小数点的位置 `idx`,枚举范围为 $[start, end - 1)$,含义为在 $sub[idx]$ 后面追加小数点。小数点前面的部分不能包含前导零,小数点后面的部分不能包含后导零。注意记得把不添加小数点的合法方案也存入搜索集合。

Java 代码:
```Java
class Solution {
    String s;
    public List<String> ambiguousCoordinates(String _s) {
        s = _s.substring(1, _s.length() - 1);
        int n = s.length();
        List<String> ans = new ArrayList<>();
        for (int i = 0; i < n - 1; i++) { // 枚举逗号:在 i 的后面追加逗号
            List<String> a = search(0, i), b = search(i + 1, n - 1);
            for (String x : a) {
                for (String y : b) {
                    ans.add("(" + x + ", " + y + ")");
                }
            }
        }
        return ans;
    }
    List<String> search(int start, int end) {
        List<String> ans = new ArrayList<>();
        if (start == end || s.charAt(start) != '0') ans.add(s.substring(start, end + 1));
        for (int i = start; i < end; i++) { // 枚举小数点:在 i 后面追加小数点
            String a = s.substring(start, i + 1), b = s.substring(i + 1, end + 1);
            if (a.length() > 1 && a.charAt(0) == '0') continue;
            if (b.charAt(b.length() - 1) == '0') continue;
            ans.add(a + "." + b);
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    string s;
    vector<string> ambiguousCoordinates(string _s) {
        s = _s.substr(1, _s.size() - 2);
        int n = s.size();
        vector<string> ans;
        for (int i = 0; i < n - 1; i++) {
            vector<string> a = search(0, i), b = search(i + 1, n - 1);
            for (auto &x : a) {
                for (auto &y : b) {
                    ans.push_back("(" + x + ", " + y + ")");
                }
            }
        }
        return ans;
    }
    vector<string> search(int start, int end) {
        vector<string> ans;
        if (start == end || s[start] != '0') ans.push_back(s.substr(start, end - start + 1));
        for (int i = start; i < end; i++) {
            string a = s.substr(start, i - start + 1), b = s.substr(i + 1, end - i);
            if (a.size() > 1 && a[0] == '0') continue;
            if (b.back() == '0') continue;
            ans.push_back(a + "." + b);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def ambiguousCoordinates(self, _s: str) -> List[str]:
        def search(s, start, end):
            ans = []
            if start == end or s[start] != '0':
                ans.append(s[start:end+1])
            for i in range(start, end):
                a, b = s[start:i+1], s[i+1:end+1]
                if len(a) > 1 and a[0] == '0':
                    continue
                if b[-1] == '0':
                    continue
                ans.append(f'{a}.{b}')
            return ans
        s = _s[1:len(_s)-1]
        n = len(s)
        ans = []
        for i in range(n - 1):
            a, b = search(s, 0, i), search(s, i + 1, n - 1)
            for x in a:
                for y in b:
                    ans.append(f'({x}, {y})')
        return ans
```
TypeScript 代码:
```TypeScript
function ambiguousCoordinates(_s: string): string[] {
    function search(s: string, start: number, end: number): string[] {
        const ans = new Array<string>()
        if (start == end || s[start] != '0') ans.push(s.substring(start, end + 1))
        for (let i = start; i < end; i++) {
            const a = s.substring(start, i + 1), b = s.substring(i + 1, end + 1)
            if (a.length > 1 && a[0] == '0') continue
            if (b[b.length - 1] == '0') continue
            ans.push(a + '.' + b)
        }
        return ans
    }
    const s = _s.substring(1, _s.length - 1)
    const n = s.length
    const ans = new Array<string>()
    for (let i = 0; i < n - 1; i++) {
        const a = search(s, 0, i), b = search(s, i + 1, n - 1)
        for (const x of a) {
            for (const y of b) {
                ans.push('(' + x + ', ' + y + ')')
            }
        }
    }
    return ans
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^3)$

### 最后

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

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

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

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


Question:
给定链表头结点 `head`,该链表上的每个结点都有一个 唯一的整型值 。同时给定列表 `nums`,该列表是上述链表中整型值的一个子集。

返回列表 `nums` 中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 `nums` 中)构成的集合。

示例 1:

```
输入: head = [0,1,2,3], nums = [0,1,3]

输出: 2

解释: 链表中,0 和 1 是相连接的,且 nums 中不包含 2,所以 [0, 1] 是 nums 的一个组件,同理 [3] 也是一个组件,故返回 2。
```
示例 2:

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

输出: 2

解释: 链表中,0 和 1 是相连接的,3 和 4 是相连接的,所以 [0, 1] 和 [3, 4] 是两个组件,故返回 2。
```

提示:
* 链表中节点数为 `n`
* $1 <= n <= 10^4$
* $0 <= Node.val < n$
* `Node.val` 中所有值 不同
* $1 <= nums.length <= n$
* $0 <= nums[i] < n$
* `nums` 中所有值 不同

Solution:
### 模拟

根据题意进行模拟即可 : 为了方便判断某个 $node.val$ 是否存在于 `nums` 中,我们先使用 `Set` 结构对所有的 $nums[i]$ 进行转存,随后每次检查连续段(组件)的个数。

Java 代码:
```Java
class Solution {
    public int numComponents(ListNode head, int[] nums) {
        int ans = 0;
        Set<Integer> set = new HashSet<>();
        for (int x : nums) set.add(x);
        while (head != null) {
            if (set.contains(head.val)) {
                while (head != null && set.contains(head.val)) head = head.next;
                ans++;
            } else {
                head = head.next;
            }
        }
        return ans;
    }
}
```
TypeScript 代码:
```TypeScript
function numComponents(head: ListNode | null, nums: number[]): number {
    let ans = 0
    const set = new Set<number>()
    for (const x of nums) set.add(x)
    while (head != null) {
        if (set.has(head.val)) {
            while (head != null && set.has(head.val)) head = head.next
            ans++
        } else {
            head = head.next
        }
    }
    return ans
}
```
Python 代码:
```Python
class Solution:
    def numComponents(self, head: Optional[ListNode], nums: List[int]) -> int:
        ans = 0
        nset = set([x for x in nums])
        while head:
            if head.val in nset:
                while head and head.val in nset:
                    head = head.next
                ans += 1
            else:
                head = head.next
        return ans
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个段落 (`paragraph`) 和一个禁用单词列表 (`banned`)。返回出现次数最多,同时不在禁用列表中的单词。

题目保证至少有一个词不在禁用列表中,而且答案唯一。

禁用列表中的单词用小写字母表示,不含标点符号。段落中的单词不区分大小写。答案都是小写字母。

示例:
```
输入:
paragraph = "Bob hit a ball, the hit BALL flew far after it was hit."
banned = ["hit"]

输出: "ball"

解释:
"hit" 出现了3次,但它是一个禁用的单词。
"ball" 出现了2次 (同时没有其他单词出现2次),所以它是段落里出现次数最多的,且不在禁用列表中的单词。
注意,所有这些单词在段落里不区分大小写,标点符号需要忽略(即使是紧挨着单词也忽略, 比如 "ball,"),
"hit"不是最终的答案,虽然它出现次数更多,但它在禁用单词列表中。
```

提示:
* $1 <= `段落长度` <= 1000$
* $0 <= `禁用单词个数` <= 100$
* $1 <= `禁用单词长度` <= 10$
* 答案是唯一的, 且都是小写字母 (即使在 `paragraph` 里是大写的,即使是一些特定的名词,答案都是小写的。)
* `paragraph` 只包含字母、空格和下列标点符号`!?',;.`
* 不存在没有连字符或者带有连字符的单词。
* 单词里只包含字母,不会出现省略号或者其他标点符号。

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    public String mostCommonWord(String s, String[] banned) {
        Set<String> set = new HashSet<>();
        for (String b : banned) set.add(b);
        char[] cs = s.toCharArray();
        int n = cs.length;
        String ans = null;
        Map<String, Integer> map = new HashMap<>();
        for (int i = 0; i < n; ) {
            if (!Character.isLetter(cs[i]) && ++i >= 0) continue;
            int j = i;
            while (j < n && Character.isLetter(cs[j])) j++;
            String sub = s.substring(i, j).toLowerCase();
            i = j + 1;
            if (set.contains(sub)) continue;
            map.put(sub, map.getOrDefault(sub, 0) + 1);
            if (ans == null || map.get(sub) > map.get(ans)) ans = sub;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n + m)$,$n$ 和 $m$ 分别代表 `s` 的字符总长度和 `banned` 的字符总长度(哈希函数的计算与长度成正比)
* 空间复杂度:$O(n + m)$

### 最后

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

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

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

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


821. 字符的最短距离

Question:
给你一个字符串 `s` 和一个字符 `c` ,且 `c` 是 `s` 中出现过的字符。

返回一个整数数组 `answer`,其中 $answer.length == s.length$ 且 $answer[i]$ 是 `s` 中从下标 $i$ 到离它 最近 的字符 `c` 的 距离 。

两个下标 $i$ 和 $j$ 之间的 距离 为 `abs(i - j)` ,其中 `abs` 是绝对值函数。

示例 1:
```
输入:s = "loveleetcode", c = "e"

输出:[3,2,1,0,1,0,0,1,2,2,1,0]

解释:字符 'e' 出现在下标 3、5、6 和 11 处(下标从 0 开始计数)。
距下标 0 最近的 'e' 出现在下标 3 ,所以距离为 abs(0 - 3) = 3 。
距下标 1 最近的 'e' 出现在下标 3 ,所以距离为 abs(1 - 3) = 2 。
对于下标 4 ,出现在下标 3 和下标 5 处的 'e' 都离它最近,但距离是一样的 abs(4 - 3) == abs(4 - 5) = 1 。
距下标 8 最近的 'e' 出现在下标 6 ,所以距离为 abs(8 - 6) = 2 。
```
示例 2:
```
输入:s = "aaab", c = "b"

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

提示:
* $1 <= s.length <= 10^4$
* $s[i]$ 和 `c` 均为小写英文字母
* 题目数据保证 `c` 在 `s` 中至少出现一次

Solution:
### 遍历

根据题意进行模拟即可:两次遍历,第一次找到每个 $i$ 左边最近的 `c`,第二次找到每个 $i$ 右边最近的 `c`。

代码:
```Java
class Solution {
    public int[] shortestToChar(String s, char c) {
        int n = s.length();
        int[] ans = new int[n];
        Arrays.fill(ans, n + 1);
        for (int i = 0, j = -1; i < n; i++) {
            if (s.charAt(i) == c) j = i;
            if (j != -1) ans[i] = i - j;
        }
        for (int i = n - 1, j = -1; i >= 0; i--) {
            if (s.charAt(i) == c) j = i;
            if (j != -1) ans[i] = Math.min(ans[i], j - i);
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### BFS

起始令所有的 $ans[i] = -1$,然后将所有的 `c` 字符的下标入队,并更新 $ans[i] = 0$,然后跑一遍 `BFS` 逻辑,通过 $ans[i]$ 是否为 $-1$ 来判断是否重复入队。

代码:
```Java
class Solution {
    public int[] shortestToChar(String s, char c) {
        int n = s.length();
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        Deque<Integer> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (s.charAt(i) == c) {
                d.addLast(i);
                ans[i] = 0;
            }
        }
        int[] dirs = new int[]{-1, 1};
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            for (int di : dirs) {
                int ne = t + di;
                if (ne >= 0 && ne < n && ans[ne] == -1) {
                    ans[ne] = ans[t] + 1;
                    d.addLast(ne);
                }
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个由若干单词组成的句子 `sentence`,单词间由空格分隔。每个单词仅由大写和小写英文字母组成。

请你将句子转换为 “山羊拉丁文(`Goat Latin`)”(一种类似于 猪拉丁文 - Pig Latin 的虚构语言)。山羊拉丁文的规则如下:

* 如果单词以元音开头(`'a'`, `'e'`, `'i'`, `'o'`, `'u'`),在单词后添加`"ma"`。
	* 例如,单词 `"apple"` 变为 `"applema"` 。
* 如果单词以辅音字母开头(即,非元音字母),移除第一个字符并将它放到末尾,之后再添加`"ma"`。
	* 例如,单词 `"goat"` 变为 `"oatgma"` 。
* 根据单词在句子中的索引,在单词最后添加与索引相同数量的字母`'a'`,索引从 $1$ 开始。
	* 例如,在第一个单词后添加 `"a"`,在第二个单词后添加 `"aa"`,以此类推。

返回将 `sentence` 转换为山羊拉丁文后的句子。

示例 1:
```
输入:sentence = "I speak Goat Latin"

输出:"Imaa peaksmaaa oatGmaaaa atinLmaaaaa"
```
示例 2:
```
输入:sentence = "The quick brown fox jumped over the lazy dog"

输出:"heTmaa uickqmaaa rownbmaaaa oxfmaaaaa umpedjmaaaaaa overmaaaaaaa hetmaaaaaaaa azylmaaaaaaaaa ogdmaaaaaaaaaa"
```

提示:
* $1 <= sentence.length <= 150$
* `sentence` 由英文字母和空格组成
* `sentence` 不含前导或尾随空格
* `sentence` 中的所有单词由单个空格分隔

Solution:
### 模拟

根据题意进行模拟即可。

代码:
```Java
class Solution {
    public String toGoatLatin(String s) {
        int n = s.length();
        String last = "a";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; ) {
            int j = i;
            while (j < n && s.charAt(j) != ' ') j++;
            if ("aeiouAEIOU".indexOf(s.charAt(i)) >= 0) {
                sb.append(s.substring(i, j)).append("ma");
            } else {
                sb.append(s.substring(i + 1, j)).append(s.charAt(i)).append("ma");
            }
            sb.append(last);
            last += "a";
            i = j + 1;
            if (i < n) sb.append(" ");
        }
        return sb.toString();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
在社交媒体网站上有 $n$ 个用户。给你一个整数数组 `ages` ,其中 $ages[i]$ 是第 $i$ 个用户的年龄。

如果下述任意一个条件为真,那么用户 $x$ 将不会向用户 $y$(`x != y`)发送好友请求:

* $age[y] <= 0.5 * age[x] + 7$
* $age[y] > age[x]$
* $age[y] > 100$ 且 $age[x] < 100$

否则,$x$ 将会向 $y$ 发送一条好友请求。

注意,如果 $x$ 向 $y$ 发送一条好友请求,$y$ 不必也向 $x$ 发送一条好友请求。另外,用户不会向自己发送好友请求。

返回在该社交媒体网站上产生的好友请求总数。

示例 1:
```
输入:ages = [16,16]

输出:2

解释:2 人互发好友请求。
```
示例 2:
```
输入:ages = [16,17,18]

输出:2

解释:产生的好友请求为 17 -> 16 ,18 -> 17 。
```
示例 3:
```
输入:ages = [20,30,100,110,120]

输出:3

解释:产生的好友请求为 110 -> 100 ,120 -> 110 ,120 -> 100 。
```

提示:
* $n == ages.length$
* $1 <= n <= 2 * 10^4$
* $1 <= ages[i] <= 120$

Solution:
### 排序 + 双指针

从三个不发送好友请求的条件来看,以 $y$ 的角度来说,可总结为:**年龄比我小的不考虑(同龄的可以),年龄比我大可以考虑,但是不能超过一定范围则不考虑。**

即对于一个确定的 $y$ 而言,会发送好友请求的 $x$ 范围为连续段:

**随着 $y$ 的逐渐增大,对应的 $x$ 连续段的左右边界均逐渐增大(数轴上均往右移动)。**

因此,我们可以先对 $ages$ 进行排序,枚举每个 $y = ages[k]$,同时使用 $i$ 和 $j$ 维护左右区间,$[i, j)$ 代表在 $ages$ 上会往 $y = ages[k]$ 发送请求的 $x$ 连续段,统计每个 $y = ages[k]$ 的 $x$ 有多少个即是答案,同时需要注意在 $[i, j)$ 范围内是包含 $y = ages[k]$ 自身,统计区间长度时需要进行 $-1$ 操作。

代码:
```Java
class Solution {
    public int numFriendRequests(int[] ages) {
        Arrays.sort(ages);
        int n = ages.length, ans = 0;
        for (int k = 0, i = 0, j = 0; k < n; k++) {
            while (i < k && !check(ages[i], ages[k])) i++;
            if (j < k) j = k;
            while (j < n && check(ages[j], ages[k])) j++;
            if (j > i) ans += j - i - 1;
        }
        return ans;
    }
    boolean check(int x, int y) {
        if (y <= 0.5 * x + 7) return false;
        if (y > x) return false;
        if (y > 100 && x < 100) return false;
        return true;
    }
}
```
* 时间复杂度:$O(n\log{n})$
* 空间复杂度:$O(\log{n})$

---

### 桶排序 + 前缀和

在解法一中,复杂度的上界在于「双轴快排」,利用本题数据范围 `1 <= ages[i] <= 120`,值域较小,我们可以通过「桶排序」的方式进行排序优化。

假设对 $ages$ 进行桶排后得到的数组为 $nums$,其中 $cnt = nums[i]$ 的含义为在 $ages$ 中年龄为 $i$ 的人有 $cnt$ 个。

同时,我们发现在解法一中,我们枚举 $y = ages[k]$,并使用 $i$ 和 $j$ 两个指针寻找连续的 $x$ 段的过程,$x$ 会始终停留于值与 $y = ages[k]$ 相等的最小下标处,而对于桶排数组而言,当前位置就是最小合法 $x$ 值(与 $y$ 相等),因此我们只需要找到最大合法 $x$ 值的位置即可(对应解法一的 $j$ 位置)。

同样,最大 $x$ 的位置在桶排数组中也是随着 $y$ 的增大(右移)逐渐增大(右移)。

剩下的问题在于,如何统计桶排数组中连续段下标的和为多少(有多少个合法 $x$ 值),这可以直接在桶排数组应用前缀和即可。

代码:
```Java
class Solution {
    int N = 130;
    public int numFriendRequests(int[] ages) {
        int[] nums = new int[N];
        for (int i : ages) nums[i]++;
        for (int i = 1; i < N; i++) nums[i] += nums[i - 1];
        int ans = 0;
        for (int y = 1, x = 1; y < N; y++) {
            int a = nums[y] - nums[y - 1]; // 有 a 个 y
            if (a == 0) continue;
            if (x < y) x = y;
            while (x < N && check(x, y)) x++;
            int b = nums[x - 1] - nums[y - 1] - 1; // [y, x) 为合法的 x 范围,对于每个 y 而言,有 b 个 x
            if (b > 0) ans += b * a;
        }
        return ans;
    }
    boolean check(int x, int y) {
        if (y <= 0.5 * x + 7) return false;
        if (y > x) return false;
        if (y > 100 && x < 100) return false;
        return true;
    }
}
```
* 时间复杂度:令 $C$ 为年龄值域大小,对于本题 $C$ 固定为 $130$。复杂度为 $O(\max(n, C))$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
给你一个大小为 `n x n` 二进制矩阵 `grid` 。最多 只能将一格 `0` 变成 `1` 。

返回执行此操作后,`grid` 中最大的岛屿面积是多少?

岛屿 由一组上、下、左、右四个方向相连的 `1` 形成。

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

输出: 3

解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。
```
示例 2:
```
输入: grid = [[1, 1], [1, 0]]

输出: 4

解释: 将一格0变成1,岛屿的面积扩大为 4。
```
示例 3:
```
输入: grid = [[1, 1], [1, 1]]

输出: 4

解释: 没有0可以让我们变成1,面积依然为 4。
```

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

Solution:
### 并查集 + 枚举

为了方便,我们令 `grid` 为 `g`。

根据题意,容易想到通过「并查集」来维护所有连通块大小,再通过「枚举」来找最优翻转点。

具体的,我们可以先使用「并查集」维护所有 $g[i][j] = 1$ 的块连通性,并在维护连通性的过程中,使用 `sz[idx]` 记录下每个连通块的大小(注意:只有连通块根编号,`sz[idx]` 才有意义,即只有 `sz[find(x)]` 才有意义)。

随后我们再次遍历 `g`,根据原始的 $g[i][j]$ 的值进行分别处理:

* 若 $g[i][j] = 1$,该位置不会作为翻转点,但真实最大面积未必是由翻转后所导致的(可能取自原有的连通块),因此我们需要将 $sz[root]$ 参与比较,其中 `root` 为 $(i, j)$ 所属的连通块根节点编号;
* 若 $g[i][j] = 0$,该位置可作为翻转点,我们可以统计其四联通位置对应的连通块大小总和 `tot`(注意若四联通方向有相同连通块,只统计一次),那么 $tot + 1$ 即是翻转该位置所得到的新连通块大小。

最后对所有连通块大小取最大值即是答案。

> 一些细节:为了方便,我们令点 $(i, j)$ 的编号从 $1$ 开始;
同时由于我们本身就要用 `sz` 数组,因此我们可以随手把并查集的「按秩合并」也加上。体现在 `union` 操作时,我们总是将小的连通块合并到大的连通块上,从而确保我们并查集单次操作即使在最坏情况下复杂度仍为 $O(\alpha(n))$(可看作常数)。需要注意只有同时应用「路径压缩」和「按秩合并」,并查集操作复杂度才为 $O(\alpha(n))$。

Java 代码:
```Java
class Solution {
    static int N = 510;
    static int[] p = new int[N * N], sz = new int[N * N];
    int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void union(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return ;
        if (sz[ra] > sz[rb]) {
            union(b, a);
        } else {
            sz[rb] += sz[ra]; p[ra] = p[rb];
        }
    }
    public int largestIsland(int[][] g) {
        int n = g.length;
        for (int i = 1; i <= n * n; i++) {
            p[i] = i; sz[i] = 1;
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 0) continue;
                for (int[] di : dirs) {
                    int x = i + di[0], y = j + di[1];
                    if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue;
                    union(i * n + j + 1, x * n + y + 1);
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1) {
                    ans = Math.max(ans, sz[find(i * n + j + 1)]);
                } else {
                    int tot = 1;
                    Set<Integer> set = new HashSet<>();
                    for (int[] di : dirs) {
                        int x = i + di[0],y = j + di[1];
                        if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue;
                        int root = find(x * n + y + 1);
                        if (set.contains(root)) continue;
                        tot += sz[root];
                        set.add(root);
                    }
                    ans = Math.max(ans, tot);
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int p[510 * 510], sz[510 * 510];
    vector<vector<int>> dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    int find(int x) {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    void unionX(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return;
        if (sz[ra] > sz[rb]) {
            unionX(b, a);
        } else {
            sz[rb] += sz[ra];
            p[ra] = p[rb];
        }
    }
    int largestIsland(vector<vector<int>>& g) {
        int n = g.size();
        for (int i = 1; i <= n * n; i++) {
            p[i] = i; sz[i] = 1;
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 0) continue;
                for (const auto& di : dirs) {
                    int x = i + di[0], y = j + di[1];
                    if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue;
                    unionX(i * n + j + 1, x * n + y + 1);
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (g[i][j] == 1) {
                    ans = max(ans, sz[find(i * n + j + 1)]);
                } else {
                    int tot = 1;
                    unordered_set<int> set;
                    for (const auto& di : dirs) {
                        int x = i + di[0], y = j + di[1];
                        if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue;
                        int root = find(x * n + y + 1);
                        if (set.find(root) != set.end()) continue;
                        tot += sz[root];
                        set.insert(root);
                    }
                    ans = max(ans, tot);
                }
            }
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def find(self, p, x):
        if p[x] != x:
            p[x] = self.find(p, p[x])
        return p[x]

    def union(self, p, sz, a, b):
        ra, rb = self.find(p, a), self.find(p, b)
        if ra == rb:
            return
        if sz[ra] > sz[rb]:
            ra, rb = rb, ra
        sz[rb] += sz[ra]
        p[ra] = p[rb]

    def largestIsland(self, g: List[List[int]]) -> int:
        n, ans = len(g), 0
        p, sz = [i for i in range(n * n + 10)], [1 for _ in range(n * n + 10)]
        dirs = [[1,0],[-1,0],[0,1],[0,-1]]
        for i in range(n):
            for j in range(n):
                if g[i][j] == 0:
                    continue
                for di in dirs:
                    x, y = i + di[0], j + di[1]
                    if x < 0 or x >= n or y < 0 or y >= n or g[x][y] == 0:
                        continue
                    self.union(p, sz, i * n + j + 1, x * n + y + 1)
        for i in range(n):
            for j in range(n):
                if g[i][j] == 1:
                    ans = max(ans, sz[self.find(p, i * n + j + 1)])
                else:
                    tot = 1
                    vis = set()
                    for di in dirs:
                        x, y = i + di[0], j + di[1]
                        if x < 0 or x >= n or y < 0 or y >= n or g[x][y] == 0:
                            continue
                        root = self.find(p, x * n + y + 1)
                        if root in vis:
                            continue
                        tot += sz[root]
                        vis.add(root)
                    ans = max(ans, tot)
        return ans
```
TypeScript 代码:
```TypeScript
const N = 510
const p = new Array<number>(N * N).fill(-1), sz = new Array<number>(N * N).fill(1)
const dirs = [[1,0], [-1,0], [0,1], [0,-1]]
function find(x: number): number {
    if (p[x] != x) p[x] = find(p[x])
    return p[x]
}
function union(a: number, b: number): void {
    const ra = find(a), rb = find(b)
    if (ra == rb) return
    if (sz[ra] > sz[rb]) {
        union(rb, ra)
    } else {
        sz[rb] += sz[ra]; p[ra] = p[rb]
    }
}
function largestIsland(g: number[][]): number {
    const n = g.length
    for (let i = 1; i <= n * n; i++) {
        p[i] = i; sz[i] = 1
    }
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (g[i][j] == 0) continue
            for (const di of dirs) {
                const x = i + di[0], y = j + di[1]
                if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue
                union(i * n + j + 1, x * n + y + 1)
            }
        }
    }
    let ans = 0
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
            if (g[i][j] == 1) {
                ans = Math.max(ans, sz[find(i * n + j + 1)])
            } else {
                let tot = 1
                const set = new Set()
                for (let di of dirs) {
                    const x = i + di[0], y = j + di[1]
                    if (x < 0 || x >= n || y < 0 || y >= n || g[x][y] == 0) continue
                    const root = find(x * n + y + 1)
                    if (set.has(root)) continue
                    tot += sz[root]
                    set.add(root)
                }
                ans = Math.max(ans, tot)
            }
        }
    }
    return ans
};
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n^2)$

### 最后

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

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

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

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


828. 统计子串中的唯一字符

Question:
我们定义了一个函数 `countUniqueChars(s)` 来统计字符串 `s` 中的唯一字符,并返回唯一字符的个数。

例如:`s = "LEETCODE"` ,则其中 `"L"`, `"T"`,`"C"`,`"O"`,`"D"` 都是唯一字符,因为它们只出现一次,所以 `countUniqueChars(s) = 5`。

本题将会给你一个字符串 `s` ,我们需要返回 `countUniqueChars(t)` 的总和,其中 `t` 是 `s` 的子字符串。

输入用例保证返回值为 `32` 位整数。

注意,某些子字符串可能是重复的,但你统计时也必须算上这些重复的子字符串(也就是说,你必须统计 `s` 的所有子字符串中的唯一字符)。

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

输出: 10

解释: 所有可能的子串为:"A","B","C","AB","BC" 和 "ABC"。
     其中,每一个子串都由独特字符构成。
     所以其长度总和为:1 + 1 + 1 + 2 + 2 + 3 = 10
```
示例 2:
```
输入: s = "ABA"

输出: 8

解释: 除了 countUniqueChars("ABA") = 1 之外,其余与示例 1 相同。
```
示例 3:
```
输入:s = "LEETCODE"

输出:92
```

提示:
* $1 <= s.length <= 10^5$
* `s` 只包含大写英文字符

Solution:
### 模拟 + 乘法原理

这道题和 [907. 子数组的最小值之和](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247493485&idx=1&sn=c6d6ac25c171e2617c5a95173aa58825) 如出一辙,只不过无须考虑运用「单调栈」。

原问题为求所有子数组的唯一字符数量和,其可等价为求每个 $s[i]$ 对答案的贡献,即每个 $s[i]$ 可作为多少个子数组的唯一元素。

假定我们能预处理出两数组 `l` 和 `r` 分别代表 $s[i]$ 作为子数组唯一字符时,其所能到达的最远两端:
* `l[i] = a` 代表下标 $a$ 为 $s[i]$ 能够作为子数组唯一字符时的最远左边界,即为 $s[i]$ 左边第一个与 $s[i]$ 值相同的位置(若不存在,则为 $a = -1$)
* `r[i] = b` 代表跳表 $b$ 为 $s[i]$ 能够作为子数组唯一字符时的最远右边界,即为 $s[i]$ 右边第一个与 $s[i]$ 值相同的位置(若不存在,则为 $b = n$)

子数组左端点个数为 $(i - a)$ 个,右端点个数为 $(b - i)$ 个,根据乘法原理可知,子数组个数为两者乘积。

预处理 `l` 和 `r` 只需要使用遍历计数即可。

Java 代码:
```Java
class Solution {
    public int uniqueLetterString(String s) {
        char[] cs = s.toCharArray();
        int n = cs.length, ans = 0;
        int[] l = new int[n], r = new int[n];
        int[] idx = new int[26];
        Arrays.fill(idx, -1);
        for (int i = 0; i < n; i++) {
            int u = cs[i] - 'A';
            l[i] = idx[u];
            idx[u] = i;
        }
        Arrays.fill(idx, n);
        for (int i = n - 1; i >= 0; i--) {
            int u = cs[i] - 'A';
            r[i] = idx[u];
            idx[u] = i;
        }
        for (int i = 0; i < n; i++) ans += (i - l[i]) * (r[i] - i);
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int uniqueLetterString(string s) {
        int n = s.size(), ans = 0;
        vector<int> l(n, -1), r(n, n);
        vector<int> idx(26, -1);
        for (int i = 0; i < n; i++) {
            int u = s[i] - 'A';
            l[i] = idx[u];
            idx[u] = i;
        }
        fill(idx.begin(), idx.end(), n);
        for (int i = n - 1; i >= 0; i--) {
            int u = s[i] - 'A';
            r[i] = idx[u];
            idx[u] = i;
        }
        for (int i = 0; i < n; i++) ans += (i - l[i]) * (r[i] - i);
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def uniqueLetterString(self, s: str) -> int:
        n = len(s)
        l, r = [-1] * n, [n] * n
        idx = [-1] * 26
        for i in range(n):
            u = ord(s[i]) - ord('A')
            l[i] = idx[u]
            idx[u] = i
        idx = [n] * 26
        for i in range(n - 1, -1, -1):
            u = ord(s[i]) - ord('A')
            r[i] = idx[u]
            idx[u] = i
        return sum((i - l[i]) * (r[i] - i) for i in range(n))
```
Typescript 代码:
```Typescript
function uniqueLetterString(s: string): number {
    let n = s.length, ans = 0
    const l = new Array<number>(n), r = new Array<number>(n)
    const idx = new Array<number>(26).fill(-1)
    for (let i = 0; i < n; i++) {
        const u = s.charCodeAt(i) - 65
        l[i] = idx[u]
        idx[u] = i
    }
    idx.fill(n)
    for (let i = n - 1; i >= 0; i--) {
        const u = s.charCodeAt(i) - 65
        r[i] = idx[u]
        idx[u] = i
    }
    for (let i = 0; i < n; i++) ans += (i - l[i]) * (r[i] - i)
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 线性 DP

另外一个实现思路是利用「动态规划」思想。

定义 $f[i]$ 为考虑以 $s[i]$ 为结尾的所有子串中的唯一字符个数。

不失一般性考虑 $f[i]$ 该如何转移:以 $s[i]$ 为结尾的子串包括在所有以 $s[i - 1]$ 为结尾的子串结尾添加一个字符而来,以及 $s[i]$ 字符本身组成的新子串。

首先我们令 $f[i] = f[i - 1]$,同时使用 $b[x]$ 记录字符 $x$ 前一次出现的下标,使用 $a[x]$ 记录字符 $x$ 在上上次出现的下标,然后假设当前处理的字符为 $c = s[i]$,考虑 $s[i]$ 对 $f[i]$ 的影响(注意 $s[i]$ 始终为子串右端点):

* 在子串左端点下标范围在 $[b[c] + 1, i]$ 的子串中,$s[i]$ 必然只出现一次(满足唯一字符要求),即可增加 $i - b[c]$ 个唯一字符 $s[i]$;
* 在子串左端点下标范围在 $[a[c] + 1, b[c]]$ 的子串中,原本位于 $b[c]$ 的字符在新子串中出现次数变为 $2$ 次(不再满足唯一字符要求),即需减少 $b[c] - a[c]$ 个唯一字符 $s[i]$。

综上,我们有状态转移方程:$f[i] = f[i - 1] + (i - b[s[i]]) - (b[s[i]] - a[s[i]])$

实现上,由于 $f[i]$ 只依赖于 $f[i - 1]$,因此我们无须真的创建动规数组,而只需要使用单个变量 `cur` 来记录当前处理到的 $f[i]$ 即可,累积所有的 $f[i]$ 即是答案。

Java 代码:
```Java
class Solution {
    public int uniqueLetterString(String s) {
        int n = s.length(), ans = 0, cur = 0;
        int[] a = new int[26], b = new int[26];
        Arrays.fill(a, -1); Arrays.fill(b, -1);
        for (int i = 0; i < n; i++) {
            int u = s.charAt(i) - 'A';
            cur += i - b[u] - (b[u] - a[u]);
            ans += cur;
            a[u] = b[u]; b[u] = i;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int uniqueLetterString(string s) {
        int n = s.length(), ans = 0, cur = 0;
        vector<int> a(26, -1), b(26, -1);
        for (int i = 0; i < n; ++i) {
            int u = s[i] - 'A';
            cur += i - b[u] - (b[u] - a[u]);
            ans += cur;
            a[u] = b[u]; b[u] = i;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def uniqueLetterString(self, s: str) -> int:
        n, ans, cur = len(s), 0, 0
        a, b = [-1] * 26, [-1] * 26
        for i in range(n):
            u = ord(s[i]) - ord('A')
            cur += i - b[u] - (b[u] - a[u])
            ans += cur
            a[u], b[u] = b[u], i
        return ans
```
TypeScript 代码:
```TypeScript
function uniqueLetterString(s: string): number {
    let n = s.length, ans = 0, cur = 0
    const a = new Array<number>(26).fill(-1), b = new Array<number>(26).fill(-1)
    for (let i = 0; i < n; i++) {
        const u = s.charCodeAt(i) - 65
        cur += i - b[u] - (b[u] - a[u])
        ans += cur
        a[u] = b[u]; b[u] = i
    }
    return ans
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$,其中 $C = 26$ 为字符集大小

### 最后

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

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

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

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


Question:
给定一个正整数 $n$,返回连续正整数满足所有数字之和为 $n$ 的组数 。

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

输出: 2

解释: 5 = 2 + 3,共有两组连续整数([5],[2,3])求和后为 5。
```
示例 2:
```
输入: n = 9

输出: 3

解释: 9 = 4 + 5 = 2 + 3 + 4
```
示例 3:
```
输入: n = 15

输出: 4

解释: 15 = 8 + 7 = 4 + 5 + 6 = 1 + 2 + 3 + 4 + 5
```

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

Solution:
### 数论

假设我们存在某个连续段之和为 $n$,假定该连续段首项为 $a$,长度为 $k$,根据「等差数列求和」可知:

$$
\frac{(a + a + k - 1) \times k}{2} = n
$$

简单变形可得:

$$
(2a + k - 1) \times k = 2n \Leftrightarrow 2a = \frac{2n}{k} - k + 1
$$

根据首项 $a$ 和 $k$ 均为正整数,可得:

$$
2a = \frac{2n}{k} - k + 1 \geq 2
$$

进一步可得:

$$
\frac{2n}{k} \geq k + 1 \Leftrightarrow \frac{2n}{k} > k
$$

综上,根据 $(2a + k - 1) \times k = 2n$ 和 $\frac{2n}{k} > k$ 可知,$k$ 必然是 $2n$ 的约数,并且为「较小」的约数。

因此我们可以在 $[1, \sqrt{2n})$ 范围内枚举 $k$,如果 $k$ 为 $2n$ 约数,并且结合 $(2a + k - 1) \times k = 2n$ 可验证 $a$ 合法,说明找到了一组合法的 $(a, k)$,对答案进行累加。

Java 代码:
```Java
class Solution {
    public int consecutiveNumbersSum(int n) {
        int ans = 0; n *= 2;
        for (int k = 1; k * k < n; k++) {
            if (n % k != 0) continue;
            if ((n / k - (k - 1)) % 2 == 0) ans++;
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int consecutiveNumbersSum(int n) {
        int ans = 0; n *= 2;
        for (int k = 1; k * k < n; k++) {
            if (n % k != 0) continue;
            if ((n / k - (k - 1)) % 2 == 0) ans++;
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def consecutiveNumbersSum(self, n: int) -> int:
        ans, n = 0, n * 2
        for k in range(1, int(n ** 0.5) + 1):
            ans += n % k == 0 and (n // k - (k - 1)) % 2 == 0
        return ans
```
TypeScript 代码:
```TypeScript
function consecutiveNumbersSum(n: number): number {
    let ans = 0; n *= 2;
    for (let k = 1; k * k < n; k++) {
        if (n % k != 0) continue;
        if ((n / k - (k - 1)) % 2 == 0) ans++;
    }
    return ans;
};
```
* 时间复杂度:$O(\sqrt{2n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给定一个二进制矩阵 A,我们想先水平翻转图像,然后反转图像并返回结果。

水平翻转图片就是将图片的每一行都进行翻转,即逆序。例如,水平翻转 [1, 1, 0] 的结果是 [0, 1, 1]。

反转图片的意思是图片中的 0 全部被 1 替换, 1 全部被 0 替换。例如,反转 [0, 1, 1] 的结果是 [1, 0, 0]。

示例 1:
```
输入:[[1,1,0],[1,0,1],[0,0,0]]
输出:[[1,0,0],[0,1,0],[1,1,1]]
解释:首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];
     然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
```
示例 2:
```
输入:[[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
输出:[[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解释:首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
     然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
```

提示:
* 1 <= A.length = A[0].length <= 20
* 0 <= A[i][j] <= 1

Solution:
### 双指针代码

对于每行而言,我们都需要对其进行「翻转」和「反转」。

这两步可以到放到一遍循环里做:
* 翻转部分:使用双指针进行数字交换
* 反转部分:将数字存储进目标位置前,使用「异或」对 0 1 进行翻转

当前有一些「小细节」需要注意:
1. 题目要求我们对参数图像进行翻转,并返回新图像。因此我们不能对输入直接进行修改,而要先进行拷贝再处理
2. 由于我们将「翻转」和「反转」合成了一步,因此对于「奇数」图像,需要对中间一列进行特殊处理:仅「反转」

对于 Java 的基本类型拷贝,有三种方式进行拷贝:
1. System.arraycopy() : 底层的数组拷贝接口,具体实现与操作系统相关,调用的是系统本地方法。需要自己创建好目标数组进行传入,可指定拷贝长度,实现局部拷贝。
2. Arrays.copyOf() : 基于 `System.arraycopy()` 封装的接口,省去了自己目标数组这一步。但无法实现局部拷贝。
3. clone() : Object 的方法。会调用每个数组成员的 clone() 方法进行拷贝。因此对于一维数组而言,可以直接使用 clone() 得到「深拷贝数组」,而对于多维数组而言,得到的是「浅拷贝数组」。

```java []
class Solution {
    public int[][] flipAndInvertImage(int[][] a) {
        int n = a.length;
        int[][] ans = new int[n][n];
        for (int i = 0; i < n; i++) {
            // ans[i] = a[i].clone();
            // ans[i] = Arrays.copyOf(a[i], n);
            System.arraycopy(a[i], 0, ans[i], 0, n);
            int l = 0, r = n - 1;
            while (l < r) {
                int c = ans[i][r];
                ans[i][r--] = ans[i][l] ^ 1;
                ans[i][l++] = c ^ 1;
            }
            if (n % 2 != 0) ans[i][r] ^= 1;
        }
        return ans;
    }
}
```
```java []
class Solution {
    public int[][] flipAndInvertImage(int[][] a) {
        int n = a.length;
        int[][] ans = new int[n][n];

        // 遍历每一行进行处理
        for (int i = 0; i < n; i++) {
            // 对每一行进行拷贝(共三种方式)
            // ans[i] = a[i].clone();
            // ans[i] = Arrays.copyOf(a[i], n);
            System.arraycopy(a[i], 0, ans[i], 0, n);

            // 使用「双指针」对其进行数组交换,实现「翻转」
            // 并通过「异或」进行 0 1 翻转,实现「反转」
            int l = 0, r = n - 1;
            while (l < r) {
                int c = ans[i][r];
                ans[i][r--] = ans[i][l] ^ 1;
                ans[i][l++] = c ^ 1;
            }

            // 由于「奇数」矩形的中间一列不会进入上述双指针逻辑
            // 需要对其进行单独「反转」
            if (n % 2 != 0) ans[i][r] ^= 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:使用了同等大小的空间存储答案。复杂度为 $O(n^2)$

***
### 补充

Q: 那么 Arrays.copyOfRange() 与 System.arraycopy() 作用是否等同呢?

A: 不等同。

Arrays.copyOf() 和 Arrays.copyOfRange() 都会内部创建目标数组。前者是直接创建一个和源数组等长的数组,而后者则是根据传参 to 和 from 计算出目标数组长度进行创建。

它们得到的数组都是完整包含了要拷贝内容的,都无法实现目标数组的局部拷贝功能。

例如我要拿到一个长度为 10 的数组,前面 5 个位置的内容来源于「源数组」的拷贝,后面 5 个位置我希望预留给我后面自己做操作,它们都无法满足,只有 System.arraycopy() 可以。

### 最后

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

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

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

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


Question:
给定一个无向、连通的树。

树中有 `n` 个标记为 `0...n-1` 的节点以及 `n-1` 条边 。

给定整数 `n` 和数组 `edges`, $edges[i] = [a_{i}, b_{i}]$表示树中的节点 $a_{i}$ 和 $b_{i}$ 之间有一条边。

返回长度为 `n` 的数组 `answer`,其中 `answer[i]` 是树中第 `i` 个节点与所有其他节点之间的距离之和。

示例 1:

```
输入: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]]

输出: [8,12,6,10,10,10]

解释: 树如图所示。
我们可以计算出 dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5)
也就是 1 + 1 + 2 + 2 + 2 = 8。 因此,answer[0] = 8,以此类推。
```
示例 2:

```
输入: n = 1, edges = []

输出: [0]
```
示例 3:

```
输入: n = 2, edges = [[1,0]]

输出: [1,1]
```

提示:
* $1 <= n <= 3 \times 10^4$
* $edges.length = n - 1$
* $edges[i].length = 2$
* $0 <= a_{i}, b_{i} < n$
* $a_{i} != b_{i}$
* 给定的输入保证为有效的树

Solution:
### 树形 DP

**对于树形 DP,可以随便以某个节点为根,把整棵树“拎起来”进行分析,通常还会以“方向”作为切入点进行思考。**

不妨以编号为 `0` 的节点作为根节点进行分析:假设当前处理到的节点为 `u`,当前节点 `u` 的父节点为 `fa`,同时 `u` 有若干子节点 `j`。

对于任意节点 `u` 而言,其树中距离之和可根据「方向/位置」分为两大类(对应示例图的左右两部分):

* 所有从节点 `u` “往下”延伸所达的节点距离之和,即所有经过 `u -> j` 边所能访问到的节点距离之和
* 所有从节点 `u` “往上”延伸所达的节点距离之和,即经过 `u -> fa` 边所能访问到的节点距离之和

假设我们能够用 $f[u]$ 和 $g[u]$ 预处理出每个节点“往下”和“往上”的距离之和,那么就有 $ans[u] = f[u] + g[u]$。

不失一般性分别考虑 $f[u]$ 和 $g[u]$ 该如何计算。

为了方便,起始先用「链式前向星」对 `edges` 进行转存,同时在递归计算 $f[u]$ 时,将父节点 `fa` 也进行传递,从而避免遍历节点 `u` 的出边时,重新走回 `fa` 。

##### $f[u]$ 的推导

对于叶子节点(没有“往下”出边的节点),我们有 $f[u] = 0$ 的天然条件,计算好的叶子节点值可用于更新其父节点,因此**求解 $f[u]$ 是一个「从下往上」的递推过程**。

假设当前处理到的节点是 `u`,往下节点有 $j_{1}$、$j_{2}$ 和 $j_{3}$ ,且所有 $f[j]$ 均已计算完成。

由于 $f[u]$ 是由所有存在“往下”出边的节点 `j` 贡献而来。而单个子节点 `j` 来说,其对 $f[u]$ 的贡献应当是:**在所有原有节点到节点 `j` 的距离路径中,额外增加一条当前出边(`u -> j`),再加上 `1`(代表节点 `u` 到节点 `j` 的距离)**。

原路径距离之和恰好是 $f[j]$,额外需要增加的出边数量为原来参与计算 $f[j]$ 的点的数量(即挂载在节点 `j` 下的数量),因此我们还需要一个 `c` 数组,来记录某个节点下的子节点数量。

最终的 $f[u]$ 为所有符合条件的节点的 `j` 的 $f[j] + c[j] + 1$ 的总和。

##### $g[u]$ 的推导

对于树形 DP 题目,“往下”的计算往往是容易的,而“往上”的计算则是稍稍麻烦。

假设当前我们处理到节点为 `u`,将要遍历的节点为 `j`,考虑如何使用已经计算好的 $f[X]$ 来求解 $g[j]$。

这里为什么是求解 $g[j]$,而不是 $g[u]$ 呢?

因为我们求解的方向是“往上”的部分,必然是用父节点的计算结果,来推导子节点的结果,即**求解 $g[u]$ 是一个「从上往下」的过程**。

对于树形 DP ,通常需要对“往上”进一步拆分:「往上再往上」和「往上再往下」:

* 往上再往上:是指经过了 `j -> u` 后,还必然经过 `u -> fa` 这条边时,所能到达的节点距离之和:

  这部分对 $g[j]$ 的贡献为:在所有原有节点到节点 `u` 的距离路径中,额外增加一条当前出边(`u -> j`),增加当前出边的数量与节点数量相同,点数量为 $n - 1 - c[u]$,含义为 总节点数量 减去 `u` 节点以及子节点数量。

  即此部分对 $g[j]$ 的贡献为 $g[u] + n - 1 - c[u]$。

* 往上再往下:是指经过了 `j -> u` 后,还经过「除 `u -> j` 以外」的其他“往下”边时,所能到达的节点距离之和:

  这部分的计算需要先在 $f[u]$ 中剔除 $f[j]$ 的贡献,然后再加上额外边(`u -> j`)的累加数量,同样也是节点数量。

  从 $f[u]$ 中剔除 $f[j]$ 后为 $f[u] - f[j] - c[j]$,而点的数量为 $c[u] - 1 - c[j]$,含义为在以节点 `u` 为根的子树中剔除调用以节点 `j` 为根节点的部分。

  即此部分对 $g[j]$ 的贡献为 $f[u] - f[j] - c[j] + c[u] - 1 - c[j]$。

Java 代码:
```Java
class Solution {
    int N = 30010, M = 60010, idx = 0, n;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int[] f = new int[N], c = new int[N], g = new int[N];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    public int[] sumOfDistancesInTree(int _n, int[][] es) {
        n = _n;
        Arrays.fill(he, -1);
        for (int[] e : es) {
            int a = e[0], b = e[1];
            add(a, b); add(b, a);
        }
        dfs1(0, -1);
        dfs2(0, -1);
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) ans[i] = f[i] + g[i];
        return ans;
    }
    int[] dfs1(int u, int fa) {
        int tot = 0, cnt = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            int[] next = dfs1(j, u);
            tot += next[0] + next[1] + 1; cnt += next[1] + 1;
        }
        f[u] = tot; c[u] = cnt;
        return new int[]{tot, cnt};
    }
    void dfs2(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            g[j] += g[u] + n - 1 - c[u]; // 往上再往上
            g[j] += f[u] - f[j] - c[j] + c[u] - 1 - c[j];  // 往上再往下
            dfs2(j, u);
        }
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int N = 30010, M = 60010, idx = 0, n;
    int he[30010], e[60010], ne[60010];
    int f[30010], c[30010], g[30010];
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx++;
    }
    vector<int> sumOfDistancesInTree(int _n, vector<vector<int>>& es) {
        n = _n;
        memset(he, -1, sizeof(he));
        for (auto& e : es) {
            int a = e[0], b = e[1];
            add(a, b);
            add(b, a);
        }
        dfs1(0, -1);
        dfs2(0, -1);
        vector<int> ans(n);
        for (int i = 0; i < n; i++) ans[i] = f[i] + g[i];
        return ans;
    }
    vector<int> dfs1(int u, int fa) {
        int tot = 0, cnt = 0;
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            vector<int> next = dfs1(j, u);
            tot += next[0] + next[1] + 1;
            cnt += next[1] + 1;
        }
        f[u] = tot; c[u] = cnt;
        return {tot, cnt};
    }
    void dfs2(int u, int fa) {
        for (int i = he[u]; i != -1; i = ne[i]) {
            int j = e[i];
            if (j == fa) continue;
            g[j] += g[u] + n - 1 - c[u];
            g[j] += f[u] - f[j] - c[j] + c[u] - 1 - c[j];
            dfs2(j, u);
        }
    }
};
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
`n` 张多米诺骨牌排成一行,将每张多米诺骨牌垂直竖立。在开始时,同时把一些多米诺骨牌向左或向右推。

每过一秒,倒向左边的多米诺骨牌会推动其左侧相邻的多米诺骨牌。同样地,倒向右边的多米诺骨牌也会推动竖立在其右侧的相邻多米诺骨牌。

如果一张垂直竖立的多米诺骨牌的两侧同时有多米诺骨牌倒下时,由于受力平衡, 该骨牌仍然保持不变。

就这个问题而言,我们会认为一张正在倒下的多米诺骨牌不会对其它正在倒下或已经倒下的多米诺骨牌施加额外的力。

给你一个字符串 `dominoes` 表示这一行多米诺骨牌的初始状态,其中:

* `dominoes[i] = 'L'`,表示第 `i` 张多米诺骨牌被推向左侧,
* `dominoes[i] = 'R'`,表示第 `i` 张多米诺骨牌被推向右侧,
* `dominoes[i] = '.'`,表示没有推动第 `i` 张多米诺骨牌。

返回表示最终状态的字符串。

示例 1:
```
输入:dominoes = "RR.L"

输出:"RR.L"

解释:第一张多米诺骨牌没有给第二张施加额外的力。
```
示例 2:

```
输入:dominoes = ".L.R...LR..L.."

输出:"LL.RR.LLRRLL.."
```

提示:
* $n == dominoes.length$
* $1 <= n <= 10^5$
* $dominoes[i]$ 为 `'L`、`'R'` 或 `'.'`

Solution:
### BFS

推倒骨牌是一个行为传递的过程,可以使用 `BFS` 来进行模拟。

起始将所有不为 `.` 的骨牌以 $(loc, time, dire)$ 三元组的形式进行入队,三元组所代表的含义为「**位置为 $loc$ 的骨牌在 $time$ 时刻受到一个方向为 $dire$ 的力**」,然后进行常规的 `BFS` 即可。

在受力(入队)时,我们尝试修改骨牌的状态,同时为了解决「一个骨牌同时受到左右推力时,维持站立状态不变」的问题,我们需要在尝试修改骨牌状态后,额外记录下该骨牌的状态修改时间,如果在同一时间内,一块骨牌受力两次(只能是来自左右两个方向的力),需要将该骨牌恢复成竖立状态。

代码:
```Java
class Solution {
    public String pushDominoes(String dominoes) {
        char[] cs = dominoes.toCharArray();
        int n = cs.length;
        int[] g = new int[n];
        Deque<int[]> d = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            if (cs[i] == '.') continue;
            int dire = cs[i] == 'L' ? -1 : 1;
            d.add(new int[]{i, 1, dire});
            g[i] = 1;
        }
        while (!d.isEmpty()) {
            int[] info = d.pollFirst();
            int loc = info[0], time = info[1], dire = info[2];
            int ne = loc + dire;
            if (cs[loc] == '.' || (ne < 0 || ne >= n)) continue;
            if (g[ne] == 0) { // 首次受力
                d.addLast(new int[]{ne, time + 1, dire});
                g[ne] = time + 1;
                cs[ne] = dire == -1 ? 'L' : 'R';
            } else if (g[ne] == time + 1) { // 多次受力
                cs[ne] = '.';
            }
        }
        return String.valueOf(cs);
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 预处理 + 双指针

我们知道,**如果一块原本竖立的骨牌最终倒下,必然是「受到来自左侧向右的力」或者「受到来自右侧向左的力」**。

基于此,我们可以**创建两个二维数组 `l` 和 `r` 分别存储每个位置 $i$ 的左侧和右侧的受力情况,每个的 $l[i]$ 和 $r[i]$ 分别存储「左侧」和「右侧」的最近受力点下标,以及该力的方向**。

然后枚举所有 $dominoes[i]$ 为 `.` 的位置,获取其左侧的最近受力点 `loc1` 和受力方向 `dire1`,以及其右侧的最近受力点 `loc2` 和受力方向 `dire2`,并进行分情况讨论即可。

根据左右侧受力情况修改骨牌状态可通过「双指针」实现。

> 一些细节:为了避免每个样例都 `new` 大数组,可以使用 `static` 优化 `l` 和 `r` 的创建。

代码:
```Java
class Solution {
    static int N = 100010;
    static int[][] l = new int[N][2], r = new int[N][2];
    public String pushDominoes(String dominoes) {
        char[] cs = dominoes.toCharArray();
        int n = cs.length;
        for (int i = 0, j = -1; i < n; i++) {
            if (cs[i] != '.') j = i;
            l[i] = new int[]{j, j != -1 ? cs[j] : '.'};
        }
        for (int i = n - 1, j = -1; i >= 0; i--) {
            if (cs[i] != '.') j = i;
            r[i] = new int[]{j, j != -1 ? cs[j] : '.'};
        }
        for (int i = 0; i < n; ) {
            if (cs[i] != '.' && ++i >= 0) continue;
            int j = i;
            while (j < n && cs[j] == '.') j++;
            j--;
            int[] a = l[i], b = r[j];
            int loc1 = a[0], dire1 = a[1], loc2 = b[0], dire2 = b[1];
            if (loc1 == -1 && loc2 == -1) { // 两侧无力
            } else if (loc1 == -1) { // 只有右侧有力,且力的方向向左
                if (dire2 == 'L') update(cs, i, j, 'L', 'L');
            } else if (loc2 == -1) { // 只有左侧有力,且力的方向向右
                if (dire1 == 'R') update(cs, i, j, 'R', 'R');
            } else { // 两侧有力,且两力方向「不同时」反向
                if (!(dire1 == 'L' && dire2 == 'R')) update(cs, i, j, (char)dire1, (char)dire2);
            }
            i = j + 1;
        }
        return String.valueOf(cs);
    }
    void update(char[] cs, int l, int r, char c1, char c2) {
        for (int p = l, q = r; p <= q; p++, q--) {
            if (p == q && c1 != c2) continue;
            cs[p] = c1; cs[q] = c2;
        }
    }
}
```
* 时间复杂度:预处理 `l` 和 `r` 的复杂度为 $O(n)$;构造答案复杂度为 $O(n)$。整体复杂度为 $O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
Alice 手中有一把牌,她想要重新排列这些牌,分成若干组,使每一组的牌数都是 groupSize ,并且由 groupSize 张连续的牌组成。

给你一个整数数组 `hand` 其中 `hand[i]` 是写在第 `i` 张牌,和一个整数 `groupSize`。

如果她可能重新排列这些牌,返回 `true` ;否则,返回 `false` 。

示例 1:
```
输入:hand = [1,2,3,6,2,3,4,7,8], groupSize = 3

输出:true

解释:Alice 手中的牌可以被重新排列为 [1,2,3],[2,3,4],[6,7,8]。
```
示例 2:
```
输入:hand = [1,2,3,4,5], groupSize = 4

输出:false

解释:Alice 手中的牌无法被重新排列成几个大小为 4 的组。
```

提示:
* $1 <= hand.length <= 10^4$
* $0 <= hand[i] <= 10^9$
* $1 <= groupSize <= hand.length$

Solution:
### 模拟 + 哈希表 + 优先队列(堆)

为了方便,我们令 $m = groupSize$。

题目要求我们将 $hand$ 分为若干份大小为 $m$ 的顺子。

**在给定 $hand$ 的情况下,划分方式唯一确定,因此本质上这是一个「模拟」的过程。**

具体的,我们可以使用「哈希表」对 $hand$ 中的数值进行「出现次数」统计,并用于后续 **实时** 维护剩余元素的出现次数。

然后使用优先队列维护(小根堆)所有的 $hand[i]$。每次从优先队列(堆)中取出堆顶元素 $t$ 来 **尝试作为**「顺子」的发起点/首个元素(当然 $t$ 能够作为发起点的前提是 $t$ 仍是剩余元素,即实时维护的出现次数不为 $0$ ),然后用 $t$ 作为发起点/首个元素构造顺子,即对 $[t, t + 1, ... , t + m - 1]$ 元素的出现次数进行 $-1$ 操作。

若构造过程中没有出现「剩余元素出现次数」不足以 $-1$ 的话,说明整个构造过程没有冲突,返回 `True`,否则返回 `False`。

代码:
```Java
class Solution {
    public boolean isNStraightHand(int[] hand, int m) {
        Map<Integer, Integer> map = new HashMap<>();
        PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->a-b);
        for (int i : hand) {
            map.put(i, map.getOrDefault(i, 0) + 1);
            q.add(i);
        }
        while (!q.isEmpty()) {
            int t = q.poll();
            if (map.get(t) == 0) continue;
            for (int i = 0; i < m; i++) {
                int cnt = map.getOrDefault(t + i, 0);
                if (cnt == 0) return false;
                map.put(t + i, cnt - 1);
            }
        }
        return true;
    }
}
```
* 时间复杂度:令 $n$ 为数组 `hand` 长度,使用哈希表进行次数统计的复杂度为 $O(n)$;将所有元素从堆中存入和取出的复杂度为 $O(n\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/841-850/847. 访问所有节点的最短路径（困难）.md

847. 访问所有节点的最短路径

Question:
存在一个由 $n$ 个节点组成的无向连通图,图中的节点按从 $0$ 到 $n - 1$ 编号。

给你一个数组 `graph` 表示这个图。其中,$graph[i]$ 是一个列表,由所有与节点 $i$ 直接相连的节点组成。

返回能够访问所有节点的最短路径的长度。你可以在任一节点开始和停止,也可以多次重访节点,并且可以重用边。

示例 1:

```
输入:graph = [[1,2,3],[0],[0],[0]]

输出:4

解释:一种可能的路径为 [1,0,2,0,3]
```
示例 2:

```
输入:graph = [[1],[0,2,4],[1,3,4],[2],[1,2]]

输出:4

解释:一种可能的路径为 [0,1,4,2,3]
```

提示:
* $n == graph.length$
* $1 <= n <= 12$
* $0 <= graph[i].length < n$
* `graph[i]` 不包含 $i$
* 如果 `graph[a]` 包含 `b` ,那么 `graph[b]` 也包含 `a`
* 输入的图总是连通图

Solution:
### 基本分析

为了方便,令点的数量为 $n$,边的数量为 $m$。

这是一个等权无向图,题目要我们求从「**一个点都没访问过**」到「**所有点都被访问**」的最短路径。

同时 $n$ 只有 $12$,容易想到使用「状态压缩」来代表「当前点的访问状态」:**使用二进制表示长度为 $32$ 的 `int` 的低 $12$ 来代指点是否被访问过。**

我们可以通过一个具体的样例,来感受下「状态压缩」是什么意思:

例如 $(000...0101)_2$ 代表编号为 $0$ 和编号为 $2$ 的节点已经被访问过,而编号为 $1$ 的节点尚未被访问。

然后再来看看使用「状态压缩」的话,一些基本的操作该如何进行:

假设变量 $state$ 存放了「当前点的访问状态」,当我们需要检查编号为 $x$ 的点是否被访问过时,可以使用位运算 `a = (state >> x) & 1`,来获取 $state$ 中第 $x$ 位的二进制表示,如果 `a` 为 $1$ 代表编号为 $x$ 的节点已被访问,如果为 $0$ 则未被访问。

同理,当我们需要将标记编号为 $x$ 的节点已经被访问的话,可以使用位运算 `state | (1 << x)` 来实现标记。

---

### 状态压缩 + BFS

因为是等权图,求从某个状态到另一状态的最短路,容易想到 `BFS`。

同时我们需要知道下一步能往哪些点进行移动,因此除了记录当前的点访问状态 $state$ 以外,还需要记录最后一步是在哪个点 $u$,因此我们需要使用二元组进行记录 $(state, u)$,同时使用 $dist$ 来记录到达 $(state, u)$ 使用的步长是多少。

> 一些细节:由于点的数量较少,使用「邻接表」或者「邻接矩阵」来存图都可以。对于本题,由于已经给出了 $graph$ 数组,因此可以直接充当「邻接表」来使用,而无须做额外的存图操作。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int shortestPathLength(int[][] graph) {
        int n = graph.length;
        int mask = 1 << n;

        // 初始化所有的 (state, u) 距离为正无穷
        int[][] dist = new int[mask][n];
        for (int i = 0; i < mask; i++) Arrays.fill(dist[i], INF);

        // 因为可以从任意起点出发,先将起始的起点状态入队,并设起点距离为 0
        Deque<int[]> d = new ArrayDeque<>(); // state, u
        for (int i = 0; i < n; i++) {
            dist[1 << i][i] = 0;
            d.addLast(new int[]{1 << i, i});
        }

        // BFS 过程,如果从点 u 能够到达点 i,则更新距离并进行入队
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int state = poll[0], u = poll[1], step = dist[state][u];
            if (state == mask - 1) return step;
            for (int i : graph[u]) {
                if (dist[state | (1 << i)][i] == INF) {
                    dist[state | (1 << i)][i] = step + 1;
                    d.addLast(new int[]{state | (1 << i), i});
                }
            }
        }
        return -1; // never
    }
}
```
* 时间复杂度:点(状态)数量为 $n \times 2^n$,边的数量为 $n^2 \times 2^n$,`BFS` 复杂度上界为点数加边数,整体复杂度为 $O(n^2 \times 2^n)$
* 空间复杂度:$O(n \times 2^n)$

---

### Floyd + 状压 DP

其实在上述方法中,我们已经使用了与 DP 状态定义分析很像的思路了。甚至我们的元祖设计 $(state, u)$ 也很像状态定义的两个维度。

**那么为什么我们不使用 $f[state][u]$ 为从「没有点被访问过」到「访问过的点状态为 $state$」,并最后一步落在点 $u$ 的状态定义,然后跑一遍 DP 来做呢?**

**是因为如果从「常规的 DP 转移思路」出发,状态之间不存在拓扑序(有环),这就导致了我们在计算某个 $f[state][u]$ 时,它所依赖的状态并不确保已经被计算/更新完成,所以我们无法使用常规的 DP 手段来求解。**

> 这里说的常规 DP 手段是指:枚举所有与 $u$ 相连的节点 $v$,用 $f[state'][v]$ 来更新 $f[state][u]$ 的转移方式。

常规的 DP 转移方式状态间不存在拓扑序,我们需要换一个思路进行转移。

对于某个 $state$ 而言,我们可以枚举其最后一个点 $i$ 是哪一个,充当其达到 $state$ 的最后一步,然后再枚举下一个点 $j$ 是哪一个,充当移动的下一步(当然前提是满足 $state$ 的第 $i$ 位为 $1$,而第 $j$ 位为 $0$)。

求解任意两点最短路径,可以使用 Floyd 算法,复杂度为 $O(n^3)$。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    public int shortestPathLength(int[][] graph) {
        int n = graph.length;
        int mask = 1 << n;

        // Floyd 求两点的最短路径
        int[][] dist = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                dist[i][j] = INF;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j : graph[i]) dist[i][j] = 1;
        }
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }

        // DP 过程,如果从 i 能够到 j 的话,使用 i 到 j 的最短距离(步长)来转移
        int[][] f = new int[mask][n];
        // 起始时,让所有状态的最短距离(步长)为正无穷
        for (int i = 0; i < mask; i++) Arrays.fill(f[i], INF);
        // 由于可以将任意点作为起点出发,可以将这些起点的最短距离(步长)设置为 0
        for (int i = 0; i < n; i++) f[1 << i][i] = 0;

        // 枚举所有的 state
        for (int state = 0; state < mask; state++) {
            // 枚举 state 中已经被访问过的点
            for (int i = 0; i < n; i++) {
                if (((state >> i) & 1) == 0) continue;
                // 枚举 state 中尚未被访问过的点
                for (int j = 0; j < n; j++) {
                    if (((state >> j) & 1) == 1) continue;
                    f[state | (1 << j)][j] = Math.min(f[state | (1 << j)][j], f[state][i] + dist[i][j]);
                }
            }
        }

        int ans = INF;
        for (int i = 0; i < n; i++) ans = Math.min(ans, f[mask - 1][i]);
        return ans;
    }
}
```
* 时间复杂度:Floyd 复杂度为 $O(n^3)$;DP 共有 $n \times 2^n$ 个状态需要被转移,每次转移复杂度为 $O(n)$,总的复杂度为 $O(n^2 \times 2^n)$。整体复杂度为 $O(\max(n^3, n^2 \times 2^n))$
* 空间复杂度:$O(n \times 2^n)$

---

### AStar 算法

显然,从 $state$ 到 $state'$ 的「理论最小修改成本」为两者二进制表示中不同位数的个数。

同时,当且仅当在 $state$ 中 $1$ 的位置与 $state'$ 中 $0$ 存在边,才有可能取到这个「理论最小修改成本」。

因此直接使用当前状态 $state$ 与最终目标状态 `1 << n` 两者二进制表示中不同位数的个数作为启发预估值是合适的。

代码:
```Java
class Solution {
    int INF = 0x3f3f3f3f;
    int n;
    int f(int state) {
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (((state >> i) & 1) == 0) ans++;
        }
        return ans;
    }
    public int shortestPathLength(int[][] g) {
        n = g.length;
        int mask = 1 << n;
        int[][] dist = new int[mask][n];
        for (int i = 0; i < mask; i++) {
            for (int j = 0; j < n; j++) {
                dist[i][j] = INF;
            }
        }
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[2]-b[2]); // state, u, val
        for (int i = 0; i < n; i++) {
            dist[1 << i][i] = 0;
            q.add(new int[]{1<< i, i, f(i << 1)});
        }
        while (!q.isEmpty()) {
            int[] poll = q.poll();
            int state = poll[0], u = poll[1], step = dist[state][u];
            if (state == mask - 1) return step;
            for (int i : g[u]) {
                int nState = state | (1 << i);
                if (dist[nState][i] > step + 1) {
                    dist[nState][i] = step + 1;
                    q.add(new int[]{nState, i, step + 1 + f(nState)});
                }
            }
        }
        return -1; // never
    }
}
```
* 时间复杂度:启发式搜索不讨论时空复杂度
* 空间复杂度:启发式搜索不讨论时空复杂度

### 最后

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

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

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

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


Question:
我们给出了一个(轴对齐的)二维矩形列表 `rectangles`。 对于 $rectangle[i] = [x_1, y_1, x_2, y_2]$,其中$(x_1, y_1)$ 是矩形 `i` 左下角的坐标,$ (x_{i1}, y_{i1})$ 是该矩形 左下角 的坐标,$ (x_{i2}, y_{i2})$ 是该矩形 右上角 的坐标。

计算平面中所有 `rectangles` 所覆盖的 总面积 。任何被两个或多个矩形覆盖的区域应只计算 一次 。

返回 总面积 。因为答案可能太大,返回 $10^9 + 7$ 的 模 。

示例 1:

```
输入:rectangles = [[0,0,2,2],[1,0,2,3],[1,0,3,1]]

输出:6

解释:如图所示,三个矩形覆盖了总面积为6的区域。
从(1,1)到(2,2),绿色矩形和红色矩形重叠。
从(1,0)到(2,3),三个矩形都重叠。
```
示例 2:
```
输入:rectangles = [[0,0,1000000000,1000000000]]

输出:49

解释:答案是 1018 对 (109 + 7) 取模的结果, 即 49 。
```

提示:
* $1 <= rectangles.length <= 200$
* $rectanges[i].length = 4$
* $0 <= x_{i1}, y_{i1}, x_{i2}, y_{i2} <= 10^9$
* 矩形叠加覆盖后的总面积不会超越 $2^{63} - 1$ ,这意味着可以用一个 `64` 位有符号整数来保存面积结果。

Solution:
### 扫描线

这是一道「扫描线」模板题。

将所有给定的矩形的左右边对应的 `x` 端点提取出来并排序,每个端点可看作是一条竖直的线段(红色),问题转换为求解「由多条竖直线段分割开」的多个矩形的面积总和(黄色):

相邻线段之间的宽度为单个矩形的「宽度」(通过 `x` 差值直接算得),问题转换为求该区间内高度的并集(即矩形的高度)。

由于数据范围只有 $200$,我们可以对给定的所有矩形进行遍历,统计所有对该矩形有贡献的 `y` 值线段(即有哪些 `rs[i]` 落在该矩形中),再对线段进行求交集(总长度),即可计算出该矩形的「高度」,从而计算出来该矩形的面积。

Java 代码:
```Java
class Solution {
    int MOD = (int)1e9+7;
    public int rectangleArea(int[][] rs) {
        List<Integer> list = new ArrayList<>();
        for (int[] info : rs) {
            list.add(info[0]); list.add(info[2]);
        }
        Collections.sort(list);
        long ans = 0;
        for (int i = 1; i < list.size(); i++) {
            int a = list.get(i - 1), b = list.get(i), len = b - a;
            if (len == 0) continue;
            List<int[]> lines = new ArrayList<>();
            for (int[] info : rs) {
                if (info[0] <= a && b <= info[2]) lines.add(new int[]{info[1], info[3]});
            }
            Collections.sort(lines, (l1, l2)->{
                return l1[0] != l2[0] ? l1[0] - l2[0] : l1[1] - l2[1];
            });
            long tot = 0, l = -1, r = -1;
            for (int[] cur : lines) {
                if (cur[0] > r) {
                    tot += r - l;
                    l = cur[0]; r = cur[1];
                } else if (cur[1] > r) {
                    r = cur[1];
                }
            }
            tot += r - l;
            ans += tot * len;
            ans %= MOD;
        }
        return (int) ans;
    }
}
```
TypeScript 代码:
```TypeScript
const MOD = BigInt(1e9+7)
function rectangleArea(rs: number[][]): number {
    const list = new Array<number>()
    for (let info of rs) {
        list.push(info[0]); list.push(info[2]);
    }
    list.sort((a,b)=>a-b)
    let ans = 0n
    for (let i = 1; i < list.length; i++) {
        const a = list[i - 1], b = list[i], len = b - a
        if (len == 0) continue
        const lines = new Array<number[]>()
        for (let info of rs) {
            if (info[0] <= a && b <= info[2]) lines.push([info[1], info[3]])
        }
        lines.sort((l1,l2)=>{
            return l1[0] != l2[0] ? l1[0] - l2[0] : l1[1] - l2[1]
        })
        let tot = 0n, l = -1, r = -1
        for (let cur of lines) {
            if (cur[0] > r) {
                tot += BigInt(r - l)
                l = cur[0]; r = cur[1]
            } else if (cur[1] > r) {
                r = cur[1]
            }
        }
        tot += BigInt(r - l)
        ans += tot * BigInt(len)
        ans %= MOD
    }
    return Number(ans)
};
```
Python 代码:
```Python
class Solution:
    def rectangleArea(self, rs: List[List[int]]) -> int:
        ps = []
        for info in rs:
            ps.append(info[0])
            ps.append(info[2])
        ps.sort()
        ans = 0
        for i in range(1, len(ps)):
            a, b = ps[i - 1], ps[i]
            width = b - a
            if width == 0:
                continue
            lines = [(info[1], info[3]) for info in rs if info[0] <= a and b <= info[2]]
            lines.sort()
            height, l, r = 0, -1, -1
            for cur in lines:
                if cur[0] > r:
                    height += r - l
                    l, r = cur
                elif cur[1] > r:
                    r = cur[1]
            height += r - l
            ans += height * width
        return ans % 1000000007
```
Go 代码:
```Go
const MOD = int64(1e9 + 7)
func rectangleArea(rectangles [][]int) int {
    list := []int{}
    for _, info := range rectangles {
        list = append(list, info[0])
        list = append(list, info[2])
    }
    sort.Ints(list)
    ans := int64(0)
    for i := 1; i < len(list); i++ {
        a, b, length := list[i - 1], list[i], list[i] - list[i - 1]
        if length == 0 {
            continue
        }
        lines := [][]int{}
        for _, info := range rectangles {
            if info[0] <= a && b <= info[2] {
                lines = append(lines, []int{info[1], info[3]})
            }
        }
        sort.Slice(lines, func(i,j int) bool {
            if lines[i][0] != lines[j][0] {
                return lines[i][0] - lines[j][0] < 0
            }
            return lines[i][1] - lines[j][1] < 0
        })
        total, l, r := int64(0), -1, -1
        for _, cur := range lines {
            if cur[0] > r {
                total += int64(r - l)
                l, r = cur[0], cur[1]
            } else if cur[1] > r {
                r = cur[1]
            }
        }
        total += int64(r - l)
        ans += total * int64(length)
        ans %= MOD
    }
    return int(ans)
}
```
* 时间复杂度:预处理所有扫描线的复杂度为 $O(n\log{n})$;处理所有相邻的扫描线,并计算相邻扫描线形成的矩形面积复杂度为 $O(n\log{n})$ 。整体复杂度为 $O(n^2\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
有一组 `n` 个人作为实验对象,从 `0` 到 `n - 1` 编号,其中每个人都有不同数目的钱,以及不同程度的安静值(`quietness`)。为了方便起见,我们将编号为 `x` 的人简称为 `"person x"`。

给你一个数组 `richer` ,其中 `richer[i] = [ai, bi]` 表示 $person$ $a_i$ 比 $person$ $b_i$ 更有钱。另给你一个整数数组 `quiet`,其中 `quiet[i]` 是 $person_i$ 的安静值。

`richer` 中所给出的数据 逻辑自恰(也就是说,在 $person_x$ 比 $person_y$ 更有钱的同时,不会出现 $person_y$ 比 $person_x$ 更有钱的情况 )。

现在,返回一个整数数组 `answer` 作为答案,其中 `answer[x] = y` 的前提是,在所有拥有的钱肯定不少于 `person x` 的人中,`person y` 是最安静的人(也就是安静值 `quiet[y]` 最小的人)。

示例 1:
```
输入:richer = [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet = [3,2,5,4,6,1,7,0]

输出:[5,5,2,5,4,5,6,7]

解释:
answer[0] = 5,
person 5 比 person 3 有更多的钱,person 3 比 person 1 有更多的钱,person 1 比 person 0 有更多的钱。
唯一较为安静(有较低的安静值 quiet[x])的人是 person 7,
但是目前还不清楚他是否比 person 0 更有钱。
answer[7] = 7,
在所有拥有的钱肯定不少于 person 7 的人中(这可能包括 person 3,4,5,6 以及 7),
最安静(有较低安静值 quiet[x])的人是 person 7。
其他的答案也可以用类似的推理来解释。
```
示例 2:
```
输入:richer = [], quiet = [0]

输出:[0]
```

提示:
* $n == quiet.length$
* $1 <= n <= 500$
* $0 <= quiet[i] < n$
* $quiet$ 的所有值 互不相同
* $0 <= richer.length <= n * (n - 1) / 2$
* $0 <= ai, bi < n$
* $ai != bi$
* `richer` 中的所有数对 互不相同
* 对 `richer` 的观察在逻辑上是一致的

Solution:
### 拓扑排序

根据题意,我们可以使用 `richer` 进行建图(邻接矩阵/邻接表),对于每组 $richer[i] = (a_i, b_i)$ 而言,添加一条从 $a$ 到 $b$ 的有向边(有钱指向没钱)。

其中题目中的「`richer` 逻辑自恰」是指在该图中不存在环,即为 DAG。

因此我们可以在建图过程中,同时统计每个节点的入度数,然后在图中跑一遍拓扑排序来求得答案 $ans$。

**对「图论 拓扑排序」不了解的同学,可以先看前置 🧀:[拓扑排序入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489706&idx=1&sn=771cd807f39d1ca545640c0ef7e5baec&chksm=fd9cb3b5caeb3aa333809633f09e3967c8f62d10feec5a463c7e30d4781e1fecb50e7f46e4af&token=126965152&lang=zh_CN#rd),里面详细说明了「拓扑排序的基本流程」&「反向图 + 拓扑排序做法的正确性证明」。**

起始时,每个 $ans[i] = i$,然后将统计入度为 $0$ 的节点进行入队,每次出队时,将该节点删掉,对该 DAG 带来影响是「该节点的邻点的入度减一」,若更新入度后数值为 $0$,则将该邻点进行入队操作。

同时,利用跑拓扑排序过程中的 $t -> u$ 关系,尝试使用 $ans[t]$ 更新 $ans[u]$(由于存在 $t$ 指向 $u$ 的边,说明 $t$ 比 $u$ 有钱,此时检查两者的安静值,若满足 $quiet[ans[t]] < quiet[ans[u]]$,则用 $ans[t]$ 更新 $ans[u]$)。

> 本题为稠密图(点数为 $n$,边数为 $m$,当 $m$ 与 $n^2$ 为同一数据级,定义以为稠密图),可直接使用「邻接矩阵」进行存图。
关于何种图选择什么存图方式,在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=126965152&lang=zh_CN#rd) 详细讲过,本次不再赘述。

代码($P1$ 为邻接矩阵,$P2$ 为邻接表):
```Java
class Solution {
    public int[] loudAndRich(int[][] richer, int[] quiet) {
        int n = quiet.length;
        int[][] w = new int[n][n];
        int[] in = new int[n];
        for (int[] r : richer) {
            int a = r[0], b = r[1];
            w[a][b] = 1; in[b]++;
        }
        Deque<Integer> d = new ArrayDeque<>();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = i;
            if (in[i] == 0) d.addLast(i);
        }
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            for (int u = 0; u < n; u++) {
                if (w[t][u] == 1) {
                    if (quiet[ans[t]] < quiet[ans[u]]) ans[u] = ans[t];
                    if (--in[u] == 0) d.addLast(u);
                }
            }
        }
        return ans;
    }
}
```

```Java
class Solution {
    int N = 510, M = N * N + 10;
    int[] he = new int[N], e = new int[M], ne = new int[M];
    int idx;
    void add(int a, int b) {
        e[idx] = b;
        ne[idx] = he[a];
        he[a] = idx;
        idx++;
    }
    public int[] loudAndRich(int[][] richer, int[] quiet) {
        int n = quiet.length;
        int[] in = new int[n];
        Arrays.fill(he, -1);
        for (int[] r : richer) {
            int a = r[0], b = r[1];
            add(a, b); in[b]++;
        }
        Deque<Integer> d = new ArrayDeque<>();
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = i;
            if (in[i] == 0) d.addLast(i);
        }
        while (!d.isEmpty()) {
            int t = d.pollFirst();
            for (int i = he[t]; i != -1; i = ne[i]) {
                int u = e[i];
                if (quiet[ans[t]] < quiet[ans[u]]) ans[u] = ans[t];
                if (--in[u] == 0) d.addLast(u);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:令 $n$ 为 `person` 数量(点数),$m$ 为 `richer` 长度(边数)。的建图的复杂度为 $O(m)$;拓扑排序复杂度为 $O(m + n)$。整体复杂度为 $O(m + n)$
* 空间复杂度:$O(m + n)$

### 最后

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

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

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

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


852. 山脉数组的峰顶索引

Question:
符合下列属性的数组 `arr` 称为 山脉数组 :
* `arr.length >= 3`
* 存在 `i(0 < i < arr.length - 1)` 使得:
  * `arr[0] < arr[1] < ... arr[i-1] < arr[i]`
  * `arr[i] > arr[i+1] > ... > arr[arr.length - 1]`

给你由整数组成的山脉数组 `arr` ,返回任何满足 `arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1]` 的下标 `i` 。

示例 1:
```
输入:arr = [0,1,0]

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

输出:1
```
示例 3:
```
输入:arr = [0,10,5,2]

输出:1
```
示例 4:
```
输入:arr = [3,4,5,1]

输出:2
```
示例 5:
```
输入:arr = [24,69,100,99,79,78,67,36,26,19]

输出:2
```

提示:
* $3 <= arr.length <= 10^4$
* $0 <= arr[i] <= 10^6$
* 题目数据保证 `arr` 是一个山脉数组

进阶:很容易想到时间复杂度 $O(n)$ 的解决方案,你可以设计一个 $O(\log{n})$ 的解决方案吗?

Solution:
### 二分

往常我们使用「二分」进行查值,需要确保序列本身满足「二段性」:当选定一个端点(基准值)后,结合「一段满足 & 另一段不满足」的特性来实现“折半”的查找效果。

但本题求的是峰顶索引值,如果我们选定数组头部或者尾部元素,其实无法根据大小关系“直接”将数组分成两段。

但可以利用题目发现如下性质:**由于 `arr` 数值各不相同,因此峰顶元素左侧必然满足严格单调递增,峰顶元素右侧必然不满足。**

因此 **以峰顶元素为分割点的 `arr` 数组,根据与 前一元素/后一元素 的大小关系,具有二段性:**

* 峰顶元素左侧满足 $arr[i-1] < arr[i]$ 性质,右侧不满足
* 峰顶元素右侧满足 $arr[i] > arr[i+1]$ 性质,左侧不满足

因此我们可以选择任意条件,写出若干「二分」版本。

代码:
```Java
class Solution {
    // 根据 arr[i-1] < arr[i] 在 [1,n-1] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 1, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid - 1] < arr[mid]) l = mid;
            else r = mid - 1;
        }
        return r;
    }
}
```
```Java
class Solution {
    // 根据 arr[i] > arr[i+1] 在 [0,n-2] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 2;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid] > arr[mid + 1]) r = mid;
            else l = mid + 1;
        }
        return r;
    }
}
```
```Java
class Solution {
    // 根据 arr[i-1] > arr[i] 在 [1,n-1] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素的前一个值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 1, r = n - 1;
        while (l < r) {
            int mid = l + r >> 1;
            if (arr[mid - 1] > arr[mid]) r = mid;
            else l = mid + 1;
        }
        return r - 1;
    }
}
```
```Java
class Solution {
    // 根据 arr[i] < arr[i+1] 在 [0,n-2] 范围内找值
    // 峰顶元素为符合条件的最靠近中心的元素的下一个值
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 2;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (arr[mid] < arr[mid + 1]) l = mid;
            else r = mid - 1;
        }
        return r + 1;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 三分

事实上,我们还可以利用「三分」来解决这个问题。

顾名思义,**「三分」就是使用两个端点将区间分成三份,然后通过每次否决三分之一的区间来逼近目标值。**

具体的,由于峰顶元素为全局最大值,因此我们可以每次将当前区间分为 $[l, m1]$、$[m1, m2]$ 和 $[m2, r]$ 三段,如果满足 $arr[m1] > arr[m2]$,说明峰顶元素不可能存在与 $[m2, r]$ 中,让 $r = m2 - 1$ 即可。另外一个区间分析同理。

代码:
```Java
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        int l = 0, r = n - 1;
        while (l < r) {
            int m1 = l + (r - l) / 3, m2 = r - (r - l) / 3;
            if (arr[m1] > arr[m2]) r = m2 - 1;
            else l = m1 + 1;
        }
        return r;
    }
}
```
* 时间复杂度:$O(\log_3{n})$
* 空间复杂度:$O(1)$

---

### 二分 & 三分 & k 分 ?

必须说明一点,「二分」和「三分」在渐进复杂度上都是一样的,都可以通过换底公式转化为可忽略的常数,因此两者的复杂度都是 $O(\log{n})$。

而选择「二分」还是「三分」取决于要解决的是什么问题:

* 二分通常用来解决单调函数的找 $target$ 问题,但进一步深入我们发现只需要满足「二段性」就能使用「二分」来找分割点;
* 三分则是解决单峰函数极值问题。

**因此一般我们将「通过比较两个端点,每次否决 1/3 区间 来解决单峰最值问题」的做法称为「三分」;而不是简单根据单次循环内将区间分为多少份来判定是否为「三分」。**

随手写了一段反例代码:
```Java
class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int left = 0, right = arr.length - 1;
        while(left < right) {
            int m1 = left + (right - left) / 3;
            int m2 = right - (right - left + 2) / 3;
            if (arr[m1] > arr[m1 + 1]) {
                right = m1;
            } else if (arr[m2] < arr[m2 + 1]) {
                left = m2 + 1;
            } else {
                left = m1; right = m2;
            }
        }
        return left;
    }
}
```

这并不是「三分」做法,最多称为「变形二分」。本质还是利用「二段性」来做分割的,只不过同时 check 了两个端点而已。

如果这算「三分」的话,那么我能在一次循环里面划分 $k - 1$ 个端点来实现 $k$ 分?

**显然这是没有意义的,因为按照这种思路写出来的所谓的「四分」、「五分」、「k 分」是需要增加同等数量的分支判断的。这时候单次 `while` 决策就不能算作 $O(1)$ 了,而是需要在 $O(k)$ 的复杂度内决定在哪个分支,就跟上述代码有三个分支进行判断一样。** 因此,这种写法只能算作是「变形二分」。

**综上,只有「二分」和「三分」的概念,不存在所谓的 $k$ 分。** 同时题解中的「三分」部分提供的做法就是标准的「三分」做法。

### 最后

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

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

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

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


LeetCode/851-860/854. 相似度为 K 的字符串（困难）.md

854. 相似度为 K 的字符串

Question:
对于某些非负整数 `k` ,如果交换 $s_1$ 中两个字母的位置恰好 `k` 次,能够使结果字符串等于 $s_2$ ,则认为字符串 $s_1$ 和 $s_2$ 的 相似度为 `k` 。

给你两个字母异位词 $s_1$ 和 $s_2$ ,返回 $s_1$ 和 $s_2$ 的相似度 `k` 的最小值。

示例 1:
```
输入:s1 = "ab", s2 = "ba"

输出:1
```
示例 2:
```
输入:s1 = "abc", s2 = "bca"

输出:2
```

提示:
* $1 <= s1.length <= 20$
* $s2.length = s1.length$
* `s1` 和 `s2`  只包含集合 `{'a', 'b', 'c', 'd', 'e', 'f'}` 中的小写字母
* `s2` 是 `s1` 的一个字母异位词

Solution:
### AStar 算法

由于题目确保了 `s1` 和 `s2` 互为字母异位词(必然有解),因此最好的求解方式是使用 AStar 算法。

可直接根据本题规则来设计 AStar 的启发式函数: 对于两个状态 `a` 和 `b` 直接计算出「理论最小转换次数」: **不同字符串的转换成本之和,由于每一次交换最多可减少两个不同的字符,我们可计算 `a` 与 `b` 的不同字符数量 $ans$,对应的理论最小转换次数为 $\left \lfloor \frac{ans + 1}{2} \right \rfloor$。**

需要注意的是:**由于我们衡量某个字符 `str` 的估值是以目标字符串 `target` 为基准,因此我们只能确保 `target` 出队时为「距离最短」,而不能确保中间节点出队时「距离最短」,因此我们不能单纯根据某个节点是否「曾经入队」而决定是否入队,还要结合当前节点的「最小距离」是否被更新而决定是否入队。**

> 一些细节:在使用当前状态(字符串)`poll` 拓展新状态(字符串)`nstr` 时,只拓展能够减少不同字符数量的方案,从而收窄搜索空间。

Java 代码:
```Java
class Solution {
    int n;
    String t;
    int f(String s) {
        int ans = 0;
        for (int i = 0; i < n; i++) ans += s.charAt(i) != t.charAt(i) ? 1 : 0;
        return ans + 1 >> 1;
    }
    public int kSimilarity(String s1, String s2) {
        if (s1.equals(s2)) return 0;
        t = s2;
        n = s1.length();
        Map<String, Integer> map = new HashMap<>();
        PriorityQueue<String> pq = new PriorityQueue<>((a,b)->{
            int v1 = f(a), v2 = f(b), d1 = map.get(a), d2 = map.get(b);
            return (v1 + d1) - (v2 + d2);
        });
        map.put(s1, 0);
        pq.add(s1);
        while (!pq.isEmpty()) {
            String poll = pq.poll();
            int step = map.get(poll);
            char[] cs = poll.toCharArray();
            int idx = 0;
            while (idx < n && cs[idx] == t.charAt(idx)) idx++;
            for (int i = idx + 1; i < n; i++) {
                if (cs[i] != t.charAt(idx) || cs[i] == t.charAt(i)) continue;
                swap(cs, idx, i);
                String nstr = String.valueOf(cs);
                swap(cs, idx, i);
                if (map.containsKey(nstr) && map.get(nstr) <= step + 1) continue;
                if (nstr.equals(t)) return step + 1;
                map.put(nstr, step + 1);
                pq.add(nstr);
            }
        }
        return -1; // never
    }
    void swap(char[] cs, int i, int j) {
        char c = cs[i];
        cs[i] = cs[j];
        cs[j] = c;
    }
}
```
* 时间复杂度:启发式搜索不分析时空复杂度
* 空间复杂度:启发式搜索不分析时空复杂度

### 最后

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

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

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

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


Question:
给定一个平衡括号字符串 `S`,按下述规则计算该字符串的分数:

* `()` 得 `1` 分。
* `AB` 得 `A + B` 分,其中 `A` 和 `B` 是平衡括号字符串。
* `(A)` 得 `2 * A` 分,其中 `A` 是平衡括号字符串。

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

输出: 1
```
示例 2:
```
输入: "(())"

输出: 2
```
示例 3:
```
输入: "()()"

输出: 2
```
示例 4:
```
输入: "(()(()))"

输出: 6
```

提示:
* `S` 是平衡括号字符串,且只含有 `(` 和 `)` 。
* $2 <= S.length <= 50$

Solution:
### 栈

初始化将答案 `0` 放入栈中,从前往后处理整个 `s`,当遇到 `(` 则存入一个占位数值 `0`,遇到 `)` 取出栈顶元素 `cur`,根据栈顶元素数值值分情况讨论:

* 栈顶元素 $cur = 0$,即当前的 `)` 的前一元素即是 `(` ,根据 `()` 得一分的规则可知,我们本次操作得到的分值为 $1$;
* 栈顶元素 $cur \neq 0$,即当前 `)` 与其匹配的 `(` 中间相隔了其他字符,根据 `(A)` 的得分规则,此时可知得分为 $cur \times 2$;

将两者结合可统一为 $\max(cur \times 2, 1)$。

由于我们每次遇到 `)` 时,都将最近一次操作计算出来。而再前面无论是 `)` 还是 `(` 我们都可以归结到 `X()` 的相邻项累加规则,将其新得分累加到栈顶元素上,其中 `(` 仍采用累加规则,则利用我们将 `(` 定义为 $0$ 的设定。

Java 代码:
```Java
class Solution {
    public int scoreOfParentheses(String s) {
        Deque<Integer> d = new ArrayDeque<>();
        d.addLast(0);
        for (char c : s.toCharArray()) {
            if (c == '(') d.addLast(0);
            else {
                int cur = d.pollLast();
                d.addLast(d.pollLast() + Math.max(cur * 2 , 1));
            }
        }
        return d.peekLast();
    }
}
```
TypeScript 代码:
```TypeScript
function scoreOfParentheses(s: string): number {
    const stk = new Array<number>()
    stk.push(0)
    for (const c of s) {
        if (c == '(') stk.push(0)
        else {
            const cur = stk.pop()
            stk.push(stk.pop() + Math.max(cur * 2, 1))
        }
    }
    return stk.pop()
}
```
Python 代码:
```Python
class Solution:
    def scoreOfParentheses(self, s: str) -> int:
        stk = [0]
        for c in s:
            if c == '(':
                stk.append(0)
            else:
                cur = stk.pop()
                stk.append(stk.pop() + max(cur * 2, 1))
        return stk[-1]
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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