Question:
给定一个非负整数 `num`。对于 `0 ≤ i ≤ num` 范围中的每个数字 `i`,计算其二进制数中的 $1$ 的数目并将它们作为数组返回。

示例 1:
```
输入: 2

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

输出: [0,1,1,2,1,2]
```
进阶:
* 给出时间复杂度为 $O(n \times sizeof(integer))$ 的解答非常容易。但你可以在线性时间 $O(n)$ 内用一趟扫描做到吗?
* 要求算法的空间复杂度为 $O(n)$ 。
* 你能进一步完善解法吗?要求在 `C++` 或任何其他语言中不使用任何内置函数(如 `C++` 中的 `__builtin_popcount`)来执行此操作。

Solution:
### 模拟

这道题要对每个数进行统计,因此不会有比 $O(n)$ 更低的做法。

而很容易想到的朴素做法是对每个数进行「位运算」计数,每个数都是 $32$ 位的,因此是一个 $O(32n)$ 的做法。

代码:
```Java
class Solution {
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = 0; i <= n; i++) ans[i] = getCnt(i);
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 $O(n)$

---

### 动态规划

事实上,这道题是有严格 $O(n)$ 的解法的,要求 $O(n)$ 复杂度又是输出方案的题,通常就是递推的 DP 题。

用已算出的值去凑出要算的值。

那么对于这类问题我们该如何考虑呢?一般是靠经验,如果实在没见过这类题型的话,我们就需要在纸上画一下,分析一下我们**朴素做法的最后一步**是怎么进行的。

不失一般性的,假设当前我要统计的数的 `i`,`i` 对应的二进制表示是 `00000...0010100101`(共 32 位)

如果我们是使用「朴素解法」求解的话,无论是从高位进行统计,还是从低位进行统计,最后一位扫描的都是边缘的数(如果是 1 就计数,不是 1 就不计数)。

* **从低位到高位**,最后一步在扫描**最高位**之前,**统计出 1 的个数应该等同于将 `i` 左移一位,并在最低位补 0,也就是等于 `ans[i << 1]`,这时候就要求我们在计算 `i` 的时候 `i << 1` 已经被算出来(从大到小遍历)**
* **从高位到低位**,最后一步在扫描**最低位**之前,**统计出 1 的个数应该等同于将 `i` 右移一位,并在最高位补 0,也就是等于 `ans[i >> 1]`,这时候就要求我们在计算 `i` 的时候 `i >> 1` 已经被算出来(从小到大遍历)**

通过**对「朴素做法」的最后一步分析**,转移方程就出来了:

* 当**从大到小遍历** :$f(i) = f(i << 1) + ((i >>31 ) \& 1)$
* 当**从小到大遍历** :$f(i) = f(i >> 1) + ( i \& 1 )$

代码:
```Java
class Solution {
    // 从小到大遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        // ans[i] = 「i >> 1 所包含的 1 的个数」+「i 的最低位是否为 1」
        for (int i = 1; i <= n; i++) ans[i] = ans[i >> 1] + (i & 1);
        return ans;
    }
}
```
-
```Java
class Solution {
    // 从大到小遍历
    public int[] countBits(int n) {
        int[] ans = new int[n + 1];
        for (int i = n; i >= 0; i--) {
            // 如果计算 i 所需要的 i << 1 超过 n,则不存储在 ans 内,需要额外计算
            int u = i << 1 <= n ? ans[i << 1] : getCnt(i << 1);
            // ans[i] =「i << 1 所包含的 1 的个数」 + 「i 的最高位是否为 1」
            ans[i] = u + ((i >> 31) & 1);
        }
        return ans;
    }
    int getCnt(int u) {
        int ans = 0;
        for (int i = 0; i < 32; i++) ans += (u >> i) & 1;
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:使用与输入同等规模的空间存储答案。复杂度为 $O(n)$

---

### 位运算说明

* `a >> b & 1` 代表检查 `a` 的第 `b` 位是否为 $1$,有两种可能性 $0$ 或者 $1$

* `a += 1 << b` 代表将 `a` 的第 `b` 位设置为 $1$ (当第 `b` 位为 $0$ 的时候适用)

如不想写对第 `b` 位为 $0$ 的前置判断,`a += 1 << b` 也可以改成 `a |= 1 << b`

在之前的题解我就强调过,以上两个操作在位运算中**使用频率超高,建议每位同学都加深理解**。

### 最后

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

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

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

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


341. 扁平化嵌套列表迭代器

Question:
给你一个嵌套的整型列表。请你设计一个迭代器,使其能够遍历这个整型列表中的所有整数。

列表中的每一项或者为一个整数,或者是另一个列表。其中列表的元素也可能是整数或是其他列表。

示例 1:
```
输入: [[1,1],2,[1,1]]
输出: [1,1,2,1,1]
解释: 通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,1,2,1,1]。
```
示例 2:
```
输入: [1,[4,[6]]]
输出: [1,4,6]
解释: 通过重复调用 next 直到 hasNext 返回 false,next 返回的元素的顺序应该是: [1,4,6]。
```

Solution:
### DFS + 队列

由于所有的元素都是在初始化时提供的,因此一个朴素的做法是在初始化的时候进行处理。

由于存在嵌套,比较简单的做法是通过 DFS 进行处理,将元素都放至队列。

代码:
```java []
public class NestedIterator implements Iterator<Integer> {

    Deque<Integer> queue = new ArrayDeque<>();

    public NestedIterator(List<NestedInteger> nestedList) {
        dfs(nestedList);
    }

    @Override
    public Integer next() {
        return hasNext() ? queue.pollFirst() : -1;
    }

    @Override
    public boolean hasNext() {
        return !queue.isEmpty();
    }

    void dfs(List<NestedInteger> list) {
        for (NestedInteger item : list) {
            if (item.isInteger()) {
                queue.addLast(item.getInteger());
            } else {
                dfs(item.getList());
            }
        }
    }
}
```
* 时间复杂度:构建迭代器的复杂度为 $O(n)$,调用 $next()$ 与 $hasNext()$ 的复杂度为 $O(1)$
* 空间复杂度:$O(n)$

***

### 递归 + 栈

另外一个做法是,我们不对所有的元素进行预处理。

而是先将所有的 `NestedInteger` 逆序放到栈中,当需要展开的时候才进行展开。

代码:
```java
public class NestedIterator implements Iterator<Integer> {

    Deque<NestedInteger> stack = new ArrayDeque<>();

    public NestedIterator(List<NestedInteger> list) {
        for (int i = list.size() - 1; i >= 0; i--) {
            NestedInteger item = list.get(i);
            stack.addLast(item);
        }
    }

    @Override
    public Integer next() {
        return hasNext() ? stack.pollLast().getInteger() : -1;
    }

    @Override
    public boolean hasNext() {
        if (stack.isEmpty()) {
            return false;
        } else {
            NestedInteger item = stack.peekLast();
            if (item.isInteger()) {
                return true;
            } else {
                item = stack.pollLast();
                List<NestedInteger> list = item.getList();
                for (int i = list.size() - 1; i >= 0; i--) {
                    stack.addLast(list.get(i));
                }
                return hasNext();
            }
        }
    }
}
```
* 时间复杂度:构建迭代器的复杂度为 $O(n)$,$hasNext()$ 的复杂度为均摊 $O(1)$,$next()$ 严格按照迭代器的访问顺序( 先 $hasNext()$ 再 $next()$ )的话为 $O(1)$,防御性编程生效的情况下为均摊 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个整数,写一个函数来判断它是否是 4 的幂次方。如果是,返回 true ;否则,返回 false 。

整数 n 是 4 的幂次方需满足:存在整数 x 使得 n == $4^x$

示例 1:
```
输入:n = 16
输出:true
```
示例 2:
```
输入:n = 5
输出:false
```
示例 3:
```
输入:n = 1
输出:true
```

提示:

* -$2^{31}$ <= n <= $2^{31}$ - 1

进阶:

你能不使用循环或者递归来完成本题吗?

Solution:
### 基本分析

一个数 $n$ 如果是 $4$ 的幂,等价于 $n$ 为质因数只有 $2$ 的平方数。

因此我们可以将问题其转换:判断 $\sqrt{n}$ 是否为 $2$ 的幂。

判断某个数是否为 $2$ 的幂的分析在[(题解)231. 2 的幂](https://leetcode-cn.com/problems/power-of-two/solution/gong-shui-san-xie-2-de-mi-by-ac_oier-qm6e/) 这里。

---

### sqrt + lowbit

我们可以先对 $n$ 执行 `sqrt` 函数,然后应用 `lowbit` 函数快速判断 $\sqrt{n}$ 是否为 $2$ 的幂。

代码:
```Java
class Solution {
    public boolean isPowerOfFour(int n) {
        if (n <= 0) return false;
        int x = (int)Math.sqrt(n);
        return x * x == n && (x & -x) == x;
    }
}
```
```Java
class Solution {
    public boolean isPowerOfFour(int n) {
        if (n <= 0) return false;
        int x = getVal(n);
        return x * x == n && (x & -x) == x;
    }
    int getVal(int n) {
        long l = 0, r = n;
        while (l < r) {
            long mid = l + r >> 1;
            if (mid * mid >= n) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return (int)r;
    }
}
```
* 时间复杂度:复杂度取决于内置函数 `sqrt`。一个简单的 `sqrt` 的实现接近于 P2 的代码。复杂度为 $O(\log{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/341-350/345. 反转字符串中的元音字母（简单）.md

345. 反转字符串中的元音字母

Question:
编写一个函数,以字符串作为输入,反转该字符串中的元音字母。

示例 1:
```
输入:"hello"

输出:"holle"
```
示例 2:
```
输入:"leetcode"

输出:"leotcede"
```

提示:
* 元音字母不包含字母 "y" 。

Solution:
### 双指针

一个朴素的做法是利用「双指针」进行前后扫描,当左右指针都是元音字母时,进行互换并移到下一位。

由于元音字母相对固定,因此我们可以使用容器将其存储,并使用 `static` 修饰,确保整个容器的创建和元音字母的填入在所有测试样例中只会发生一次。

我们期望该容器能在 $O(1)$ 的复杂度内判断是否为元音字母,可以使用语言自带的哈希类容器($P2$ 代码)或是使用数组模拟($P1$ 代码)。

> 一些细节:由于题目没有说字符串中只包含字母,因此在使用数组模拟哈希表时,我们需要用当前字符减去 ASCII 码的最小值(空字符),而不是 'A'

代码:
```Java
class Solution {
    static boolean[] hash = new boolean[128];
    static char[] vowels = new char[]{'a','e','i','o','u'};
    static {
        for (char c : vowels) {
            hash[c - ' '] = hash[Character.toUpperCase(c) - ' '] = true;
        }
    }
    public String reverseVowels(String s) {
        char[] cs = s.toCharArray();
        int n = s.length();
        int l = 0, r = n - 1;
        while (l < r) {
            if (hash[cs[l] - ' '] && hash[cs[r] - ' ']) {
                swap(cs, l++, r--);
            } else {
                if (!hash[cs[l] - ' ']) l++;
                if (!hash[cs[r] - ' ']) r--;
            }
        }
        return String.valueOf(cs);
    }
    void swap(char[] cs, int l, int r) {
        char c = cs[l];
        cs[l] = cs[r];
        cs[r] = c;
    }
}
```

```Java
class Solution {
    static char[] vowels = new char[]{'a','e','i','o','u'};
    static Set<Character> set = new HashSet<>();
    static {
        for (char c : vowels) {
            set.add(c);
            set.add(Character.toUpperCase(c));
        }
    }
    public String reverseVowels(String s) {
        char[] cs = s.toCharArray();
        int n = s.length();
        int l = 0, r = n - 1;
        while (l < r) {
            if (set.contains(cs[l]) && set.contains(cs[r])) {
                swap(cs, l++, r--);
            } else {
                if (!set.contains(cs[l])) l++;
                if (!set.contains(cs[r])) r--;
            }
        }
        return String.valueOf(cs);
    }
    void swap(char[] cs, int l, int r) {
        char c = cs[l];
        cs[l] = cs[r];
        cs[r] = c;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:由于 `toCharArray` 会创建新数组,复杂度为 $O(n)$

### 最后

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

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

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

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


LeetCode/351-360/352. 将数据流变为多个不相交区间（困难）.md

352. 将数据流变为多个不相交区间

Question:
给你一个由非负整数 `a1, a2, ..., an` 组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。

实现 `SummaryRanges` 类:

* `SummaryRanges()` 使用一个空数据流初始化对象。
* `void addNum(int val)` 向数据流中加入整数 `val` 。
* `int[][] getIntervals()` 以不相交区间 `[starti, endi]` 的列表形式返回对数据流中整数的总结。

示例:
```
输入:
["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"]
[[], [1], [], [3], [], [7], [], [2], [], [6], []]

输出:
[null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]]

解释:
SummaryRanges summaryRanges = new SummaryRanges();
summaryRanges.addNum(1);      // arr = [1]
summaryRanges.getIntervals(); // 返回 [[1, 1]]
summaryRanges.addNum(3);      // arr = [1, 3]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]]
summaryRanges.addNum(7);      // arr = [1, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]]
summaryRanges.addNum(2);      // arr = [1, 2, 3, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]]
summaryRanges.addNum(6);      // arr = [1, 2, 3, 6, 7]
summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]
```

提示:
* 0 <= val <= $10^4$
* 最多调用 addNum 和 getIntervals 方法 $3 * 10^4$ 次

**进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?**

Solution:
### 二分查找 + 朴素维护区间

一个朴素的做法是直接使用 `ArrayList`(数组)来维护所有的不相交区间,然后调用 `addNum` 时先通过二分找到当前值相邻的区间,然后根据题意进行模拟即可。

同时为了简化复杂的分情况讨论,起始时我们可以先往 `ArrayList` 添加两个哨兵分别代表正无穷和负无穷。

代码(不使用哨兵的代码见 $P2$):
```Java
class SummaryRanges {
    List<int[]> list = new ArrayList<>();
    int[] head = new int[]{-10, -10}, tail = new int[]{10010, 10010};
    public SummaryRanges() {
        list.add(head);
        list.add(tail);
    }
    public void addNum(int val) {
        int n = list.size();
        if (n == 0) {
            list.add(new int[]{val, val});
            return ;
        }
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (list.get(mid)[0] <= val) l = mid;
            else r = mid - 1;
        }
        int[] cur = new int[]{val, val};
        int[] prev = list.get(r);
        int[] next = list.get(r + 1);
        if ((prev[0] <= val && val <= prev[1]) || (next[0] <= val && val <= next[1])) {
            // pass
        } else if (prev[1] + 1 == val && val == next[0] - 1) {
            prev[1] = next[1];
            list.remove(next);
        } else if (prev[1] + 1 == val) {
            prev[1] = val;
        } else if (next[0] - 1 == val) {
            next[0] = val;
        } else {
            list.add(r + 1, cur);
        }
    }
    public int[][] getIntervals() {
        int n = list.size();
        int[][] ans = new int[n - 2][2];
        int idx = 0;
        for (int i = 1; i < n - 1; i++) ans[idx++] = list.get(i);
        return ans;
    }
}
```

```Java
class SummaryRanges {
    List<int[]> list = new ArrayList<>();
    public void addNum(int val) {
        int n = list.size();
        if (n == 0) {
            list.add(new int[]{val, val});
            return ;
        }
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (list.get(mid)[0] <= val) l = mid;
            else r = mid - 1;
        }
        int[] cur = list.get(r);
        if (cur[0] > val) {
            if (val + 1 == cur[0]) {
                cur[0] = val;
            } else {
                list.add(r, new int[]{val, val});
            }
            return ;
        }
        if (cur[0] <= val && val <= cur[1]) {
            // pass
        } else if (r == n - 1) {
            if (cur[1] + 1 == val) {
                cur[1] = val;
            } else {
                list.add(new int[]{val, val});
            }
        } else {
            int[] next = list.get(r + 1);
            if (cur[1] + 1 == val && val == next[0] - 1) {
                cur[1] = next[1];
                list.remove(r + 1);
            } else if (cur[1] + 1 == val) {
                cur[1] = val;
            } else if (next[0] - 1 == val) {
                next[0] = val;
            } else {
                list.add(r + 1, new int[]{val, val});
            }
        }
    }
    public int[][] getIntervals() {
        int n = list.size();
        int[][] ans = new int[n][2];
        for (int i = 0; i < n; i++) ans[i] = list.get(i);
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为不交区间个数,`addNum` 操作中查询相邻区间的复杂度为 $O(\log{m})$,维护不相交区间时,由于需要针对 $list$ 的索引位置进行元素插入和删除,复杂度为 $O(m)$,整体复杂度为 $O(m)$;`getIntervals` 操作需要遍历所有的 $list$ 元素,复杂度为 $O(m)$
* 空间复杂度:$O(m)$

---

### 二分查找 + 优化维护区间

解法一中虽然在 `addNum` 使用到了复杂度诶 $O(\log{m})$ 的二分查找,但在维护区间时,由于要对 $list$ 进行针对索引的插入和删除,导致整体复杂度为 $O(m)$。

我们期望找到一种插入/删除操作复杂度同样为 $O(\log{m})$ 的方式来维护区间,这引导我们使用 `TreeSet`(红黑树)等数据结构。

具体做法不变,仍然是先使用「二分」(`TreeSet` 自带的 `floor/ceiling`)来找到当前值的相邻区间,

同时为了简化复杂的分情况讨论,起始时我们可以先往 `TreeSet` 添加两个哨兵分别代表正无穷和负无穷,以确保调用 `floor/ceiling` 时不会返回空。

代码:
```Java
class SummaryRanges {
    TreeSet<int[]> ts = new TreeSet<>((a, b)->a[0]-b[0]);
    int[] head = new int[]{-10, -10}, tail = new int[]{10010, 10010};
    public SummaryRanges() {
        ts.add(head);
        ts.add(tail);
    }
    public void addNum(int val) {
        int[] cur = new int[]{val, val};
        int[] prev = ts.floor(cur);
        int[] next = ts.ceiling(cur);
        if ((prev[0] <= val && val <= prev[1]) || (next[0] <= val && val <= next[1])) {
            // pass
        } else if (prev[1] + 1 == val && next[0] - 1 == val) {
            prev[1] = next[1];
            ts.remove(next);
        } else if (prev[1] + 1 == val) {
            prev[1] = val;
        } else if (next[0] - 1 == val) {
            next[0] = val;
        } else {
            ts.add(cur);
        }
    }
    public int[][] getIntervals() {
        // using ceiling
        // int n = ts.size();
        // int[][] ans = new int[n - 2][2];
        // int[] cur = head;
        // for (int i = 0; i < n - 2; i++) {
        //     ans[i] = ts.ceiling(new int[]{cur[0] + 1, cur[1] + 1});
        //     cur = ans[i];
        // }
        // return ans;

        // using iterator
        int n = ts.size();
        int[][] ans = new int[n - 2][2];
        Iterator<int[]> iterator = ts.iterator();
        iterator.next();
        for (int i = 0; i < n - 2; i++) ans[i] = iterator.next();
        return ans;
    }
}
```
* 时间复杂度:令 $m$ 为不交区间个数,`addNum` 操作中「查询相邻区间」和「维护不相交区间」的复杂度为 $O(\log{m})$,整体复杂度为 $O(\log{m})$;`getIntervals` 操作需要获取有序集合中的所有元素,使用 `ceiling` 复杂度为 $O(m\log{m})$。使用 `iterator` 的话,由于 `TreeSet` 底层同步维护了一个 `TreeMap`,迭代器由 `TreeMap` 所提供,获取 `iterator` 来遍历所有元素的复杂度为 $O(m)$
* 空间复杂度:$O(m)$

---

### 进阶

如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?

即需要确保 `addNum` 的复杂度,而对 `getIntervals` 复杂度要求不高,上述解法就已经是这么做的了。

### 最后

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

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

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

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


354. 俄罗斯套娃信封问题

Question:
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算「最多能有多少个」信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

示例 1:
```
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
```
示例 2:
```
输入:envelopes = [[1,1],[1,1],[1,1]]
输出:1
```

提示:
* 1 <= envelopes.length <= 5000
* envelopes[i].length == 2
* 1 <= wi, hi <= $10^4$

Solution:
### 动态规划

这是一道经典的 DP 模型题目:最长上升子序列(LIS)。

首先我们先对 `envelopes` 进行排序,确保信封是从小到大进行排序。

问题就转化为我们从这个序列中选择 `k` 个信封形成新的序列,使得新序列中的每个信封都能严格覆盖前面的信封(宽高都严格大于)。

我们可以**定义状态 $f[i]$ 为考虑前 i 个物品,并以第 $i$ 个物品为结尾的最大值。**

对于每个$f[i]$ 而言,最小值为 $1$,代表只选择自己一个信封。

那么对于一般的 $f[i]$ 该如何求解呢?因为第 i 件物品是必须选择的。我们可以枚举前面的 $i - 1$ 件物品,哪一件可以作为第 $i$ 件物品的上一件物品。

在前 $i - 1$ 件物品中只要有符合条件的,我们就使用 $max(f[i], f[j] + 1)$ 更新 $f[i]$。

然后在所有方案中取一个 `max` 即是答案。

代码:

```Java
class Solution {
    public int maxEnvelopes(int[][] es) {
        int n = es.length;
        if (n == 0) return n;
        // 因为我们在找第 i 件物品的前一件物品时,会对前面的 i - 1 件物品都遍历一遍,因此第二维(高度)排序与否都不影响
        Arrays.sort(es, (a, b)->a[0]-b[0]);
        int[] f = new int[n]; // f(i) 为考虑前 i 个物品,并以第 i 个物品为结尾的最大值
        int ans = 1;
        for (int i = 0; i < n; i++) {
            // 对于每个 f[i] 都满足最小值为 1
            f[i] = 1;
            // 枚举第 i 件物品的前一件物品,
            for (int j = i - 1; j >= 0; j--) {
                // 只要有满足条件的前一件物品,我们就尝试使用 f[j] + 1 更新 f[i]
                if (check(es, j, i)) {
                    f[i] = Math.max(f[i], f[j] + 1);
                }
            }
            // 在所有的 f[i] 中取 max 作为 ans
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
    boolean check(int[][] es, int mid, int i) {
        return es[mid][0] < es[i][0] && es[mid][1] < es[i][1];
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

***

### 二分 + 动态规划

上述方案其实算是一个朴素方案,复杂度是 $O(n^2)$ 的,也是我最先想到思路,但是题目没有给出数据范围,也不知道能不能过。

唯唯诺诺交了一个居然过了。

下面讲下其他优化解法。

首先还是和之前一样,我们可以通过复杂度分析来想优化方向。

指数算法往下优化就是对数解法或者线性解法。

仔细观察朴素解法,其实可优化的地方主要就是找第 $i$ 件物品的前一件物品的过程。

如果想要加快这个查找过程,我们需要使用某种数据结构进行记录。

并且是边迭代边更新数据结构里面的内容。

首先因为我们对 `w` 进行了排序(从小到大),然后迭代也是从前往后进行,因此我们只需要保证迭代过程中,对于 `w` 相同的数据不更新,就能保证 `g` 中只会出现满足 `w` 条件的信封。

到这一步,还需要用到的东西有两个:一个是 `h`,因为只有 `w` 和 `h` 都同时满足,我们才能加入上升序列中;一个是信封所对应的上升序列长度,这是我们加速查找的核心。

我们使用数组 `g` 来记录,$g[i]$ 表示长度为 $i$ 的最长上升子序列的中的最小「信封高度」,同时需要使用 `len` 记录当前记录到的最大长度。

还是不理解?没关系,我们可以直接看看代码,我把基本逻辑写在了注释当中(你的重点应该落在对 $g[]$ 数组的理解上)。

代码:

```Java
class Solution {
    public int maxEnvelopes(int[][] es) {
        int n = es.length;
        if (n == 0) return n;
        // 由于我们使用了 g 记录高度,因此这里只需将 w 从小到达排序即可
        Arrays.sort(es, (a, b)->a[0] - b[0]);
        // f(i) 为考虑前 i 个物品,并以第 i 个物品为结尾的最大值
        int[] f = new int[n];
        // g(i) 记录的是长度为 i 的最长上升子序列的最小「信封高度」
        int[] g = new int[n];
        // 因为要取 min,用一个足够大(不可能)的高度初始化
        Arrays.fill(g, Integer.MAX_VALUE);
        g[0] = 0;
        int ans = 1;
        for (int i = 0, j = 0, len = 1; i < n; i++) {
            // 对于 w 相同的数据,不更新 g 数组
            if (es[i][0] != es[j][0]) {
                // 限制 j 不能越过 i,确保 g 数组中只会出现第 i 个信封前的「历史信封」
                while (j < i) {
                    int prev = f[j], cur = es[j][1];
                    if (prev == len) {
                        // 与当前长度一致了,说明上升序列多增加一位
                        g[len++] = cur;
                    } else {
                        // 始终保留最小的「信封高度」,这样可以确保有更多的信封可以与其行程上升序列
                        // 举例:同样是上升长度为 5 的序列,保留最小高度为 5 记录(而不是保留任意的,比如 10),这样之后高度为 7 8 9 的信封都能形成序列;
                        g[prev] = Math.min(g[prev], cur);
                    }
                    j++;
                }
            }

            // 二分过程
            // g[i] 代表的是上升子序列长度为 i 的「最小信封高度」
            int l = 0, r = len;
            while (l < r) {
                int mid = l + r >> 1;
                // 令 check 条件为 es[i][1] <= g[mid](代表 w 和 h 都严格小于当前信封)
                // 这样我们找到的就是满足条件,最靠近数组中心点的数据(也就是满足 check 条件的最大下标)
                // 对应回 g[] 数组的含义,其实就是找到 w 和 h 都满足条件的最大上升长度
                if (es[i][1] <= g[mid]) {
                    r = mid;
                } else {
                    l = mid + 1;
                }
            }
            // 更新 f[i] 与答案
            f[i] = r;
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
* 时间复杂度:对于每件物品都是通过「二分」找到其前一件物品。复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

***

### 证明

我们可以这样做的前提是 `g` 数组具有二段性,可以通过证明其具有「单调性」来实现。

当然这里指的是 `g` 被使用的部分,也就是 $[0, len - 1]$ 的部分。

我们再回顾一下 `g[]` 数组的定义:**$g[i]$ 表示长度为 $i$ 的最长上升子序列的中的最小「信封高度」**

例如 $g[] = [0, 3, 4, 5]$ 代表的含义是:
* 上升序列长度为 0 的最小历史信封高度为 0
* 上升序列长度为 1 的最小历史信封高度为 3
* 上升序列长度为 2 的最小历史信封高度为 4
* 上升序列长度为 3 的最小历史信封高度为 5

可以通过反证法来证明其单调性:

假设 $g[]$ 不具有单调性,即至少有 $g[i] > g[j]$ ( $i < j$,令 $a = g[i]$, $b = g[j]$)

显然与我们的处理逻辑冲突。因为如果考虑一个「最小高度」为 `b` 的信封能够凑出长度为 `j` 的上升序列,自然也能凑出比 `j` 短的上升序列,对吧?

举个🌰,我们有信封:[[1,1],[2,2],[3,3],[4,4],[5,5]],我们能凑出很多种长度为 2 的上升序列方案,其中最小的方案是高度最小的方案是 [[1,1],[2,2]]。因此这时候 g[2] = 2,代表能凑出长度为 2 的上升序列所 **必须使用的信封** 的最小高度为 2。

这时候反过来考虑,如果使用 [2,2] 能够凑出长度为 2 的上升序列,必然也能凑出长度为 1 的上升序列(删除前面的其他信封即可)。

**推而广之,如果我们有 $g[j] = b$,也就是凑成长度为 `j` 必须使用的最小信封高度为 b。那么我必然能够保留高度为 `b` 的信封,删掉上升序列中的一些信封,凑成任意长度比 `j` 小的上升序列。**

综上,$g[i] > g[j](i < j)$ 与处理逻辑冲突,$g[]$ 数组为严格单调上升数组。

既然 $g[]$ 具有单调性,我们可以通过「二分」找到恰满足 check 条件的最大下标(最大下标达标表示最长上升序列长度)。

***

### 树状数组 + 动态规划

在「二分 + 动态规划」的解法中,我们通过「二分」来优化找第 $i$ 个文件的前一个文件过程。

这个过程同样能通过「树状数组」来实现。

首先仍然是对 `w` 进行排序,然后使用「树状数组」来维护 h 维度的前缀最大值。

对于 `h` 的高度,我们只关心多个信封之间的大小关系,而不关心具体相差多少,我们需要对 `h` 进行离散化。

通常使用「树状数组」都需要进行离散化,尤其是这里我们本身就要使用 $O(n)$ 的空间来存储 dp 值。

代码:

```java
class Solution {
    int[] tree;
    int lowbit(int x) {
        return x & -x;
    }

    public int maxEnvelopes(int[][] es) {
        int n = es.length;
        if (n == 0) return n;

        // 由于我们使用了 g 记录高度,因此这里只需将 w 从小到达排序即可
        Arrays.sort(es, (a, b)->a[0] - b[0]);

        // 先将所有的 h 进行离散化
        Set<Integer> set = new HashSet<>();
        for (int i = 0; i < n; i++) set.add(es[i][1]);
        int cnt = set.size();
        int[] hs = new int[cnt];
        int idx = 0;
        for (int i : set) hs[idx++] = i;
        Arrays.sort(hs);
        for (int i = 0; i < n; i++) es[i][1] = Arrays.binarySearch(hs, es[i][1]) + 1;

        // 创建树状数组
        tree = new int[cnt + 1];

        // f(i) 为考虑前 i 个物品,并以第 i 个物品为结尾的最大值
        int[] f = new int[n];
        int ans = 1;
        for (int i = 0, j = 0; i < n; i++) {
            // 对于 w 相同的数据,不更新 tree 数组
            if (es[i][0] != es[j][0]) {
                // 限制 j 不能越过 i,确保 tree 数组中只会出现第 i 个信封前的「历史信封」
                while (j < i) {
                    for (int u = es[j][1]; u <= cnt; u += lowbit(u)) {
                        tree[u] = Math.max(tree[u], f[j]);
                    }
                    j++;
                }
            }
            f[i] = 1;
            for (int u = es[i][1] - 1; u > 0; u -= lowbit(u)) {
                f[i] = Math.max(f[i], tree[u] + 1);
            }
            ans = Math.max(ans, f[i]);
        }
        return ans;
    }
}
```
* 时间复杂度:处理每个物品时更新「树状数组」复杂度为$O(\log{n})$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


LeetCode/351-360/357. 统计各位数字都不同的数字个数（中等）.md

357. 统计各位数字都不同的数字个数

Question:
给你一个整数 $n$ ,统计并返回各位数字都不同的数字 $x$ 的个数,其中 $0 <= x < 10^n$ 。

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

输出:91

解释:答案应为除去 11、22、33、44、55、66、77、88、99 外,在 0 ≤ x < 100 范围内的所有数字。
```
示例 2:
```
输入:n = 0

输出:1
```

提示:
* $0 <= n <= 8$

Solution:
### 乘法原理

对于 $n = 0$ 的情况较为特殊,特判一下,返回 $1$。

对于其他情况,由于不能含有前导 $0$,最高位可选择的数值个数为 $9$,而从次高位开始到最低位,可选的个数从 $9$ 开始逐一递减。

利用乘法原理,每位数可选的数值个数相乘即是长度为 $n$ 的数的可能方案数 $cur$,而所有长度 $[1, n]$ 的方案数累加即是答案。

代码:
```Java
class Solution {
    public int countNumbersWithUniqueDigits(int n) {
        if (n == 0) return 1;
        int ans = 10;
        for (int i = 2, last = 9; i <= n; i++) {
            int cur = last * (10 - i + 1);
            ans += cur; last = cur;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

---

### 数位 DP

一种更为进阶的做法,应当是可以回答任意区间 $[l, r]$ 内合法数的个数。

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

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

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

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

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

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

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

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

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

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

### 最后

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

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

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

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


LeetCode/361-370/363. 矩形区域不超过 K 的最大数值和（困难）.md

363. 矩形区域不超过 K 的最大数值和

Question:
给你一个 `m x n` 的矩阵 `matrix` 和一个整数 $k$ ,找出并返回矩阵内部矩形区域的不超过 $k$ 的最大数值和。

题目数据保证总会存在一个数值和不超过 $k$ 的矩形区域。

示例 1:

```
输入:matrix = [[1,0,1],[0,-2,3]], k = 2

输出:2

解释:蓝色边框圈出来的矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
```
示例 2:
```
输入:matrix = [[2,2,-1]], k = 3

输出:3
```

提示:
* $m == matrix.length$
* $n == matrix[i].length$
* $1 <= m, n <= 100$
* $-100 <= matrix[i][j] <= 100$
* -$10^5 <= k <= 10^5$

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

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

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

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

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

Java 代码:
```Java
class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        int[][] sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                for (int p = i; p <= m; p++) {
                    for (int q = j; q <= n; q++) {
                        int cur = sum[p][q] - sum[i - 1][q] - sum[p][j - 1] + sum[i - 1][j - 1];
                        if (cur <= k) {
                            ans = Math.max(ans, cur);
                        }
                    }
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
int sum[110][110];
class Solution {
public:
    int maxSumSubmatrix(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = INT_MIN;
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                for(int p = i; p <= m; p++){
                    for(int q = j; q <= n; q++){
                        int cur = sum[p][q] - sum[i - 1][q] - sum[p][j - 1] + sum[i - 1][j - 1];
                        if(cur <= k){
                            ans = max(ans,cur);
                        }
                    }
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:预处理前缀和数组复杂度为 $O(m * n)$,查找答案的复杂度为 $O(m^2 * n^2)$。整体复杂度为 $O(m^2 * n^2)$。
* 空间复杂度:$O(m * n)$

---

### 前缀和 & 二分(抽象一维)

我们来细想一下「朴素二维前缀和」解法是如何搜索答案(子矩阵):**通过枚举「左上角」&「右下角」来唯一确定某个矩阵**。

**换句话说是通过枚举 $(i,j)$ 和 $(p,q)$ 来唯一确定子矩阵的四条边,每个坐标点可以看作确定子矩阵的某条边。**

既然要确定的边有四条,我们可以如何降低复杂度呢?

简单的,我们先思考一下同样是枚举的 [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) 问题。

在 [1. 两数之和](https://leetcode-cn.com/problems/two-sum/) 中可以暴力枚举两个数,也可以只枚举其中一个数,然后使用数据结构(哈希表)来加速找另一个数(这是一个通用的「降低枚举复杂度」思考方向)。

对应到本题,我们可以枚举其中三条边,然后使用数据结构来加速找第四条边。

当我们确定了三条边(红色)之后,形成的子矩阵就单纯取决于第四条边的位置(黄色):

于是问题转化为「**如何快速求得第四条边(黄色)的位置在哪**」。

我们可以进一步将问题缩小,考虑矩阵只有一行(一维)的情况:

这时候问题进一步转化为「**在一维数组中,求解和不超过 K 的最大连续子数组之和**」。

对于这个一维问题,我们可以先预处理出「前缀和」,然后枚举子数组的左端点,然后通过「二分」来求解其右端点的位置。

假定我们已经求得一维数组的前缀和数组 `sum`,即可得下标范围 $[i,j]$ 的和为:

$$areaSum(i, j) = sum[j] - sum[i - 1] \leqslant k$$

经过变形后得:

$$sum[j] \leqslant k + sum[i - 1]$$

我们有两种思路来最大化 $areaSum(i, j)$:

* 确定(枚举)左端点位置 `i`,求得符合条件的最大右端点 `sum[j]`
* 确定(枚举)右端点位置 `j`,求得符合条件的最小左端点 `sum[i]`

**对于没有负权值的一维数组,我们可以枚举左端点 `i`,同时利用前缀和的「单调递增」特性,通过「二分」找到符合 $sum[j] \leqslant k + sum[i - 1]$ 条件的最大值 `sum[j]`,从而求解出答案。**

**但是如果是含有负权值的话,前缀和将会丢失「单调递增」的特性,我们也就无法使用枚举 `i` 并结合「二分」查找 `j` 的做法。**

**这时候需要将过程反过来处理:我们从左到右枚举 `j`,并使用「有序集合」结构维护遍历过的位置,找到符合 $sum[j] - k \leqslant sum[i]$ 条件的最小值 `sum[i]`,从而求解出答案。**

基于上述分析,解决这样的一维数组问题复杂度是 $O(n\log{n})$ 的。

将这样的思路应用到二维需要一点点抽象能力。

同时,将一维思路应用到本题(二维),复杂度要么是 $O(m^2 * n\log{n})$ 要么是 $O(n^2 * m\log{m})$。

我们先不考虑「最大化二分收益」问题,先假设我们是固定枚举「上下行」和「右边列」,这时候唯一能够确定一个子矩阵则是取决于「左边列」:

**重点是如何与「一维」问题进行关联:显然「目标子矩阵的和」等于「子矩阵的右边列 与 原矩阵的左边列 形成的子矩阵和」-「子矩阵左边列 与 原矩阵左边列 形成的子矩阵和」**

我们可以使用 `area[r]` 代表「**子矩阵的右边列 与 原矩阵的左边列 形成的子矩阵和**」,使用 `area[l - 1]` 代表「**子矩阵的左边列 与 原矩阵的左边列 形成的子矩阵和**」的话,则有:

$$
target = area[r] - area[l - 1] \leqslant k
$$

这与我们「一维问题」完全一致,同时由「上下行」&「右边列」可以直接确定 `area[r]` 的大小,通过「有序集合」存储我们遍历 `r` 过程中的所有的 `area[r]` 从而实现「二分」查找符合 $area[r] - k \leqslant area[l - 1]$ 条件的 **最小** 的 `area[l - 1]`。

至此,我们通过预处理前缀和 + 容斥原理彻底将题目转化为「一维问题」进行来求解。

Java 代码:
```Java
class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;

        // 预处理前缀和
        int[][] sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }

        int ans = Integer.MIN_VALUE;
        // 遍历子矩阵的上边界
        for (int top = 1; top <= m; top++) {
            // 遍历子矩阵的下边界
            for (int bot = top; bot <= m; bot++) {
                // 使用「有序集合」维护所有遍历到的右边界
                TreeSet<Integer> ts = new TreeSet<>();
                ts.add(0);
                // 遍历子矩阵的右边界
                for (int r = 1; r <= n; r++) {
                    // 通过前缀和计算 right
                    int right = sum[bot][r] - sum[top - 1][r];
                    // 通过二分找 left
                    Integer left = ts.ceiling(right - k);
                    if (left != null) {
                        int cur = right - left;
                        ans = Math.max(ans, cur);
                    }
                    // 将遍历过的 right 加到有序集合
                    ts.add(right);
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxSumSubmatrix(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> sum(m + 1,vector<int>(n + 1,0));
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = INT_MIN;
        for(int top = 1; top <= m; top++){
            for(int bot = top; bot <= m; bot++){
                set<int> st;
                st.insert(0);
                for(int r = 1; r <= n; r++){
                    int right = sum[bot][r] - sum[top - 1][r];
                    auto left = st.lower_bound(right - k);
                    if(left != st.end()){
                        int cur = right - *left;
                        ans = max(ans,cur);
                    }
                    st.insert(right);
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:枚举上下边界复杂度为 $O(m^2)$;枚举右边界为 $O(n)$,使用 `TreeSet`(基于红黑树)存储和查找左边界复杂度为 $O(\log{n})$。整体复杂度为 $O(m^2 * n\log{n})$
* 空间复杂度:$O(m * n)$

---

### 最大化「二分」效益

上述解法中,我们先枚举的是「上下行」和「右边列」,然后通过 `TreeSet` 来「二分」出符合条件的「左边列」。

事实上,我们需要将「二分过程」应用到数值较大的行或者列之中,这样才能最大化我们查找的效率(同时也回答了本题的进阶部分)。

Java 代码:
```Java
class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        int[][] sum = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        // 固定的是否为右边界
        boolean isRight = n > m;
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= (isRight ? m : n); i++) {
            for (int j = i; j <= (isRight ? m : n); j++) {
                TreeSet<Integer> ts = new TreeSet<>();
                ts.add(0);
                for (int fixed = 1; fixed <= (isRight ? n : m); fixed++) {
                    int a = isRight ? sum[j][fixed] - sum[i - 1][fixed] : sum[fixed][j] - sum[fixed][i - 1];
                    Integer b = ts.ceiling(a - k);
                    if (b != null) {
                        int cur = a - b;
                        ans = Math.max(ans, cur);
                    }
                    ts.add(a);
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxSumSubmatrix(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        vector<vector<int>> sum(m + 1,vector<int>(n + 1,0));
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        bool isRight = n > m;
        int ans = INT_MIN;
        for(int i = 1; i <= (isRight ? m : n); i++){
            for(int j = i; j <= (isRight ? m : n); j++){
                set<int> st;
                st.insert(0);
                for(int fixed = 1; fixed <= (isRight ? n : m); fixed++){
                    int a = isRight ? sum[j][fixed] - sum[i - 1][fixed] : sum[fixed][j] - sum[fixed][i - 1];
                    auto b = st.lower_bound(a - k);
                    if(b != st.end()){
                        int cur = a - *b;
                        ans = max(ans,cur);
                    }
                    st.insert(a);
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:预处理「每行」或「每列」的前缀和,复杂度为 $O(m * n)$;枚举子矩阵的「上下行」或「左右行」,复杂度为 $O(\min(m, n)^2)$;结合二维前缀和套用一维最大连续子数组解决方案,复杂度为$\max(m, n)\log{\max(m, n)}$。整体复杂度为 $O(\min(m, n)^2 * \max(m, n)\log{\max(m, n)})$
* 空间复杂度:$O(m * n)$

---

### 空间优化

不难发现,我们在原矩阵搜索目标子矩阵的过程是严格的「从上到下」&「从左到右」的。

因此我们可以将计算前缀和的逻辑下放到搜索子矩阵的循环里去做,从而将 $O(m * n)$ 的空间复杂度下降到 $O(\max(m,n))$。

Java 代码:
```Java
class Solution {
    public int maxSumSubmatrix(int[][] mat, int k) {
        int m = mat.length, n = mat[0].length;
        boolean isRight = n > m;
        int[] sum = isRight ? new int[n + 1] : new int[m + 1];
        int ans = Integer.MIN_VALUE;
        for (int i = 1; i <= (isRight ? m : n); i++) {
            Arrays.fill(sum, 0);
            for (int j = i; j <= (isRight ? m : n); j++) {
                TreeSet<Integer> ts = new TreeSet<>();
                ts.add(0);
                int a = 0;
                for (int fixed = 1; fixed <= (isRight ? n : m); fixed++) {
                    sum[fixed] += isRight ? mat[j - 1][fixed - 1] : mat[fixed - 1][j - 1] ;
                    a += sum[fixed];
                    Integer b = ts.ceiling(a - k);
                    if (b != null) {
                        int cur = a - b;
                        ans = Math.max(ans, cur);
                    }
                    ts.add(a);
                }
            }
        }
        return ans;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    int maxSumSubmatrix(vector<vector<int>>& mat, int k) {
        int m = mat.size(), n = mat[0].size();
        bool isRight = n > m;
        vector<int> sum((isRight ? n + 1 : m + 1), 0);
        int ans = INT_MIN;
        for(int i = 1; i <= (isRight ? m : n); i++){
            fill(sum.begin(),sum.end(),0);
            for(int j = i; j <= (isRight ? m : n); j++){
                set<int> st;
                st.insert(0);
                int a = 0;
                for(int fixed = 1; fixed <= (isRight ? n : m); fixed++){
                    sum[fixed] += isRight ? mat[j - 1][fixed - 1] : mat[fixed - 1][j - 1];
                    a += sum[fixed];
                    auto b = st.lower_bound(a - k);
                    if(b != st.end()){
                        int cur = a - *b;
                        ans = max(ans,cur);
                    }
                    st.insert(a);
                }
            }
        }
        return ans;
    }
};
```
* 时间复杂度:预处理「每行」或「每列」的前缀和,复杂度为 $O(m * n)$;枚举子矩阵的「上下行」或「左右行」,复杂度为 $O(\min(m, n)^2)$;结合二维前缀和套用一维最大连续子数组解决方案,复杂度为$\max(m, n)\log{max(m, n)}$。整体复杂度为 $O(\min(m, n)^2 * \max(m, n)\log{\max(m, n)})$
* 空间复杂度:$O(\max(m, n))$

### 最后

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

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

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

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


367. 有效的完全平方数

Question:
给定一个 正整数 $num$ ,编写一个函数,如果 $num$ 是一个完全平方数,则返回 $true$ ,否则返回 $false$ 。

进阶:不要使用任何内置的库函数,如  `sqrt`。

示例 1:
```
输入:num = 16

输出:true
```
示例 2:
```
输入:num = 14

输出:false
```

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

Solution:
### 二分

假如答案为 $ans$,那么以 $ans$ 为分割点的数轴上具有二段性:

* 小于 $ans$ 的一段 $x$ 必然不满足 $x * x \geq num$;
* 大于等于 $ans$ 的一段 $x$ 必然满足 $x * x \geq num$。

因此可以通过「二分」来找到分割点 $ans$。

代码:
```Java
class Solution {
    public boolean isPerfectSquare(int num) {
        long l = 0, r = num;
        while (l < r) {
            long mid = l + r + 1 >> 1;
            if (mid * mid <= num) l = mid;
            else r = mid - 1;
        }
        return r * r == num;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 数学

我们知道对于一个完全平方数而言,可以写成如下形式:

$$
num = n^2 = 1 + 3 + 5 + ... + (2 * n - 1)
$$

因此另外一种做法是对 $num$ 进行不断的奇数试减,如果最终能够减到 $0$,说明 $num$ 可展开成如 $1+3+5+...+(2*n-1)$ 的形式,$num$ 为完全平方数。

代码:
```Java
class Solution {
    public boolean isPerfectSquare(int num) {
        int x = 1;
        while (num > 0) {
            num -= x;
            x += 2;
        }
        return num == 0;
    }
}
```
* 时间复杂度:$O(\sqrt{n})$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:
* answer[i] % answer[j] == 0 ,或
* answer[j] % answer[i] == 0

如果存在多个有效解子集,返回其中任何一个均可。

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

输出:[1,2]

解释:[1,3] 也会被视为正确答案。
```
示例 2:
```
输入:nums = [1,2,4,8]

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

提示:
* 1 <= nums.length <= 1000
* 1 <= nums[i] <= 2 * $10^9$
* nums 中的所有整数 互不相同

Solution:
### 基本分析

根据题意:**对于符合要求的「整除子集」中的任意两个值,必然满足「较大数」是「较小数」的倍数。**

数据范围是 $10^3$,我们不可能采取获取所有子集,再检查子集是否合法的爆搜解法。

通常「递归」做不了,我们就往「递推」方向去考虑。

**由于存在「整除子集」中任意两个值必然存在倍数/约数关系的性质,我们自然会想到对 `nums` 进行排序,然后从集合 `nums` 中从大到小进行取数,每次取数只考虑当前决策的数是否与「整除子集」中的最后一个数成倍数关系即可。**

这时候你可能会想枚举每个数作为「整除子集」的起点,然后从前往后遍历一遍,每次都将符合「与当前子集最后一个元素成倍数」关系的数加入答案。

举个🌰,假设有原数组 `[1,2,4,8]`,“或许”我们期望的决策过程是:

1. 遍历到数字 `1`,此时「整除子集」为空,加到「整除子集」中;
2. 遍历到数字 `2`,与「整除子集」的最后一个元素(`1`)成倍数关系,加到「整除子集」中;
3. 遍历到数字 `4`,与「整除子集」的最后一个元素(`2`)成倍数关系,自然也与 `2` 之前的元素成倍数关系,加到「整除子集」中;
4. 遍历到数字 `8`,与「整除子集」的最后一个元素(`4`)成倍数关系,自然也与 `4` 之前的元素成倍数关系,加到「整除子集」中。

**但这样的做法只能够确保得到「合法解」,无法确保得到的是「最长整除子集」**。

当时担心本题数据太弱,上述错误的解法也能够通过,所以还特意实现了一下,还好被卡住了(🤣

同时也得到这个反例:`[9,18,54,90,108,180,360,540,720]`,如果按照我们上述逻辑,我们得到的是 `[9,18,54,108,540]` 答案(长度为 5),但事实上存在更长的「整除子集」: `[9,18,90,180,360,720]`(长度为 6)。

**其本质是因为同一个数的不同倍数之间不存在必然的「倍数/约数关系」,而只存在「具有公约数」的性质,这会导致我们「模拟解法」错过最优解。**

比如上述 🌰,`54` & `90` 和 `18` 存在倍数关系,但两者本身不存在倍数关系。

因此当我们决策到某一个数 `nums[i]` 时(`nums` 已排好序),我们无法直接将 `nums[i]` 直接接在符合「约数关系」的、最靠近位置 `i` 的数后面,而是要**检查位置 `i` 前面的所有符合「约数关系」的位置,找一个已经形成「整除子集」长度最大的数**。

**换句话说,当我们对 `nums` 排好序并从前往后处理时,在处理到 `nums[i]` 时,我们希望知道位置 `i` 之前的下标已经形成的「整除子集」长度是多少,然后从中选一个最长的「整除子集」,将 `nums[i]` 接在后面(前提是符合「倍数关系」)。**

---

### 动态规划

基于上述分析,我们不难发现这其实是一个序列 DP 问题:**某个状态的转移依赖于与前一个状态的关系。即 `nums[i]` 能否接在 `nums[j]` 后面,取决于是否满足 `nums[i] % nums[j] == 0` 条件。**

可看做是「最长上升子序列」问题的变形题。

**定义 $f[i]$ 为考虑前 `i` 个数字,且以第 `i` 个数为结尾的最长「整除子集」长度。**

我们不失一般性的考虑任意位置 `i`,存在两种情况:

* 如果在 `i` 之前找不到符合条件 `nums[i] % nums[j] == 0` 的位置 `j`,那么 `nums[i]` 不能接在位置 `i` 之前的任何数的后面,只能自己独立作为「整除子集」的第一个数,此时状态转移方程为 $f[i] = 1$;
* 如果在 `i` 之前能够找到符合条件的位置 `j`,则取所有符合条件的 `f[j]` 的最大值,代表如果希望找到以 `nums[i]` 为结尾的最长「整除子集」,需要将 `nums[i]` 接到符合条件的最长的 `nums[j]` 后面,此时状态转移方程为 $f[i] = f[j] + 1$。

同时由于我们需要输出具体方案,需要额外使用 `g[]` 数组来记录每个状态是由哪个状态转移而来。

**定义 $g[i]$ 为记录 $f[i]$ 是由哪个下标的状态转移而来,如果 $f[i] = f[j] + 1$, 则有 $g[i] = j$。**

对于求方案数的题目,多开一个数组来记录状态从何转移而来是最常见的手段。

当我们求得所有的状态值之后,可以对 `f[]` 数组进行遍历,取得具体的最长「整除子集」长度和对应下标,然后使用 `g[]` 数组进行回溯,取得答案。

代码:
```Java
class Solution {
    public List<Integer> largestDivisibleSubset(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] f = new int[n];
        int[] g = new int[n];
        for (int i = 0; i < n; i++) {
            // 至少包含自身一个数,因此起始长度为 1,由自身转移而来
            int len = 1, prev = i;
            for (int j = 0; j < i; j++) {
                if (nums[i] % nums[j] == 0) {
                    // 如果能接在更长的序列后面,则更新「最大长度」&「从何转移而来」
                    if (f[j] + 1 > len) {
                        len = f[j] + 1;
                        prev = j;
                    }
                }
            }
            // 记录「最终长度」&「从何转移而来」
            f[i] = len;
            g[i] = prev;
        }

        // 遍历所有的 f[i],取得「最大长度」和「对应下标」
        int max = -1, idx = -1;
        for (int i = 0; i < n; i++) {
            if (f[i] > max) {
                idx = i;
                max = f[i];
            }
        }

        // 使用 g[] 数组回溯出具体方案
        List<Integer> ans = new ArrayList<>();
        while (ans.size() != max) {
            ans.add(nums[idx]);
            idx = g[idx];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n^2)$
* 空间复杂度:$O(n)$

---

### 证明

**之所以上述解法能够成立,问题能够转化为「最长上升子序列(LIS)」问题进行求解,本质是利用了「全序关系」中的「可传递性」。**

在 LIS 问题中,我们是利用了「关系运算符 $\geqslant$ 」的传递性,因此当我们某个数 `a` 能够接在 `b` 后面,只需要确保 $a \geqslant b$ 成立,即可确保 `a` 大于等于 `b` 之前的所有值。

那么同理,如果我们想要上述解法成立,我们还需要证明如下内容:

* #### 「倍数/约数关系」具有传递性
由于我们将 `nums[i]` 往某个数字后面接时(假设为 `nums[j]`),只检查了其与 `nums[j]` 的关系,并没有去检查 `nums[i]` 与 `nums[j]` 之前的数值是否具有「倍数/约数关系」。

换句话说,我们只确保了最终答案 `[a1, a2, a3, ..., an]` 相邻两数值之间具有「倍数/约数关系」,并不明确任意两值之间具有「倍数/约数关系」。

因此需要证得由 $a | b$ 和 $b | c$,可推导出 $a | c$ 的传递性:

由 $a | b$ 可得 $b = x * a$
由 $b | c$ 可得 $c = y * b$

最终有 $c = y * b = y * x * a$,由于 $x$ 和 $y$ 都是整数,因此可得 $a | c$。

得证「倍数/约数关系」具有传递性。

### 最后

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

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

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

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


Question:
给你两个整数 `a` 和 `b` ,不使用 运算符 `+` 和 `-`,计算并返回两整数之和。

示例 1:
```
输入:a = 1, b = 2

输出:3
```
示例 2:
```
输入:a = 2, b = 3

输出:5
```

提示:
* -1000 <= a, b <= 1000

Solution:
### 位运算

一个朴素的做法是使用「位运算」,利用二进制的「逢二进一」和「`int` 二进制表示长度为 $32$」,我们可以从低位往高位进行处理,处理过程中使用变量 $t$ 记录进位信息。

然后对两数当前位进行分情况讨论:

* 两个当前位均为 $1$:此时当前位是什么取决于前一位的进位情况,即有 `ans |= (t << i)`,同时进位 $t = 1$;
* 两个当前位只有一位是 $1$:此时当前位是什么取决于前一位的进位情况(整理后可统一为 `ans |= ((1 ^ t) << i)`:
    * 前一进位若为 $1$,结合该位为 $1$,则有当前位为 $0$,进位不变 $t = 1$;
    * 前一进位若为 $0$,结合该位为 $1$,则有当前位为 $1$,进位不变 $t = 0$;
* 两个当前位为 $0$:此时当前位是什么取决于前一位的进位情况,则有 `ans |= (t << i)`,同时进位 $t = 0$。

代码:
```Java
class Solution {
    public int getSum(int a, int b) {
        int ans = 0;
        for (int i = 0, t = 0; i < 32; i++) {
            int u1 = (a >> i) & 1, u2 = (b >> i) & 1;
            if (u1 == 1 && u2 == 1) {
                ans |= (t << i);
                t = 1;
            } else if (u1 == 1 || u2 == 1) {
                ans |= ((1 ^ t) << i);
            } else {
                ans |= (t << i);
                t = 0;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(C)$,$C$ 为常数,固定为 $32$
* 空间复杂度:$O(1)$

---

### 递归

在彻底理解「解法一」后,不难发现「解法一」中本质是分别对两数的当前位进行“拆分”求解。

先计算原始的 $a$ 的和原始 $b$ 在不考虑进位的情况下的结果,结果为 `a ^ b`,然后在此基础上,考虑将进位累加进来,累加操作可以递归使用 `getSum` 来处理。

问题转化为如何求得 $a$ 和 $b$ 的进位值。

不难发现,当且仅当 $a$ 和 $b$ 的当前位均为 $1$,该位才存在进位,同时进位回应用到当前位的下一位(左边的一边,高一位),因此最终的进位结果为 `(a & b) << 1`。

因此,递归调用 `getSum(a ^ b, (a & b) << 1)` 我们可以得到最终答案。

最后还要考虑,该拆分过程何时结束。

由于在进位结果 `(a & b) << 1` 中存在左移操作,因此最多执行 $32$ 次的递归操作之后,该值会变为 $0$,而 $0$ 与任何值 $x$ 相加结果均为 $x$。

代码:
```Java
class Solution {
    public int getSum(int a, int b) {
        return b == 0 ? a : getSum(a ^ b, (a & b) << 1);
    }
}
```
* 时间复杂度:令 $C$ 为常数,固定为 $32$,最多执行 $C$ 次的 `(a & b) << 1`,递归过程结束。复杂度为 $O(C)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
你的任务是计算 `ab` 对 $1337$ 取模,`a` 是一个正整数,`b` 是一个非常大的正整数且会以数组形式给出。

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

输出:8
```
示例 2:
```
输入:a = 2, b = [1,0]

输出:1024
```
示例 3:
```
输入:a = 1, b = [4,3,3,8,5,2]

输出:1
```
示例 4:
```
输入:a = 2147483647, b = [2,0,0]

输出:1198
```

提示:
* $1 <= a <= 2^{31} - 1$
* $1 <= b.length <= 2000$
* $0 <= b[i] <= 9$
* b 不含前导 0

Solution:
### 递归 + 快速幂(可选)

根据题意,要我们求得的是 $a^b \bmod {1337}$ 的值,其中 $b$ 是以数组形式给出。

刚一看是一道快速幂的题目(事实上也确实可以使用快速幂,但不是必须),由于 $b$ 是数组形式,因此我们还需要对其进行分解。

假设 $b$ 所代表的数值为 $K$,则有:

$$
a^{K} = a^{(\left \lfloor \frac{K}{10} \right \rfloor * 10) + (K \bmod 10)} = a^{(\left \lfloor \frac{K}{10} \right \rfloor * 10)} * a^{(K \bmod 10)} = ({a^{\left \lfloor \frac{K}{10} \right \rfloor}) ^  {10}} * a^{(K \bmod 10)}
$$

也就是说,我们每次只需要计算 $b$ 数组的最后一位作为次方的值即可将问题规模缩小。

上述公式可能不好直接理解,举个🌰,设我们的 $a$ 为 $99$,要计算的 $b$ 数组所代表的数值为 $K = 2345$,那么其计算过程可以分解为:

0. $a^K = 99^{2345}$
1. $99^{2345} = 99^{234 * 10 + 5}$
2. $99^{234 * 10 + 5} = 99^{234 * 10} * 99^{5}$
3. $99^{234 * 10} * 99^{5} = {(99^{234})}^{10} * 99^{5}$
...

可见,真正涉及计算次方的操作,所用到的次方都是一个 $10$ 以内的数字,因此并非一定要使用快速幂。

代码($P2$ 为不使用快速幂):
```Java
class Solution {
    int MOD = 1337;
    public int superPow(int a, int[] b) {
        return dfs(a, b, b.length - 1);
    }
    int dfs(int a, int[] b, int u) {
        if (u == -1) return 1;
        return qpow(dfs(a, b, u - 1), 10) * qpow(a, b[u]) % MOD;
    }
    int qpow(int a, int b) {
        int ans = 1;
        a %= MOD;
        while (b != 0) {
            if ((b & 1) != 0) ans = ans * a % MOD;
            a = a * a % MOD;
            b >>= 1;
        }
        return ans;
    }
}
```

```Java
class Solution {
    int MOD = 1337;
    public int superPow(int a, int[] b) {
        return dfs(a, b, b.length - 1);
    }
    int dfs(int a, int[] b, int u) {
        if (u == -1) return 1;
        return pow(dfs(a, b, u - 1), 10) * pow(a, b[u]) % MOD;
    }
    int pow(int a, int b) {
        int ans = 1;
        a %= MOD;
        while (b-- > 0) ans = ans * a % MOD;
        return ans;
    }
}
```
* 时间复杂度:假设 $b$ 数组所代表的数字为 $K$,使用快速幂的复杂度为 $O(\log{K})$,或者说是 $O(n * \log{10})$,其中 $n$ 为数组 $b$ 的长度,数字 $10$ 所代表的含义是计算一个次方为 $10$ 以内的值;而不使用快速幂的复杂度为 $O(n * 10)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


373. 查找和最小的K对数字

Question:
给定两个以升序排列的整数数组 `nums1` 和 `nums2` , 以及一个整数 `k` 。

定义一对值 $(u,v)$,其中第一个元素来自 `nums1`,第二个元素来自 `nums2`。

请找到和最小的 `k` 个数对 $(u_1,v_1),  (u_2,v_2)  ...  (u_k,v_k)$ 。

示例 1:
```
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3

输出: [1,2],[1,4],[1,6]

解释: 返回序列中的前 3 对数:
     [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
```
示例 2:
```
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2

输出: [1,1],[1,1]

解释: 返回序列中的前 2 对数:
     [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
```
示例 3:
```
输入: nums1 = [1,2], nums2 = [3], k = 3

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

解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]
```

提示:
* $1 <= nums1.length, nums2.length <= 10^4$
* $-10^9 <= nums1[i], nums2[i] <= 10^9$
* $nums1, nums2 均为升序排列$
* $1 <= k <= 1000$

Solution:
### 基本分析

这道题和 [(题解) 786. 第 K 个最小的素数分数](https://leetcode-cn.com/problems/k-th-smallest-prime-fraction/solution/gong-shui-san-xie-yi-ti-shuang-jie-you-x-8ymk/) 几乎是一模一样,先做哪一道都是一样的,难度上没有区别 🤣

最常规的做法是使用「多路归并」,还不熟悉「多路归并」的同学,建议先学习前置🧀:[多路归并入门](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247490029&idx=1&sn=bba9ddff88d247db310406ee418d5a15&chksm=fd9cb2f2caeb3be4b1f84962677337dcb5884374e5b6b80340834eaff79298d11151da2dd5f7&token=252055586&lang=zh_CN#rd),里面讲述了如何从「朴素优先队列」往「多路归并」进行转换。

---

### 多路归并

令 $nums1$ 的长度为 $n$,$nums2$ 的长度为 $m$,所有的点对数量为 $n * m$。

其中每个 $nums1[i]$ 参与所组成的点序列为:

$$
[(nums1[0], nums2[0]), (nums1[0], nums2[1]), ..., (nums1[0], nums2[m - 1])]\\
[(nums1[1], nums2[0]), (nums1[1], nums2[1]), ..., (nums1[1], nums2[m - 1])]\\
...\\
[(nums1[n - 1], nums2[0]), (nums1[n - 1], nums2[1]), ..., (nums1[n - 1], nums2[m - 1])]\\
$$

由于 $nums1$ 和 $nums2$ 均已按升序排序,因此每个 $nums1[i]$ 参与构成的点序列也为升序排序,这引导我们使用「多路归并」来进行求解。

具体的,起始我们将这 $n$ 个序列的首位元素(点对)以二元组 $(i, j)$ 放入优先队列(小根堆),其中 $i$ 为该点对中 $nums1[i]$ 的下标,$j$ 为该点对中 $nums2[j]$ 的下标,这步操作的复杂度为 $O(n\log{n})$。这里也可以得出一个小优化是:我们始终确保 $nums1$ 为两数组中长度较少的那个,然后通过标识位来记录是否发生过交换,确保答案的点顺序的正确性。

每次从优先队列(堆)中取出堆顶元素(含义为当前未被加入到答案的所有点对中的最小值),加入答案,并将该点对所在序列的下一位(如果有)加入优先队列中。

举个 🌰,首次取出的二元组为 $(0, 0)$,即点对 $(nums1[0], nums2[0])$,取完后将序列的下一位点对 $(nums1[0], nums2[1])$ 以二元组 $(0, 1)$ 形式放入优先队列。

可通过「反证法」证明,每次这样的「取当前,放入下一位」的操作,可以确保当前未被加入答案的所有点对的最小值必然在优先队列(堆)中,即前 $k$ 个出堆的元素必然是所有点对的前 $k$ 小的值。

Java 代码:
```Java
class Solution {
    boolean flag = true;
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> ans = new ArrayList<>();
        int n = nums1.length, m = nums2.length;
        if (n > m && !(flag = false)) return kSmallestPairs(nums2, nums1, k);
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->(nums1[a[0]]+nums2[a[1]])-(nums1[b[0]]+nums2[b[1]]));
        for (int i = 0; i < Math.min(n, k); i++) q.add(new int[]{i, 0});
        while (ans.size() < k && !q.isEmpty()) {
            int[] poll = q.poll();
            int a = poll[0], b = poll[1];
            ans.add(new ArrayList<>(){{
                add(flag ? nums1[a] : nums2[b]);
                add(flag ? nums2[b] : nums1[a]);
            }});
            if (b + 1 < m) q.add(new int[]{a, b + 1});
        }
        return ans;
    }
}
```
Python3 代码:
```Python3
class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        flag, ans = (n := len(nums1)) > (m := len(nums2)), []
        if flag:
            n, m, nums1, nums2 = m, n, nums2, nums1
        pq = []
        for i in range(min(n, k)):
            heapq.heappush(pq, (nums1[i] + nums2[0], i, 0))
        while len(ans) < k and pq:
            _, a, b = heapq.heappop(pq)
            ans.append([nums2[b], nums1[a]] if flag else [nums1[a], nums2[b]])
            if b + 1 < m:
                heapq.heappush(pq, (nums1[a] + nums2[b + 1], a, b + 1))
        return ans
```
Golang 代码:
```Golang
func kSmallestPairs(nums1 []int, nums2 []int, k int) [][]int {
    n, m, ans := len(nums1), len(nums2), [][]int{}
    flag := n > m
    if flag {
        n, m, nums1, nums2 = m, n, nums2, nums1
    }
    if n > k {
        n = k
    }
    pq := make(hp, n)
    for i := 0; i < n; i++ {
        pq[i] = []int{nums1[i] + nums2[0], i, 0}
    }
    heap.Init(&pq)
    for pq.Len() > 0 && len(ans) < k {
        poll := heap.Pop(&pq).([]int)
        a, b := poll[1], poll[2]
        if flag{
            ans = append(ans, []int{nums2[b], nums1[a]})
        }else{
            ans = append(ans, []int{nums1[a], nums2[b]})
        }
        if b < m - 1 {
            heap.Push(&pq, []int{nums1[a] + nums2[b + 1], a, b + 1})
        }
    }
    return ans
}
// 最小堆模板
type hp [][]int
func (h hp) Len() int            { return len(h) }
func (h hp) Less(i, j int) bool  { return h[i][0] < h[j][0] }
func (h hp) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h *hp) Push(v interface{}) { *h = append(*h, v.([]int)) }
func (h *hp) Pop() interface{}   { a := *h; v := a[len(a)-1]; *h = a[:len(a)-1]; return v }
```
* 时间复杂度:令 $M$ 为 $n$、$m$ 和 $k$ 三者中的最小值,复杂度为 $O(M + k) \times \log{M})$
* 空间复杂度:$O(M)$

---

### 二分

我们还能够使用多次「二分」来做。

假设我们将所有「数对和」按照升序排序,两端的值分别为 $l = nums1[0] + nums2[0]$ 和 $r = nums1[n - 1] + nums2[m - 1]$。

因此我们可以在值域 $[l, r]$ 上进行二分,找到第一个满足「点对和小于等于 $x$ 的,且数量超过 $k$ 的值 $x$」。

之所以能够二分,是因为 $x$ 所在的点对和数轴上具有二段性:

* 点对和小于 $x$ 的点对数量少于 $k$ 个;
* 点对和大于等于 $x$ 的点对数量大于等于 $k$ 个。

判定小于等于 $x$ 的点对数量是否大于等于 $k$ 个这一步可直接使用循环来做,由于二分是从中间值开始,这一步不会出现跑满两层循环的情况。

当二分出第 $k$ 小的值为 $x$ 后,由于存在不同点对的点对和值相等,我们需要先将所有点对和小于等于 $x$ 的值加入答案,然后酌情把值等于 $x$ 的点对加入答案,知道满足答案数量为 $k$。

找值为 $x$ 的所有点对这一步,可以通过枚举 $nums1[i]$,然后在 $nums2$ 上二分目标值 $x - nums1[i]$ 的左右端点来做。

最后,在所有处理过程中,我们都可以利用答案数组的大小与 $k$ 的关系做剪枝。

Java 代码:
```Java
class Solution {
    int[] nums1, nums2;
    int n, m;
    public List<List<Integer>> kSmallestPairs(int[] n1, int[] n2, int k) {
        nums1 = n1; nums2 = n2;
        n = nums1.length; m = nums2.length;
        List<List<Integer>> ans = new ArrayList<>();
        int l = nums1[0] + nums2[0], r = nums1[n - 1] + nums2[m - 1];
        while (l < r) {
            int mid = (int)(0L + l + r >> 1);
            if (check(mid, k)) r = mid;
            else l = mid + 1;
        }
        int x = r;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (nums1[i] + nums2[j] < x) {
                    List<Integer> temp = new ArrayList<>();
                    temp.add(nums1[i]); temp.add(nums2[j]);
                    ans.add(temp);
                } else break;
            }
        }
        for (int i = 0; i < n && ans.size() < k; i++) {
            int a = nums1[i], b = x - a;
            int c = -1, d = -1;
            l = 0; r = m - 1;
            while (l < r) {
                int mid = (int)(0L + l + r >> 1);
                if (nums2[mid] >= b) r = mid;
                else l = mid + 1;
            }
            if (nums2[r] != b) continue;
            c = r;
            l = 0; r = m - 1;
            while (l < r) {
                int mid = (int)(0L + l + r + 1) >> 1;
                if (nums2[mid] <= b) l = mid;
                else r = mid - 1;
            }
            d = r;
            for (int p = c; p <= d && ans.size() < k; p++) {
                List<Integer> temp = new ArrayList<>();
                temp.add(a); temp.add(b);
                ans.add(temp);
            }
        }
        return ans;
    }
    boolean check(int x, int k) {
        int ans = 0;
        for (int i = 0; i < n && ans < k; i++) {
            for (int j = 0; j < m && ans < k; j++) {
                if (nums1[i] + nums2[j] <= x) ans++;
                else break;
            }
        }
        return ans >= k;
    }
}
```
* 时间复杂度:假设点对和的值域大小范围为 $M$,第一次二分的复杂度为 $O((n \times m) \times \log{M})$;统计点对和值小于目标值 $x$ 的复杂度为 $O(n \times m)$;统计所有点对和等于目标值的复杂度为 $O(\max(n \times \log{m}, k))$(整个处理过程中利用了大小关系做了剪枝,大多循环都不会跑满,实际计算量会比理论分析的要低)
* 空间复杂度:$O(k)$

### 最后

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

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

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

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


Question:
猜数字游戏的规则如下:
* 每轮游戏,我都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
* 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-1,1 或 0):

* -1:我选出的数字比你猜的数字小 pick < num
* 1:我选出的数字比你猜的数字大 pick > num
* 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

示例 1:
```
输入:n = 10, pick = 6
输出:6
```
示例 2:
```
输入:n = 1, pick = 1
输出:1
```
示例 3:
```
输入:n = 2, pick = 1
输出:1
```
示例 4:
```
输入:n = 2, pick = 2
输出:2
```

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

Solution:
### 二分

端午安康呀 🍭🍭🍭

今天的题目表达不太好 🤣,还是逐字阅读才明白是啥意思 🤣

就当做是因为题目本身要比 [278. 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version/) 简单(不需要考虑完全相同,没有二段性的情况),所以在阅读上增加了难度吧 🤣

一道交互题,根据题意可知,是尽可能少调用 `guess` 方法来找到分割点。

直接使用 `guess` 当做 `check` 函数进行二分即可。

另外根据数据范围需要注意计算 `mid` 时的爆 `int` 问题,可以通过使用类似 `l + (r - l) / 2` 的做法解决,也可以通过一个临时 `long` 来解决。

代码:
```Java
public class Solution extends GuessGame {
    public int guessNumber(int n) {
        int l = 1, r = n;
        while (l < r) {
            long tmp = (long)l + r >> 1;
            int mid = (int)tmp;
            if (guess(mid) <= 0) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return r;
    }
}
```
```Java
public class Solution extends GuessGame {
    public int guessNumber(int n) {
        int l = 1, r = n;
        while (l < r) {
            long tmp = (long)l + r + 1 >> 1;
            int mid = (int)tmp;
            if (guess(mid) >= 0) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        return r;
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:$O(1)$

---

### 其他「二分」相关题解

* 二分模板
    [29. 两数相除](https://leetcode-cn.com/problems/divide-two-integers/) : [二分 + 倍增乘法解法(含模板)](https://leetcode-cn.com/problems/divide-two-integers/solution/shua-chuan-lc-er-fen-bei-zeng-cheng-fa-j-m73b/)

* 二分模板题
    [278. 第一个错误的版本](https://leetcode-cn.com/problems/first-bad-version/) : [使用交互函数充当 check 进行二分](https://leetcode-cn.com/problems/first-bad-version/solution/gong-shui-san-xie-shi-yong-jiao-hu-han-s-8hpv/)

    [374. 猜数字大小](https://leetcode-cn.com/problems/guess-number-higher-or-lower/) : [使用交互函数充当 check 进行二分](https://leetcode-cn.com/problems/guess-number-higher-or-lower/solution/gong-shui-san-xie-shi-yong-jiao-hu-han-s-tocm/)

* 二分本质 & 恢复二段性处理

    [33. 搜索旋转排序数组(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array/solution/shua-chuan-lc-yan-ge-ologn100yi-qi-kan-q-xifo/)

    [81. 搜索旋转排序数组 II(找目标值)](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-xtam4/)

    [153. 寻找旋转排序数组中的最小值(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/) : [严格 O(logN),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/solution/gong-shui-san-xie-yan-ge-olognyi-qi-kan-6d969/)

    [154. 寻找旋转排序数组中的最小值 II(找最小值)](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/) : [详解为何元素相同会导致 O(n),一起看清二分的本质](https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/solution/gong-shui-san-xie-xiang-jie-wei-he-yuan-7xbty/)

* 二分 check 函数如何确定
    [34. 在排序数组中查找元素的第一个和最后一个位置](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) : [考察对「二分」的理解,以及 check 函数的「大于 小于」怎么写](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/solution/gong-shui-san-xie-kao-cha-dui-er-fen-de-86bk0/)

### 最后

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

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

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

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


Question:
我们正在玩一个猜数游戏,游戏规则如下:
1. 我从 `1` 到 `n` 之间选择一个数字。
2. 你来猜我选了哪个数字。
3. 如果你猜到正确的数字,就会 赢得游戏 。
4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
5. 每当你猜了数字 `x` 并且猜错了的时候,你需要支付金额为 `x` 的现金。如果你花光了钱,就会 输掉游戏 。

给你一个特定的数字 `n` ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。

示例 1:

```
输入:n = 10

输出:16

解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
    - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
    - 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
        - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
        - 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
        - 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
    - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
        - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
        - 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
            - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
            - 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
            - 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
        - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
            - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
            - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
```
示例 2:
```
输入:n = 1

输出:0

解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
```
示例 3:
```
输入:n = 2

输出:1

解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
    - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
    - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。
```

提示:
* 1 <= n <= 200

Solution:
### 基本分析

这不是一道可通过「二分」求解的题目,主要原因为每次惩罚的金额不固定,最小惩罚次数不等同于猜中数字的最小成本。

---

### 记忆化搜索

比较容易想到的做法为使用「递归」进行求解。

设计递归函数为 `int dfs(int l, int r)` 传入参数 `l` 和 `r` 代表在范围 $[l, r]$ 内进行猜数,返回值为在 $[l, r]$ 内猜中数字至少需要多少钱。

**我们可决策的部分为「选择猜哪个数 $x$」,而不可决策的是「选择某个数 $x$ 之后(假设没有猜中),真实值会落在哪边」。**

因此为求得「最坏情况下最好」的结果,我们应当取所有的 $x$ 中的最小值。

最后,为减少重复计算,我们需要在「递归」基础上加入记忆化搜索。并且当我们使用 `static` 修饰 $cache$ 时,可以确保每个区间的计算在所有样例中只会发生一次。

代码:
```Java
class Solution {
    static int N = 210;
    static int[][] cache = new int[N][N];
    public int getMoneyAmount(int n) {
        return dfs(1, n);
    }
    int dfs(int l, int r) {
        if (l >= r) return 0;
        if (cache[l][r] != 0) return cache[l][r];
        int ans = 0x3f3f3f3f;
        for (int x = l; x <= r; x++) {
            // 当选择的数位 x 时,至少需要 cur 才能猜中数字
            int cur = Math.max(dfs(l, x - 1), dfs(x + 1, r)) + x;
            // 在所有我们可以决策的数值之间取最优
            ans = Math.min(ans, cur);
        }
        cache[l][r] = ans;
        return ans;
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(n^2)$

---

### 区间 DP

同样能够通过「递推」来进行求解。

**通过「记忆化搜索」的递归过程,我们发现,在求解 $[l, r]$ 的最小成本时,需要依赖于 $[l, i - 1]$ 和 $[i + 1, r]$ 这样的比 $[l, r]$ 更小的区间。**

这引导我们使用「区间 DP」进行求解,对「区间 DP」不了解的同学可以先看 [「区间 DP」入门题](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247489400&idx=1&sn=0b629d3669329a6bf4f6ec71c2571ce7&chksm=fd9cbc67caeb357132fe0a1ca6240e2183748d94039100f539193d3eeb1dc223e0ddd4aa9584&token=2094656911&lang=zh_CN#rd) 。

**定义 $f[l][r]$ 为考虑在 $[l, r]$ 范围内进行猜数的最小成本。**

不失一般性的考虑 $f[l][r]$ 该如何计算。**同样的,我们可决策的部分为「选择猜哪个数 $x$」,而不可决策的是「选择某个数 $x$ 之后(假设没有猜中),真实值在落在哪边」。**

我们对本次选择哪个数进行讨论,假设本次选择的数值为 $x$ ( $l <= x <= r$ ),则有 $cur = \max(f[l][x - 1], f[x + 1][r]) + x$

最终的 $f[l][r]$ 为所有可选的数值 $x$ 中的最小值。

代码:
```Java
class Solution {
    public int getMoneyAmount(int n) {
        int[][] f = new int[n + 10][n + 10];
        for (int len = 2; len <= n; len++) {
            for (int l = 1; l + len - 1 <= n; l++) {
                int r = l + len - 1;
                f[l][r] = 0x3f3f3f3f;
                for (int x = l; x <= r; x++) {
                    int cur = Math.max(f[l][x - 1], f[x + 1][r]) + x;
                    f[l][r] = Math.min(f[l][r], cur);
                }
            }
        }
        return f[1][n];
    }
}
```
* 时间复杂度:$O(n^3)$
* 空间复杂度:$O(n^2)$

---

### 打表

由于任意的 $[l,r]$ 对应结果均为定值,可以进行打表预处理。

代码:
```Java
class Solution {
    static int N = 200;
    static int[][] f = new int[N + 10][N + 10];
    static {
        for (int len = 2; len <= N; len++) {
            for (int l = 1; l + len - 1 <= N; l++) {
                int r = l + len - 1;
                f[l][r] = 0x3f3f3f3f;
                for (int x = l; x <= r; x++) {
                    int cur = Math.max(f[l][x - 1], f[x + 1][r]) + x;
                    f[l][r] = Math.min(f[l][r], cur);
                }
            }
        }
    }
    public int getMoneyAmount(int n) {
        return f[1][n];
    }
}
```
* 时间复杂度:$O(C^3)$
* 空间复杂度:$O(C^2)$

### 最后

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

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

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

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


LeetCode/371-380/380. O(1) 时间插入、删除和获取随机元素（中等）.md

380. O(1) 时间插入、删除和获取随机元素

Question:
实现 `RandomizedSet` 类:

* `RandomizedSet()` 初始化 `RandomizedSet` 对象
* `bool insert(int val)` 当元素 `val` 不存在时,向集合中插入该项,并返回 `true`;否则,返回 `false`。
* `bool remove(int val)` 当元素 `val` 存在时,从集合中移除该项,并返回 `true`;否则,返回 `false`。
* `int getRandom()` 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。

你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 $O(1)$ 。

示例:
```
输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]

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

解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
```

提示:
* $-2^{31} <= val <= 2^{31} - 1$
* 最多调用 `insert`、`remove` 和 `getRandom` 函数 $2 * 10^5$ 次
* 在调用 `getRandom` 方法时,数据结构中 至少存在一个 元素。

Solution:
### 哈希表 + 删除交换

对于 `insert` 和 `remove` 操作容易想到使用「哈希表」来实现 $O(1)$ 复杂度,但对于 `getRandom` 操作,比较理想的情况是能够在一个数组内随机下标进行返回。

将两者结合,我们可以将哈希表设计为:以入参 `val` 为键,数组下标 `loc` 为值。

**为了确保严格 $O(1)$,我们不能「使用拒绝采样」和「在数组非结尾位置添加/删除元素」。**

因此我们需要申请一个足够大的数组 `nums`(利用数据范围为 $2* 10^5$),并使用变量 `idx` 记录当前使用到哪一位(即下标在 $[0, idx]$ 范围内均是存活值)。

对于几类操作逻辑:

* `insert` 操作:使用哈希表判断 `val` 是否存在,存在的话返回 `false`,否则将其添加到 `nums`,更新 `idx`,同时更新哈希表;
* `remove` 操作:使用哈希表判断 `val` 是否存在,不存在的话返回 `false`,否则从哈希表中将 `val` 删除,同时取出其所在 `nums` 的下标 `loc`,然后将 `nums[idx]` 赋值到 `loc` 位置,并更新 `idx`(含义为将原本处于 `loc` 位置的元素删除),同时更新原本位于 `idx` 位置的数在哈希表中的值为 `loc`(若 `loc` 与 `idx` 相等,说明删除的是最后一个元素,这一步可跳过);
* `getRandom` 操作:由于我们人为确保了 $[0, idx]$ 均为存活值,因此直接在 $[0, idx + 1)$ 范围内进行随机即可。

代码:
```Java
class RandomizedSet {
    static int[] nums = new int[200010];
    Random random = new Random();
    Map<Integer, Integer> map = new HashMap<>();
    int idx = -1;
    public boolean insert(int val) {
        if (map.containsKey(val)) return false;
        nums[++idx] = val;
        map.put(val, idx);
        return true;
    }
    public boolean remove(int val) {
        if (!map.containsKey(val)) return false;
        int loc = map.remove(val);
        if (loc != idx) map.put(nums[idx], loc);
        nums[loc] = nums[idx--];
        return true;
    }
    public int getRandom() {
        return nums[random.nextInt(idx + 1)];
    }
}
```
* 时间复杂度:所有操作均为 $O(1)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点 **被选中的概率一样** 。

实现 `Solution` 类:
* `Solution(ListNode head)` 使用整数数组初始化对象。
* `int getRandom()` 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。

示例:

```
输入
["Solution", "getRandom", "getRandom", "getRandom", "getRandom", "getRandom"]
[[[1, 2, 3]], [], [], [], [], []]

输出
[null, 1, 3, 2, 2, 3]

解释
Solution solution = new Solution([1, 2, 3]);
solution.getRandom(); // 返回 1
solution.getRandom(); // 返回 3
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 2
solution.getRandom(); // 返回 3
// getRandom() 方法应随机返回 1、2、3中的一个,每个元素被返回的概率相等。
```

提示:
* $链表中的节点数在范围 [1, 10^4] 内$
* $-10^4 <= Node.val <= 10^4$
* 至多调用 `getRandom` 方法 $10^4$ 次

进阶:
* 如果链表非常大且长度未知,该怎么处理?
* 你能否在不使用额外空间的情况下解决此问题?

Solution:
### 模拟

由于链表长度只有 $10^4$,因此可以在初始化时遍历整条链表,将所有的链表值预处理到一个数组内。

在查询时随机一个下标,并将数组中对应下标内容返回出去。

**代码(感谢 [@Benhao](/u/himymben/) 同学提供的其他语言版本):**
```Java
class Solution {
    List<Integer> list = new ArrayList<>();
    Random random = new Random(20220116);
    public Solution(ListNode head) {
        while (head != null) {
            list.add(head.val);
            head = head.next;
        }
    }
    public int getRandom() {
        int idx = random.nextInt(list.size());
        return list.get(idx);
    }
}
```
-
```Python3
class Solution:

    def __init__(self, head: Optional[ListNode]):
        self.nodes = []
        while head:
            self.nodes.append(head)
            head = head.next

    def getRandom(self) -> int:
        return self.nodes[randint(0, len(self.nodes) - 1)].val
```
-
```Golang
type Solution struct {
    Nodes []int
}
func Constructor(head *ListNode) Solution {
    nodes := make([]int, 0)
    for head != nil {
        nodes = append(nodes, head.Val)
        head = head.Next
    }
    return Solution{nodes}
}
func (this *Solution) GetRandom() int {
    return this.Nodes[rand.Intn(len(this.Nodes))]
}
```
* 时间复杂度:令 $n$ 为链表长度,预处理数组的复杂度为 $O(n)$;随机获取某个值的复杂度为 $O(1)$
* 空间复杂度:$O(n)$

---

### 蓄水池抽样

整理题意:总的样本数量未知,从所有样本中抽取若干个,要求每个样本被抽到的概率相等。

具体做法为:**从前往后处理每个样本,每个样本成为答案的概率为 $\frac{1}{i}$,其中 $i$ 为样本编号(编号从 $1$ 开始),最终可以确保每个样本成为答案的概率均为 $\frac{1}{n}$(其中 $n$ 为样本总数)。**

容易证明该做法的正确性,假设最终成为答案的样本编号为 $k$,那么 $k$ 成为答案的充要条件为「在遍历到 $k$ 时被选中」并且「遍历大于 $k$ 的所有元素时,均没有被选择(没有覆盖 $k$)」。

对应事件概率为:

$$
P = \frac{1}{k} * (1 - \frac{1}{k + 1}) * (1 - \frac{1}{k + 2}) * ... * (1 - \frac{1}{n})
$$

首项 $\frac{1}{k}$ 为选中 $k$ 的概率,后面每项分别为编号为 $[k + 1, n]$ 的样本 **不被选中** 的概率。

化简得:

$$
P = \frac{1}{k} * \frac{k}{k + 1} * \frac{k + 1}{k + 2} * ... * \frac{n - 1}{n}
$$

进一步抵消化简后,可得:
$$
P = \frac{1}{n}
$$

因此,在每一次 `getRandom` 时,从前往后处理每个节点,同时记录当前节点的编号,当处理到节点 $k$ 时,在 $[0, k)$ 范围内进行随机,若随机到结果为 $0$(发生概率为 $\frac{1}{k}$),则将节点 $k$ 的值存入答案,最后一次覆盖答案的节点即为本次抽样结果。

**代码(感谢 [@Benhao](/u/himymben/) 同学提供的其他语言版本):**
```Java
class Solution {
    ListNode head;
    Random random = new Random(20220116);
    public Solution(ListNode _head) {
        head = _head;
    }
    public int getRandom() {
        int ans = 0, idx = 0;
        ListNode t = head;
        while (t != null && ++idx >= 0) {
            if (random.nextInt(idx) == 0) ans = t.val;
            t = t.next;
        }
        return ans;
    }
}
```
-
```Python3
class Solution:

    def __init__(self, head: Optional[ListNode]):
        self.root = head

    def getRandom(self) -> int:
        node, ans, i = self.root, None, 0
        while node:
            if not randint(0, i):
                ans = node.val
            node, i = node.next, i + 1
        return ans
```
-
```Golang
type Solution struct {
    Root *ListNode
}
func Constructor(head *ListNode) Solution {
    return Solution{head}
}
func (this *Solution) GetRandom() (ans int) {
    for node, idx := this.Root, 1;node != nil; idx++ {
        if rand.Intn(idx) == 0{
            ans = node.Val
        }
        node = node.Next
    }
    return
}
```
* 时间复杂度:令 $n$ 为链表长度,随机获取某个值的复杂度为 $O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


Question:
为了不在赎金信中暴露字迹,从杂志上搜索各个需要的字母,组成单词来表达意思。

给你一个赎金信 (`ransomNote`) 字符串和一个杂志(`magazine`)字符串,判断` ransomNote` 能不能由 `magazines` 里面的字符构成。

如果可以构成,返回 `true` ;否则返回 `false` 。

`magazine` 中的每个字符只能在 `ransomNote` 中使用一次。

示例 1:
```
输入:ransomNote = "a", magazine = "b"

输出:false
```
示例 2:
```
输入:ransomNote = "aa", magazine = "ab"

输出:false
```
示例 3:
```
输入:ransomNote = "aa", magazine = "aab"

输出:true
```

提示:
* $1 <= ransomNote.length, magazine.length <= 10^5$
* `ransomNote` 和 `magazine` 由小写英文字母组成

Solution:
### 计数模拟

根据题意并利用两字符串只包含小写字母,先使用大小为字符集大小的数组(充当哈希表)对 $magazine$ 进行词频统计,然后在遍历 $ransomNote$ 时对计数数组进行抵消操作。

若处理过程中出现词频数量为负数,则说明 $magazine$ 不能凑出 $ransomNote$。

代码:
```Java
class Solution {
    public boolean canConstruct(String a, String b) {
        int[] cnt = new int[26];
        for (char c : b.toCharArray()) cnt[c - 'a']++;
        for (char c : a.toCharArray()) if (--cnt[c - 'a'] < 0) return false;
        return true;
    }
}
```
* 时间复杂度:令 $n$ 和 $m$ 分别为两字符串的长度,复杂度为 $O(n + m)$
* 空间复杂度:令字符集大小为 $C = 26$。偷懒使用 `toCharArray` 操作的话,复杂度为 $O(n + m + C)$;使用 `charAt` 代替的话,复杂度为 $O(C)$

### 最后

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

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

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

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


Question:
给你一个整数数组 `nums`,设计算法来打乱一个没有重复元素的数组。

实现 `Solution class`:
* `Solution(int[] nums)` 使用整数数组 `nums` 初始化对象
* `int[] reset()` 重设数组到它的初始状态并返回
* `int[] shuffle()` 返回数组随机打乱后的结果

示例:
```
输入
["Solution", "shuffle", "reset", "shuffle"]
[[[1, 2, 3]], [], [], []]
输出
[null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]

解释
Solution solution = new Solution([1, 2, 3]);
solution.shuffle();    // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2]
solution.reset();      // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3]
solution.shuffle();    // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]
```

提示:
* $1 <= nums.length <= 200$
* $-10^6 <= nums[i] <= 10^6$
* `nums` 中的所有元素都是 唯一的
* 最多可以调用 $5 \times 10^4$ 次 `reset` 和 `shuffle`

Solution:
### 洗牌算法

共有 $n$ 个不同的数,根据每个位置能够选择什么数,共有 $n!$ 种组合。

题目要求每次调用 `shuffle` 时等概率返回某个方案,或者说每个元素都够等概率出现在每个位置中。

我们可以使用 $Knuth$ 洗牌算法,在 $O(n)$ 复杂度内等概率返回某个方案。

具体的,我们从前往后尝试填充 $[0, n - 1]$ 该填入什么数时,通过随机当前下标与(剩余的)哪个下标进行值交换来实现。

对于下标 $x$ 而言,我们从 $[x, n - 1]$ 中随机出一个位置与 $x$ 进行值交换,当所有位置都进行这样的处理后,我们便得到了一个公平的洗牌方案。

> 对于下标为 $0$ 位置,从 $[0, n - 1]$ 随机一个位置进行交换,共有 $n$ 种选择;下标为 $1$ 的位置,从 $[1, n - 1]$ 随机一个位置进行交换,共有 $n - 1$ 种选择 ... 且每个位置的随机位置交换过程相互独立。

Java 代码:
```Java
class Solution {
    int[] nums;
    int n;
    Random random = new Random();
    public Solution(int[] _nums) {
        nums = _nums;
        n = nums.length;
    }
    public int[] reset() {
        return nums;
    }
    public int[] shuffle() {
        int[] ans = nums.clone();
        for (int i = 0; i < n; i++) {
            swap(ans, i, i + random.nextInt(n - i));
        }
        return ans;
    }
    void swap(int[] arr, int i, int j) {
        int c = arr[i];
        arr[i] = arr[j];
        arr[j] = c;
    }
}
```
C++ 代码:
```C++
class Solution {
public:
    vector<int> nums;
    int n;
    Solution(vector<int>& _nums) : nums(_nums), n(_nums.size()) {}
    vector<int> reset() {
        return nums;
    }
    vector<int> shuffle() {
        vector<int> ans = nums;
        for (int i = 0; i < n; i++) {
            swap(ans[i], ans[i + rand() % (n - i)]);
        }
        return ans;
    }
};
```
Python 代码:
```Python
class Solution:
    def __init__(self, nums: List[int]):
        self.nums = nums
        self.n = len(nums)

    def reset(self) -> List[int]:
        return self.nums

    def shuffle(self) -> List[int]:
        ans = self.nums[:]
        for i in range(self.n):
            j = i + random.randint(0, self.n - i - 1)
            ans[i], ans[j] = ans[j], ans[i]
        return ans
```
TypeScript 代码:
```TypeScript
class Solution {
    private nums: number[];
    private n: number;

    constructor(nums: number[]) {
        this.nums = nums;
        this.n = nums.length;
    }

    reset(): number[] {
        return this.nums.slice();
    }

    shuffle(): number[] {
        const ans: number[] = this.nums.slice();
        for (let i = 0; i < this.n; i++) {
            const j = i + Math.floor(Math.random() * (this.n - i));
            [ans[i], ans[j]] = [ans[j], ans[i]];
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


385. 迷你语法分析器

Question:
给定一个字符串 `s` 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 `NestedInteger` 。

列表中的每个元素只可能是整数或整数嵌套列表

示例 1:
```
输入:s = "324",

输出:324

解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。
```
示例 2:
```
输入:s = "[123,[456,[789]]]",

输出:[123,[456,[789]]]

解释:返回一个 NestedInteger 对象包含一个有两个元素的嵌套列表:
1. 一个 integer 包含值 123
2. 一个包含两个元素的嵌套列表:
    i.  一个 integer 包含值 456
    ii. 一个包含一个元素的嵌套列表
         a. 一个 integer 包含值 789
```

提示:
* $1 <= s.length <= 5 * 10^4$
* `s` 由数字、方括号 `"[]"`、负号 `'-'` 、逗号 `','`组成
* 用例保证 `s` 是可解析的 `NestedInteger`
* 输入中的所有值的范围是 $[-10^6, 10^6]$

Solution:
### 栈

每个 `[` 的出现意味着存在一个嵌套类型的 `NestedInteger` 实例,同时每个 `NestedInteger` 实例(无论是嵌套类型还是数值类型)都归属于其最近一个左边的嵌套类型的 `NestedInteger` 实例(即其左边最近一个 `[`),因此我们可以使用栈来处理。

对出现的几类字符进行简单分情况讨论:

* `,`:跳过即可;
* `-` 或 `数字`:将连续段代表数值的字符串取出,创建数值型的 `NestedInteger` 实例并压入栈中;
* `[`:创建一个嵌套类型的 `NestedInteger` 实例并压入栈中,同时为了方便,同时压入一个起「标识」作用的 `NestedInteger` 对象;
* `]`:从栈中取出元素,直到遇到起「标识」作用的 `NestedInteger` 对象(说明找到与当前 `]` 成对的 `[`),将 `[` 和 `]` 之间的所有元素添加到 `[` 所代指的嵌套 `NestedInteger` 实例中,然后将 `[` 所代指的嵌套 `NestedInteger` 实例重新放入栈中。

按照上述逻辑处理完整个 `s`,最终栈顶元素即是答案。

代码:
```Java
class Solution {
    static NestedInteger ph = new NestedInteger(0);
    public NestedInteger deserialize(String s) {
        Deque<NestedInteger> d = new ArrayDeque<>();
        char[] cs = s.toCharArray();
        int n = cs.length, i = 0;
        while (i < n) {
            if (cs[i] == ',' && ++i >= 0) continue;
            if (cs[i] == '-' || (cs[i] >= '0' && cs[i] <= '9')) {
                int j = cs[i] == '-' ? i + 1 : i, num = 0;
                while (j < n && (cs[j] >= '0' && cs[j] <= '9')) num = num * 10 + (cs[j++] - '0');
                d.addLast(new NestedInteger(cs[i] == '-' ? -num : num));
                i = j;
            } else if (cs[i] == '[') {
                d.addLast(new NestedInteger());
                d.addLast(ph);
                i++;
            } else {
                List<NestedInteger> list = new ArrayList<>();
                while (!d.isEmpty()) {
                    NestedInteger poll = d.pollLast();
                    if (poll == ph) break;
                    list.add(poll);
                }
                for (int j = list.size() - 1; j >= 0; j--) d.peekLast().add(list.get(j));
                i++;
            }
        }
        return d.peekLast();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给你一个整数 `n` ,按字典序返回范围 $[1, n]$ 内所有整数。

你必须设计一个时间复杂度为 $O(n)$ 且使用 $O(1)$ 额外空间的算法。

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

输出:[1,10,11,12,13,2,3,4,5,6,7,8,9]
```
示例 2:
```
输入:n = 2

输出:[1,2]
```

提示:
* $1 <= n <= 5 * 10^4$

Solution:
### 递归

首先容易想到使用「递归」来实现 `DFS`。

将 $[1, n]$ 的数按照字典序添加到答案,本质上是对一颗节点数量为 $n$,形态类似字典树的多阶树进行遍历,根节点为 $0$,需要被跳过,因此我们可以从树的第二层开始搜索。

树中每个节点的值为其搜索路径所代表的数字,且每个节点有 $[0, 9]$ 共 $10$ 个子节点。

代码:
```Java
class Solution {
    List<Integer> ans = new ArrayList<>();
    public List<Integer> lexicalOrder(int n) {
        for (int i = 1; i <= 9; i++) dfs(i, n);
        return ans;
    }
    void dfs(int cur, int limit) {
        if (cur > limit) return ;
        ans.add(cur);
        for (int i = 0; i <= 9; i++) dfs(cur * 10 + i, limit);
    }
}
```
* 时间复杂度:本质上在搜索一棵节点数量为 $n$ 的多阶树(形态类似于字典树),复杂度为 $O(n)$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

---

### 迭代

递归具有额外的空间开销,为了实现严格的 $O(1)$ 空间,我们需要使用「迭代」来实现 `DFS`。

共有 $n$ 个数需要被处理,假设当前处理到的数为 $j$,根据字典序规则,在满足条件的前提下,我们优先在 $j$ 的后面添加 $0$(即 $j * 10 < n$ 满足),否则我们考虑将上一位回退并进行加一操作。

代码:
```Java
class Solution {
    public List<Integer> lexicalOrder(int n) {
        List<Integer> ans = new ArrayList<>();
        for (int i = 0, j = 1; i < n; i++) {
            ans.add(j);
            if (j * 10 <= n) {
                j *= 10;
            } else {
                while (j % 10 == 9 || j + 1 > n) j /= 10;
                j++;
            }
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


388. 文件的最长绝对路径

Question:
假设有一个同时存储文件和目录的文件系统。下图展示了文件系统的一个示例:

这里将 `dir` 作为根目录中的唯一目录。`dir` 包含两个子目录 `subdir1` 和 `subdir2` 。

`subdir1` 包含文件 `file1.ext` 和子目录 `subsubdir1`;`subdir2` 包含子目录 `subsubdir2`,该子目录下包含文件 `file2.ext`。

在文本格式中,如下所示(⟶表示制表符):
```
dir
⟶ subdir1
⟶ ⟶ file1.ext
⟶ ⟶ subsubdir1
⟶ subdir2
⟶ ⟶ subsubdir2
⟶ ⟶ ⟶ file2.ext
```

如果是代码表示,上面的文件系统可以写为 `"dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"` 。`'\n'` 和 `'\t'` 分别是换行符和制表符。

文件系统中的每个文件和文件夹都有一个唯一的 绝对路径 ,即必须打开才能到达文件/目录所在位置的目录顺序,所有路径用 '/' 连接。上面例子中,指向 `file2.ext` 的 绝对路径 是 `"dir/subdir2/subsubdir2/file2.ext"` 。每个目录名由字母、数字和/或空格组成,每个文件名遵循 `name.extension` 的格式,其中 `name` 和 `extension` 由字母、数字和/或空格组成。

给定一个以上述格式表示文件系统的字符串 `input` ,返回文件系统中 指向 文件 的 最长绝对路径 的长度 。 如果系统中没有文件,返回 $0$。

示例 1:

```
输入:input = "dir\n\tsubdir1\n\tsubdir2\n\t\tfile.ext"

输出:20

解释:只有一个文件,绝对路径为 "dir/subdir2/file.ext" ,路径长度 20
```
示例 2:

```
输入:input = "dir\n\tsubdir1\n\t\tfile1.ext\n\t\tsubsubdir1\n\tsubdir2\n\t\tsubsubdir2\n\t\t\tfile2.ext"

输出:32

解释:存在两个文件:
"dir/subdir1/file1.ext" ,路径长度 21
"dir/subdir2/subsubdir2/file2.ext" ,路径长度 32
返回 32 ,因为这是最长的路径
```
示例 3:
```
输入:input = "a"

输出:0

解释:不存在任何文件
```
示例 4:
```
输入:input = "file1.txt\nfile2.txt\nlongfile.txt"

输出:12

解释:根目录下有 3 个文件。
因为根目录中任何东西的绝对路径只是名称本身,所以答案是 "longfile.txt" ,路径长度为 12
```

提示:
* $1 <= input.length <= 10^4$
* `input` 可能包含小写或大写的英文字母,一个换行符 `'\n'`,一个制表符 `'\t'`,一个点 `'.'`,一个空格 `' '`,和数字。

Solution:
### 模拟 + 哈希表

为了方便,我们将 `input` 替换为 `s`。

对于每一个文件或文件夹而言,我们可以通过访问到结尾(`\n`)的方式取得,记为 `cur`,然后根据 `cur` 前面有多少个 `\t` 得知其所在的层级,假设当前其所在层级为 `level`,那么它自然归属到最新一个层级为 `level - 1` 的文件夹中,因此我们可以使用哈希表记录每个层级最新的文件夹路径,通过字符串拼接的方式得到 `cur` 所在的完整路径 `path`,并在处理整个 `s` 过程中,统计长度最大的文件路径。

代码:
```Java
class Solution {
    public int lengthLongestPath(String s) {
        Map<Integer, String> map = new HashMap<>();
        int n = s.length();
        String ans = null;
        for (int i = 0; i < n; ) {
            int level = 0;
            while (i < n && s.charAt(i) == '\t' && ++level >= 0) i++;
            int j = i;
            boolean isDir = true;
            while (j < n && s.charAt(j) != '\n') {
                if (s.charAt(j++) == '.') isDir = false;
            }
            String cur = s.substring(i, j);
            String prev = map.getOrDefault(level - 1, null);
            String path = prev == null ? cur : prev + "/" + cur;
            if (isDir) map.put(level, path);
            else if (ans == null || path.length() > ans.length()) ans = path;
            i = j + 1;
        }
        return ans == null ? 0 : ans.length();
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(n)$

---

### 优化

上述做法只是为了方便我们输出具体方案。

实际上,我们只关心最终的路径长度,而不关心具体路径,因此容易将解法一修改为只记录长度,而不记录路径的做法,从而避免掉字符串拼接带来的消耗,同时利用 `s` 的长度数据范围,使用数组来替代常数较大的哈希表。

代码:
```Java
class Solution {
    static int[] hash = new int[10010];
    public int lengthLongestPath(String s) {
        Arrays.fill(hash, -1);
        int n = s.length(), ans = 0;
        for (int i = 0; i < n; ) {
            int level = 0;
            while (i < n && s.charAt(i) == '\t' && ++level >= 0) i++;
            int j = i;
            boolean isDir = true;
            while (j < n && s.charAt(j) != '\n') {
                if (s.charAt(j++) == '.') isDir = false;
            }
            Integer cur = j - i;
            Integer prev = level - 1 >= 0 ? hash[level - 1] : -1;
            Integer path = prev + 1 + cur;
            if (isDir) hash[level] = path;
            else if (path > ans) ans = path;
            i = j + 1;
        }
        return ans;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(C)$

### 最后

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

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

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

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


Question:
列表 `arr` 由在范围 `[1, n]` 中的所有整数组成,并按严格递增排序。

请你对 `arr` 应用下述算法:

* 从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。
* 重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。
* 不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。

给你整数 `n`,返回 `arr` 最后剩下的数字。

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

输出:6

解释:
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr = [2, 4, 6, 8]
arr = [2, 6]
arr = [6]
```
示例 2:
```
输入:n = 1

输出:1
```

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

Solution:
### 约瑟夫环

与求解约瑟夫环类似,本题也可以通常找规律,分析出公式之后进行递推求解。

对于本题,定义 $f[i]$ 为在 **连续序列** $[1, i]$ 中进行「起始先从左到右,再从右往左」的轮流换向间隔删除,最终左边剩余的编号;定义 $f'[i]$ 为在 **连续序列** $[1, i]$ 中进行「起始先从右到左,再从左往右」的轮流换向间隔删除,最终右边剩余的编号。

**由于「从左往右」和「从右往左」分别为「从左端点发起,间隔删除」和「从右端点发起,间隔删除」,因此整个删除过程在连续序列中 $[1, i]$ 中具有对称性,两者最终剩余的编号在连续序列中也具有对称性。**

即可得出第一个公式:

$$
f[i] + f'[i] = i + 1
$$

考虑题目规定的「左右轮流进行发起删除」的操作如何进行。

由于我们对 $f[i]$ 和 $f'[i]$ 的定义都是「连续序列」,因此如果我们希望使用 $f[i]$ 和 $f'[i]$ 得出最终答案,我们需要在每次消除后对序列进行「重新编号」,确保能够使用 $f[i]$ 和 $f'[i]$ 作为合法状态值,在计算出「重新编号」后的,需要将答案(编号)映射回去重新编号前的值。

起始时,我们对连续序列 $[1, 2, 3, ... , i]$ 执行了一次「从左往右」的消除之后,得到的序列为 $[2, 4, 6, ..., x]$(其中 $x$ 根据 $i$ 的奇偶性不同,可能为 $i$ 或 $i - 1$)。新序列的长度为 $\left \lfloor \frac{i}{2} \right \rfloor$。

考虑对得到的序列进行重新编号,使其继续保有「连续序列」的定义,即变为 $[1, 2, 3, ... , \left \lfloor \frac{i}{2} \right \rfloor]$,然后执行「从右往左」的间隔删除,最终得到 $f'[\left \lfloor \frac{i}{2} \right \rfloor]$,之后考虑将答案编号映射回「重新编号」前的值。

此时可得到第二个公式:

$$
f[i] = f'[\left \lfloor \frac{i}{2} \right \rfloor] * 2
$$

通过上述两个公式,我们可以将 $f'[i]$ 进行消除,得到最终的 $f[i]$ 关系式:

$$
f[i] = 2 * (\left \lfloor \frac{i}{2} \right \rfloor + 1 - f[\left \lfloor \frac{i}{2} \right \rfloor])
$$

我们知道需要实现的函数 `lastRemaining` 其实就是 $f[i]$,因此该递推过程我们可以使用递归进行实现(注意的出口条件 $f[1] = 1$)。

代码:
```Java
class Solution {
    public int lastRemaining(int n) {
        return n == 1 ? 1 : 2 * (n / 2 + 1 - lastRemaining(n / 2));
    }
}
```
* 时间复杂度:$O(\log{n})$
* 空间复杂度:忽略递归带来的额外空间开销,复杂度为 $O(1)$

### 最后

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

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

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

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


Question:
给你一个数组 `rectangles`,其中 $rectangles[i] = [x_i, y_i, a_i, b_i]$ 表示一个坐标轴平行的矩形。这个矩形的左下顶点是 $(x_i, y_i)$ ,右上顶点是 $(a_i, b_i)$ 。

如果所有矩形一起精确覆盖了某个矩形区域,则返回 `true` ;否则,返回 `false` 。

示例 1:

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

输出:true

解释:5 个矩形一起可以精确地覆盖一个矩形区域。
```
示例 2:

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

输出:false

解释:两个矩形之间有间隔,无法覆盖成一个矩形。
```
示例 3:

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

输出:false

解释:图形顶端留有空缺,无法覆盖成一个矩形。
```
示例 4:

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

输出:false

解释:因为中间有相交区域,虽然形成了矩形,但不是精确覆盖。
```

提示:
* $1 <= rectangles.length <= 2 \times 10^4$
* $rectangles[i].length = 4$
* $-10^5 <= x_i, y_i, a_i, b_i <= 10^5$

Solution:
### 扫描线

将每个矩形 $rectangles[i]$ 看做两条竖直方向的边,使用 $(x, y1, y2)$ 的形式进行存储(其中 $y1$ 代表该竖边的下端点,$y2$ 代表竖边的上端点),同时为了区分是矩形的左边还是右边,再引入一个标识位,即以四元组 $(x, y1, y2, flag)$ 的形式进行存储。

一个完美矩形的充要条件为:**对于完美矩形的每一条非边缘的竖边,都「成对」出现(存在两条完全相同的左边和右边重叠在一起);对于完美矩形的两条边缘竖边,均独立为一条连续的(不重叠)的竖边。**

如图(红色框的为「完美矩形的边缘竖边」,绿框的为「完美矩形的非边缘竖边」):

* 绿色:非边缘竖边必然有成对的左右两条完全相同的竖边重叠在一起;
* 红色:边缘竖边由于只有单边,必然不重叠,且连接成一条完成的竖边。

代码:
```Java
class Solution {
    public boolean isRectangleCover(int[][] rectangles) {
        int n = rectangles.length;
        int[][] rs = new int[n * 2][4];
        for (int i = 0, idx = 0; i < n; i++) {
            int[] re = rectangles[i];
            rs[idx++] = new int[]{re[0], re[1], re[3], 1};
            rs[idx++] = new int[]{re[2], re[1], re[3], -1};
        }
        Arrays.sort(rs, (a,b)->{
            if (a[0] != b[0]) return a[0] - b[0];
            return a[1] - b[1];
        });
        n *= 2;
        // 分别存储相同的横坐标下「左边的线段」和「右边的线段」 (y1, y2)
        List<int[]> l1 = new ArrayList<>(), l2 = new ArrayList<>();
        for (int l = 0; l < n; ) {
            int r = l;
            l1.clear(); l2.clear();
            // 找到横坐标相同部分
            while (r < n && rs[r][0] == rs[l][0]) r++;
            for (int i = l; i < r; i++) {
                int[] cur = new int[]{rs[i][1], rs[i][2]};
                List<int[]> list = rs[i][3] == 1 ? l1 : l2;
                if (list.isEmpty()) {
                    list.add(cur);
                } else {
                    int[] prev = list.get(list.size() - 1);
                    if (cur[0] < prev[1]) return false; // 存在重叠
                    else if (cur[0] == prev[1]) prev[1] = cur[1]; // 首尾相连
                    else list.add(cur);
                }
            }
            if (l > 0 && r < n) {
                // 若不是完美矩形的边缘竖边,检查是否成对出现
                if (l1.size() != l2.size()) return false;
                for (int i = 0; i < l1.size(); i++) {
                    if (l1.get(i)[0] == l2.get(i)[0] && l1.get(i)[1] == l2.get(i)[1]) continue;
                    return false;
                }
            } else {
                // 若是完美矩形的边缘竖边,检查是否形成完整一段
                if (l1.size() + l2.size() != 1) return false;
            }
            l = r;
        }
        return true;
    }
}
```
* 时间复杂度:将 `rectangles` 划分成边集的复杂度为 $O(n)$;对边集进行排序的复杂度为 $O(n\log{n})$,对排序好的边集进行遍历检查,每条边会被扫描线性次,复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$
* 空间复杂度:$O(n)$

### 最后

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

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

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

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


Question:
给定一个表示数据的整数数组 `data` ,返回它是否为有效的 $UTF-8$ 编码。

`UTF-8` 中的一个字符可能的长度为 $1$ 到 $4$ 字节,遵循以下的规则:

1. 对于 $1$ 字节 的字符,字节的第一位设为 $0$ ,后面 $7$ 位为这个符号的 `unicode` 码。
2. 对于 $n$ 字节 的字符 ($n > 1$),第一个字节的前 $n$ 位都设为 $1$,第 $n+1$ 位设为 $0$ ,后面字节的前两位一律设为 $10$ 。剩下的没有提及的二进制位,全部为这个符号的 `unicode` 码。

这是 UTF-8 编码的工作方式:
```
   Char. number range  |        UTF-8 octet sequence
      (hexadecimal)    |              (binary)
   --------------------+---------------------------------------------
   0000 0000-0000 007F | 0xxxxxxx
   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
```
注意:输入是整数数组。只有每个整数的 最低 8 个有效位 用来存储数据。这意味着每个整数只表示 1 字节的数据。

示例 1:
```
输入:data = [197,130,1]

输出:true

解释:数据表示字节序列:11000101 10000010 00000001。
这是有效的 utf-8 编码,为一个 2 字节字符,跟着一个 1 字节字符。
```
示例 2:
```
输入:data = [235,140,4]

输出:false

解释:数据表示 8 位的序列: 11101011 10001100 00000100.
前 3 位都是 1 ,第 4 位为 0 表示它是一个 3 字节字符。
下一个字节是开头为 10 的延续字节,这是正确的。
但第二个延续字节不以 10 开头,所以是不符合规则的。
```

提示:
* $1 <= data.length <= 2 \times 10^4$
* $0 <= data[i] <= 255$

Solution:
### 模拟

根据题意进行模拟即可。

从前往后处理每个 $data[i]$,先统计 $data[i]$ 从第 $7$ 位开始往后有多少位连续的 $1$,代表这是一个几字节的字符,记为 $cnt$ :

* 如果 $cnt$ 为 $1$ 或者大于 $4$ 均违反编码规则(与字符长度为 $1$ 时的编码规则 和 字符长度只能是 $1$ 到 $4$ 冲突),返回 `False`;
* 如果位置 $i$ 后面不足 $cnt - 1$ 也返回 `False`;
* 否则检查下标范围为 $[i + 1, i + cnt - 1]$ 的数是否满足前两位为 $10$ 的要求,若不满足返回 `False`。

如果上述过程满足要求,跳到下一个检查点进行检查,整个 `data` 都没有冲突则返回 `True`。

代码:
```Java
class Solution {
    public boolean validUtf8(int[] data) {
        int n = data.length;
        for (int i = 0; i < n; ) {
            int t = data[i], j = 7;
            while (j >= 0 && (((t >> j) & 1) == 1)) j--;
            int cnt = 7 - j;
            if (cnt == 1 || cnt > 4) return false;
            if (i + cnt - 1 >= n) return false;
            for (int k = i + 1; k < i + cnt; k++) {
                if ((((data[k] >> 7) & 1) == 1) && (((data[k] >> 6) & 1) == 0)) continue;
                return false;
            }
            i += cnt == 0 ? 1 : cnt;
        }
        return true;
    }
}
```
* 时间复杂度:$O(n)$
* 空间复杂度:$O(1)$

### 最后

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

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

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

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


LeetCode/391-400/395. 至少有 K 个重复字符的最长子串（中等）.md

395. 至少有 K 个重复字符的最长子串

Question:
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。

示例 1:
```
输入:s = "aaabb", k = 3
输出:3
解释:最长子串为 "aaa" ,其中 'a' 重复了 3 次。
```
示例 2:
```
输入:s = "ababbc", k = 2
输出:5
解释:最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。
```

提示:
* 1 <= s.length <= $10^4$
* s 仅由小写英文字母组成
* 1 <= k <= $10^5$

Solution:
### 枚举 + 双指针

其实看到这道题,我第一反应是「二分」,直接「二分」答案。

但是往下分析就能发现「二分」不可行,因为不具有二段性质。

也就是假设有长度 `t` 的一段区间满足要求的话,`t + 1` 长度的区间是否「一定满足」或者「一定不满足」呢?

显然并不一定,是否满足取决于 `t + 1` 个位置出现的字符在不在原有区间内。

举个🌰吧,假设我们已经画出来一段长度为 `t` 的区间满足要求(且此时 k > 1),那么当我们将长度扩成 `t + 1` 的时候(无论是往左扩还是往右扩):

* 如果新位置的字符在原有区间**出现过**,那必然还是满足出现次数大于 k,这时候 **`t + 1` 的长度满足要求**
* 如果新位置的字符在原有区间**没出现过**,那新字符的出现次数只有一次,这时候 **`t + 1` 的长度不满足要求**

因此我们无法是使用「二分」,相应的也无法直接使用「滑动窗口」思路的双指针。

因为双指针其实也是利用了二段性质,当一个指针确定在某个位置,另外一个指针能够落在某个明确的分割点,使得左半部分满足,右半部分不满足。

**那么还有什么性质可以利用呢?这时候要留意数据范围「数值小」的内容。**

题目说明了只包含小写字母(26 个,为有限数据),**我们可以枚举最大长度所包含的字符类型数量,答案必然是 [1, 26],即最少包含 1 个字母,最多包含 26 个字母。**

你会发现,**当确定了长度所包含的字符种类数量时,区间重新具有了二段性质。**

当我们使用双指针的时候:
1. 右端点往右移动必然会导致字符类型数量增加(或不变)
2. 左端点往右移动必然会导致字符类型数量减少(或不变)

当然,我们还需要记录有多少字符符合要求(出现次数不少于 k),当区间内所有字符都符合时更新答案。

代码:
```Java
class Solution {
    public int longestSubstring(String s, int k) {
        int ans = 0;
        int n = s.length();
        char[] cs = s.toCharArray();
        int[] cnt = new int[26];
        for (int p = 1; p <= 26; p++) {
            Arrays.fill(cnt, 0);
            // tot 代表 [j, i] 区间所有的字符种类数量;sum 代表满足「出现次数不少于 k」的字符种类数量
            for (int i = 0, j = 0, tot = 0, sum = 0; i < n; i++) {
                int u = cs[i] - 'a';
                cnt[u]++;
                // 如果添加到 cnt 之后为 1,说明字符总数 +1
                if (cnt[u] == 1) tot++;
                // 如果添加到 cnt 之后等于 k,说明该字符从不达标变为达标,达标数量 + 1
                if (cnt[u] == k) sum++;
                // 当区间所包含的字符种类数量 tot 超过了当前限定的数量 p,那么我们要删除掉一些字母,即「左指针」右移
                while (tot > p) {
                    int t = cs[j++] - 'a';
                    cnt[t]--;
                    // 如果添加到 cnt 之后为 0,说明字符总数-1
                    if (cnt[t] == 0) tot--;
                    // 如果添加到 cnt 之后等于 k - 1,说明该字符从达标变为不达标,达标数量 - 1
                    if (cnt[t] == k - 1) sum--;
                }
                // 当所有字符都符合要求,更新答案
                if (tot == sum) ans = Math.max(ans, i - j + 1);
            }
        }
        return ans;
    }
}
```
* 时间复杂度:枚举 26 种可能性,每种可能性会扫描一遍数组。复杂度为 $O(n)$
* 空间复杂度:$O(n)$

***

### 总结 & 补充

【总结】:

「当确定了窗口内所包含的字符数量时,区间重新具有了二段性质」。这是本题的滑动窗口解法和迄今为止做的滑动窗口题目的最大不同,本题需要手动增加限制,即限制窗口内字符种类。

【补充】这里解释一下「为什么需要先枚举 26 种可能性」:

首先我们知道「**答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中,否则就不会是最优解**」。

但**如果我们只从该性质出发的话,朴素解法应该是使用一个滑动窗口,不断的调整滑动窗口的左右边界,使其满足「左边界左侧的字符以及右边界右侧的字符一定不会出现在窗口中」**,这实际上就是双指针解法,但是如果不先敲定(枚举)出答案所包含的字符数量的话,**这里的双指针是不具有单调性的**。

换句话说,只利用这一性质是没法完成逻辑的。

这时候我们面临的问题是:**性质是正确的,但是还无法直接利用**。

因此我们需要**先利用字符数量有限性(可枚举)作为切入点,使得「答案子串的左边界左侧的字符以及右边界右侧的字符一定不会出现在子串中」这一性质在双指针的实现下具有单调性**。也就是题解说的「让区间重新具有二段性质」。

然后遍历 26 种可能性(答案所包含的字符种类数量),**对每种可能性应用滑动窗口(由上述性质确保正确),可以得到每种可能性的最大值(局部最优),由所有可能性的最大值可以得出答案(全局最优)**。

***

### 点评

这道题的突破口分析其实和 [1178. 猜字谜](https://leetcode-cn.com/problems/number-of-valid-words-for-each-puzzle/solution/xiang-jin-zhu-shi-xiang-jie-po-su-wei-yu-3cr2/) 类似。

解决思路:当我们采用常规的分析思路发现无法进行时,**要去关注一下数据范围中「数值小」的值。因为数值小其实是代表了「可枚举」,往往是解题或者降低复杂度的一个重要(甚至是唯一)的突破口。**

### 最后

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

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

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

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