From ff0bb30a31d6670cc0f532446f759797c2cd04bd Mon Sep 17 00:00:00 2001 From: AC_Oier Date: Tue, 7 Feb 2023 16:57:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8feat:=20add=201129=E3=80=811669?= =?UTF-8?q?=E3=80=811798=E3=80=811976=E3=80=81393=E3=80=81396?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...10\344\270\255\347\255\211\357\274\211.md" | 185 ++++++++++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 102 ++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 116 +++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 6 +- ...10\344\270\255\347\255\211\357\274\211.md" | 5 +- ...10\344\270\255\347\255\211\357\274\211.md" | 14 +- 6 files changed, 415 insertions(+), 13 deletions(-) create mode 100644 "LeetCode/1221-1230/1129. \351\242\234\350\211\262\344\272\244\346\233\277\347\232\204\346\234\200\347\237\255\350\267\257\345\276\204\357\274\210\344\270\255\347\255\211\357\274\211.md" create mode 100644 "LeetCode/1661-1670/1669. \345\220\210\345\271\266\344\270\244\344\270\252\351\223\276\350\241\250\357\274\210\344\270\255\347\255\211\357\274\211.md" create mode 100644 "LeetCode/1791-1800/1798. \344\275\240\350\203\275\346\236\204\351\200\240\345\207\272\350\277\236\347\273\255\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\347\233\256\357\274\210\344\270\255\347\255\211\357\274\211.md" diff --git "a/LeetCode/1221-1230/1129. \351\242\234\350\211\262\344\272\244\346\233\277\347\232\204\346\234\200\347\237\255\350\267\257\345\276\204\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1221-1230/1129. \351\242\234\350\211\262\344\272\244\346\233\277\347\232\204\346\234\200\347\237\255\350\267\257\345\276\204\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..b1cd37ba --- /dev/null +++ "b/LeetCode/1221-1230/1129. \351\242\234\350\211\262\344\272\244\346\233\277\347\232\204\346\234\200\347\237\255\350\267\257\345\276\204\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,185 @@ +### 题目描述 + +这是 LeetCode 上的 **[1129. 颜色交替的最短路径]()** ,难度为 **中等**。 + +Tag : 「BFS」、「最短路」 + + + +在一个有向图中,节点分别标记为 `0, 1, ..., n-1`。图中每条边为红色或者蓝色,且存在自环或平行边。 + +`red_edges` 中的每一个 `[i, j]` 对表示从节点 `i` 到节点 `j` 的红色有向边。类似地,`blue_edges` 中的每一个 `[i, j]` 对表示从节点 `i` 到节点 `j` 的蓝色有向边。 + +返回长度为 `n` 的数组 `answer`,其中 `answer[X]` 是从节点 `0` 到节点 `X` 的红色边和蓝色边交替出现的最短路径的长度。如果不存在这样的路径,那么 `answer[x] = -1`。 + +示例 1: +``` +输入:n = 3, red_edges = [[0,1],[1,2]], blue_edges = [] + +输出:[0,1,-1] +``` +示例 2: +``` +输入:n = 3, red_edges = [[0,1]], blue_edges = [[2,1]] + +输出:[0,1,-1] +``` +示例 3: +``` +输入:n = 3, red_edges = [[1,0]], blue_edges = [[2,1]] + +输出:[0,-1,-1] +``` +示例 4: +``` +输入:n = 3, red_edges = [[0,1]], blue_edges = [[1,2]] + +输出:[0,1,2] +``` +示例 5: +``` +输入:n = 3, red_edges = [[0,1],[0,2]], blue_edges = [[1,0]] + +输出:[0,1,1] +``` + +提示: +* `1 <= n <= 100` +* `red_edges.length <= 400` +* `blue_edges.length <= 400` +* `red_edges[i].length = blue_edges[i].length = 2` +* `0 <= red_edges[i][j], blue_edges[i][j] < n` + +--- + +### 朴素 BFS + +为了方便,将 `redEdges` 记为 `rs`,将 `blueEdges` 记为 `bs`。 + +使用两数组进行建图,利用点数和边数关系(稀疏图),使用「邻接表」方式进行建图。 + +将所有红色有向边权值记为 `1`,将所有蓝色有向边权值记为 `-1`。注意这里的权重仅表示该边的颜色,并非代表经过该边的真实成本。 + +也正是经过所有边的成本均相同,对于原问题「从 `0` 号节点进行出发,求到所有点的颜色交替的最短路径」,我们容易想到使用 `BFS` 进行求解。 + +`BFS` 过程中将三元组 $(point, last, step)$ 进行入队: + +* `point` 代表当前所在点编号 +* `last` 代表到达 `point` 所使用的边的颜色,默认为 `0` 代表不需要经过任何边到达起点 +* `step` 代表到达 `point` 所使用的步数 + +利用数据范围不大(点数为 $100$,边数为 $800$),可以直接对每个点进行一次独立的 `BFS` 来求最短路。由于图存在重边和自环,因此在求解最短路的过程需要对边进行去重。 + +代码: +```Java +class Solution { + static int N = 110, M = 810, idx = 0; + static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M]; + void add(int a, int b, int c) { + e[idx] = b; + w[idx] = c; + ne[idx] = he[a]; + he[a] = idx++; + } + public int[] shortestAlternatingPaths(int n, int[][] rs, int[][] bs) { + idx = 0; + Arrays.fill(he, -1); + for (int[] e : rs) add(e[0], e[1], 1); + for (int[] e : bs) add(e[0], e[1], -1); + int[] ans = new int[n]; + boolean[] vis = new boolean[rs.length + bs.length]; + out:for (int k = 1; k < n; k++) { + ans[k] = -1; + Arrays.fill(vis, false); + Deque d = new ArrayDeque<>(); + d.addLast(new int[]{0, 0, 0}); // point, last, step + while (!d.isEmpty()) { + int[] info = d.pollFirst(); + int p = info[0], last = info[1], step = info[2]; + for (int i = he[p]; i != -1; i = ne[i]) { + int j = e[i], c = w[i]; + if (vis[i]) continue; + if (c + last == 0 || last == 0) { + if (j == k) { + ans[k] = step + 1; + continue out; + } else { + d.addLast(new int[]{j, c, step + 1}); + vis[i] = true; + } + } + } + } + } + return ans; + } +} +``` +* 时间复杂度:将点数记为 `n` ,边数记为 `m`。建图复杂度为 $O(m)$;构造答案复杂度为 $O(n \times (n + m))$。整体复杂度为 $O(n \times (n + m))$ +* 空间复杂度:$O(n + m)$ + +--- + +### 优化 + +实际上,我们并没有对每个点进行独立 `BFS` 的必要。 + +为了获取所有从节点 `0` 出发的最短路,可直接从节点 `0` 进行出发(对边进行去重),所有能够从节点 `0` 沿交替路径到达的节点必然都会被访问到,且节点首次被访问到时必然是最短路径。 + +因此我们只需要初始化所有的 `ans[i] = -1`,并从节点 `0` 开始进行一次 `BFS` 即可。 + +代码: +```Java +class Solution { + static int N = 110, M = 810, idx = 0; + static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M]; + void add(int a, int b, int c) { + e[idx] = b; + w[idx] = c; + ne[idx] = he[a]; + he[a] = idx++; + } + public int[] shortestAlternatingPaths(int n, int[][] rs, int[][] bs) { + idx = 0; + Arrays.fill(he, -1); + for (int[] e : rs) add(e[0], e[1], 1); + for (int[] e : bs) add(e[0], e[1], -1); + int[] ans = new int[n]; + Arrays.fill(ans, -1); + ans[0] = 0; + boolean[] vis = new boolean[rs.length + bs.length]; + Arrays.fill(vis, false); + Deque d = new ArrayDeque<>(); + d.addLast(new int[]{0, 0, 0}); // point, last, step + while (!d.isEmpty()) { + int[] info = d.pollFirst(); + int p = info[0], last = info[1], step = info[2]; + for (int i = he[p]; i != -1; i = ne[i]) { + if (vis[i]) continue; + int j = e[i], c = w[i]; + if (c + last == 0 || last == 0) { + ans[j] = ans[j] == -1 ? step + 1 : Math.min(ans[j], step + 1); + d.addLast(new int[]{j, c, step + 1}); + vis[i] = true; + } + } + } + return ans; + } +} +``` +* 时间复杂度:将点数记为 `n` ,边数记为 `m`。建图复杂度为 $O(m)$;构造答案复杂度为 $O(n + m)$。整体复杂度为 $O(n + m)$ +* 空间复杂度:$O(n + m)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1224` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/1661-1670/1669. \345\220\210\345\271\266\344\270\244\344\270\252\351\223\276\350\241\250\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1661-1670/1669. \345\220\210\345\271\266\344\270\244\344\270\252\351\223\276\350\241\250\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..bd52fd1b --- /dev/null +++ "b/LeetCode/1661-1670/1669. \345\220\210\345\271\266\344\270\244\344\270\252\351\223\276\350\241\250\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,102 @@ +### 题目描述 + +这是 LeetCode 上的 **[1669. 合并两个链表](/)** ,难度为 **中等**。 + +Tag : 「链表」、「模拟」 + + + + +给你两个链表 `list1` 和 `list2`,它们包含的元素分别为 `n` 个和 `m` 个。 + +请你将 `list1` 中下标从 `a` 到 `b` 的全部节点都删除,并将 `list2` 接在被删除节点的位置。 + +下图中蓝色边和节点展示了操作后的结果: + +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/28/fig1.png) + +请你返回结果链表的头指针。 + + + +示例 1: +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/28/merge_linked_list_ex1.png) + +``` +输入:list1 = [0,1,2,3,4,5], a = 3, b = 4, list2 = [1000000,1000001,1000002] + +输出:[0,1,2,1000000,1000001,1000002,5] + +解释:我们删除 list1 中下标为 3 和 4 的两个节点,并将 list2 接在该位置。上图中蓝色的边和节点为答案链表。 +``` +示例 2: +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2020/11/28/merge_linked_list_ex2.png) +``` +输入:list1 = [0,1,2,3,4,5,6], a = 2, b = 5, list2 = [1000000,1000001,1000002,1000003,1000004] + +输出:[0,1,1000000,1000001,1000002,1000003,1000004,6] + +解释:上图中蓝色的边和节点为答案链表。 +``` + +提示: +* $3 <= list1.length <= 10^4$ +* $1 <= a <= b < list1.length - 1$ +* $1 <= list2.length <= 10^4$ + +--- + +### 模拟 + +根据题意进行模拟即可。 + +使用两个变量 `A` 和 `B` 分别指向 `list1` 中两个断联的位置,分别将 `A` 指向 `list2` 的开头,将 `list2` 的 `next` 指针指向 `B`。 + + +Java 代码: +```Java +class Solution { + public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) { + ListNode ans = list1; + ListNode A = null, B = null; + while (--a > 0 && --b > 0) list1 = list1.next; + A = list1; + while (b-- > 0) list1 = list1.next; + B = list1; + A.next = list2; + while (list2.next != null) list2 = list2.next; + list2.next = B.next; + return ans; + } +} +``` +TypeScript 代码: +```TypeScript +function mergeInBetween(list1: ListNode | null, a: number, b: number, list2: ListNode | null): ListNode | null { + const ans = list1 + let A = null, B = null + while (--a > 0 && --b > 0) list1 = list1.next + A = list1 + while (b-- > 0) list1 = list1.next + B = list1 + A.next = list2 + while (list2.next != null) list2 = list2.next + list2.next = B.next + return ans +} +``` +* 时间复杂度:$O(n + m)$ +* 空间复杂度:$O(1)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1699` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/1791-1800/1798. \344\275\240\350\203\275\346\236\204\351\200\240\345\207\272\350\277\236\347\273\255\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\347\233\256\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1791-1800/1798. \344\275\240\350\203\275\346\236\204\351\200\240\345\207\272\350\277\236\347\273\255\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\347\233\256\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..4e6839e2 --- /dev/null +++ "b/LeetCode/1791-1800/1798. \344\275\240\350\203\275\346\236\204\351\200\240\345\207\272\350\277\236\347\273\255\345\200\274\347\232\204\346\234\200\345\244\247\346\225\260\347\233\256\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,116 @@ +### 题目描述 + +这是 LeetCode 上的 **[1798. 你能构造出连续值的最大数目]()** ,难度为 **中等**。 + +Tag : 「数学」、「脑筋急转弯」、「排序」、「构造」 + + + +给你一个长度为 `n` 的整数数组 `coins`,它代表你拥有的 `n` 个硬币。第 `i` 个硬币的值为 `coins[i]`。如果你从这些硬币中选出一部分硬币,它们的和为 `x` ,那么称,你可以构造出 `x` 。 + +请返回从 `0` 开始(包括 `0` ),你最多能构造出多少个连续整数。 + +你可能有多个相同值的硬币。 + +示例 1: +``` +输入:coins = [1,3] + +输出:2 + +解释:你可以得到以下这些值: +- 0:什么都不取 [] +- 1:取 [1] +从 0 开始,你可以构造出 2 个连续整数。 +``` +示例 2: +``` +输入:coins = [1,1,1,4] + +输出:8 + +解释:你可以得到以下这些值: +- 0:什么都不取 [] +- 1:取 [1] +- 2:取 [1,1] +- 3:取 [1,1,1] +- 4:取 [4] +- 5:取 [4,1] +- 6:取 [4,1,1] +- 7:取 [4,1,1,1] +从 0 开始,你可以构造出 8 个连续整数。 +``` +示例 3: +``` +输入:nums = [1,4,10,3,1] + +输出:20 +``` + +提示: +* $coins.length = n$ +* $1 <= n <= 4 \times 10^4$ +* $1 <= coins[i] <= 4 \times 10^4$ + +--- + +### 数学 + +`n` 的数据范围为 $4 \times 10^4$,必然不是考察我们使用 `coins` 来构造单个数值 `x` 的逻辑,因为「遍历 + 逐个构造验证」的做法会超时,**因此只能是考察我们能否推导出整段构造的相关性质**。 + +假设我们已经用前 `k` 个数值构造出连段 $[0, x]$ 中的任意数,当增加第 $k + 1$ 个数值时,还能否进行连续构造: + +* 若不能,则连续构造中断,答案为 $[0, x]$,共 $x + 1$ 个 +* 若能,则再考虑连续构造的右边界会到哪个地方 + +由于题目允许我们任意使用 `coins` 中的数,同时整段构造又是不断扩大 $[0, x]$ 中右边界的过程(从小到大),为了方便,我们可以先对 `coins` 进行排序。 + +不失一般性,假设我们已经使用 `coins` 中的前 `k` 个数构造出了范围 $[0, x]$ 中的任意数。当考虑增加一个 $coins[k]$ 后,我们可在每一个原有构造方案中增加 $coins[k]$,这样由 $coins[k]$ 所拓展出的构造范围便是 $[coins[k], coins[k] + x]$。 + +原来的连续数是 $[0, x]$,若要保证连续,我们需要保证 $coins[k] <= x + 1$,此时构造连续段也从 $[0, x]$ 变为 $[0, coins[k] + x]$。 + +即 $coins[k] > k + 1$ 是中断构造必要条件,再结合我们实现对 `coins` 进行了排序,容易证明如果 $coins[k]$ 都不能满足 $coins[k] <= x + 1$,排在 $coins[k]$ 后面比其大的数均不能满足要求。 + +一些细节:起始时,我们可以不选 `coins` 中的任何数,即此时连续构造范围为 $[0, 0]$,随后从小到大遍历 `coins`,检查当前 $coins[i]$ 是否会中断构造。 + +Java 代码: +```Java +class Solution { + public int getMaximumConsecutive(int[] coins) { + Arrays.sort(coins); + int ans = 0; + for (int c : coins) { + if (c > ans + 1) break; + ans += c; + } + return ans + 1; + } +} +``` +TypeScript 代码: +```TypeScript +function getMaximumConsecutive(coins: number[]): number { + coins.sort((a,b)=>a-b) + let ans = 0 + for (const c of coins) { + if (c > ans + 1) break + ans += c + } + return ans + 1 +} +``` +* 时间复杂度:$O(n\log{n})$ +* 空间复杂度:$O(\log{n})$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1798` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/1971-1980/1976. \345\210\260\350\276\276\347\233\256\347\232\204\345\234\260\347\232\204\346\226\271\346\241\210\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1971-1980/1976. \345\210\260\350\276\276\347\233\256\347\232\204\345\234\260\347\232\204\346\226\271\346\241\210\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" index 416ac2aa..55f453bc 100644 --- "a/LeetCode/1971-1980/1976. \345\210\260\350\276\276\347\233\256\347\232\204\345\234\260\347\232\204\346\226\271\346\241\210\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/1971-1980/1976. \345\210\260\350\276\276\347\233\256\347\232\204\345\234\260\347\232\204\346\226\271\346\241\210\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -10,7 +10,7 @@ Tag : 「最短路」、「拓扑排序」、「动态规划」 给你一个整数 `n` 和二维整数数组 `roads`,其中 $roads[i] = [u_i, v_i, time_i]$ 表示在路口 $u_i$ 和 $v_i$ 之间有一条需要花费 $time_i$ 时间才能通过的道路。你想知道花费 最少时间 从路口 $0$ 出发到达路口 $n - 1$ 的方案数。 -请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 109 + 7 取余 后返回。 +请返回花费 最少时间 到达目的地的 路径数目 。由于答案可能很大,将结果对 $10^9 + 7$ 取余 后返回。 示例 1: ![](https://assets.leetcode.com/uploads/2021/07/17/graph2.png) @@ -37,10 +37,10 @@ Tag : 「最短路」、「拓扑排序」、「动态规划」 提示: * $1 <= n <= 200$ -* $n - 1 <= roads.length <= n * (n - 1) / 2$ +* $n - 1 <= roads.length <= \frac{n \times (n - 1)}{2}$ * $roads[i].length == 3$ * $0 <= u_i, v_i <= n - 1$ -* $1 <= time_i <= 109$ +* $1 <= time_i <= 10^9$ * $u_i != v_i$ * 任意两个路口之间至多有一条路。 * 从任意路口出发,你能够到达其他任意路口。 diff --git "a/LeetCode/391-400/393. UTF-8 \347\274\226\347\240\201\351\252\214\350\257\201\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/391-400/393. UTF-8 \347\274\226\347\240\201\351\252\214\350\257\201\357\274\210\344\270\255\347\255\211\357\274\211.md" index 9d271c1f..8825007f 100644 --- "a/LeetCode/391-400/393. UTF-8 \347\274\226\347\240\201\351\252\214\350\257\201\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/391-400/393. UTF-8 \347\274\226\347\240\201\351\252\214\350\257\201\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -49,7 +49,7 @@ Tag : 「模拟」 ``` 提示: -* $1 <= data.length <= 2 * 10^4$ +* $1 <= data.length <= 2 \times 10^4$ * $0 <= data[i] <= 255$ --- @@ -81,8 +81,7 @@ class Solution { if ((((data[k] >> 7) & 1) == 1) && (((data[k] >> 6) & 1) == 0)) continue; return false; } - if (cnt == 0) i++; - else i += cnt; + i += cnt == 0 ? 1 : cnt; } return true; } diff --git "a/LeetCode/391-400/396. \346\227\213\350\275\254\345\207\275\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/391-400/396. \346\227\213\350\275\254\345\207\275\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" index 7f4d2bc2..86d8836e 100644 --- "a/LeetCode/391-400/396. \346\227\213\350\275\254\345\207\275\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/391-400/396. \346\227\213\350\275\254\345\207\275\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -12,7 +12,7 @@ Tag : 「前缀和」、「滑动窗口」 * `F(k) = 0 * arrk[0] + 1 * arrk[1] + ... + (n - 1) * arrk[n - 1]` -返回 `F(0), F(1), ..., F(n-1)`中的最大值 。 +返回 `F(0), F(1), ..., F(n-1)` 中的最大值 。 生成的测试用例让答案符合 $32$ 位 整数。 @@ -37,7 +37,7 @@ F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 ``` 提示: -* $n == nums.length$ +* $n = nums.length$ * $1 <= n <= 10^5$ * $-100 <= nums[i] <= 100$ @@ -47,28 +47,28 @@ F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26 为了方便,我们将 $nums$ 的长度记为 $n$。 -题目要对「旋转数组」做逻辑,容易想到将 $nums$ 进行复制拼接,得到长度为 $2 * n$ 的新数组,在新数组上任意一个长度为 $n$ 的滑动窗口都对应了一个旋转数组。 +题目要对「旋转数组」做逻辑,容易想到将 $nums$ 进行复制拼接,得到长度为 $2 \times n$ 的新数组,在新数组上任意一个长度为 $n$ 的滑动窗口都对应了一个旋转数组。 然后考虑在窗口的滑动过程中,计算结果会如何变化,假设当前我们处理到下标为 $[i, i + n - 1]$ 的滑动窗口,根据题意,当前结果为: $$ -cur = nums[i] * 0 + nums[i + 1] * 1 + ... + nums[i + n - 1] * (n - 1) +cur = nums[i] \times 0 + nums[i + 1] \times 1 + ... + nums[i + n - 1] \times (n - 1) $$ 当窗口往后移动一位,也就是窗口的右端点来到 $i + n$ 的位置,左端点来到 $i + 1$ 的位置。 -我们需要增加「新右端点」的值,即增加 $nums[i + n] * (n - 1)$,同时减去「旧左端点」的值,即减少 $nums[i] * 0$(固定为 $0$),然后更新新旧窗口的公共部分 $[i + 1, i + n - 1]$。 +我们需要增加「新右端点」的值,即增加 $nums[i + n] \times (n - 1)$,同时减去「旧左端点」的值,即减少 $nums[i] \times 0$(固定为 $0$),然后更新新旧窗口的公共部分 $[i + 1, i + n - 1]$。 不难发现,随着窗口的逐步右移,每一位公共部分的权值系数都会进行减一。 $$ -nums[i + 1] * 1 + nums[i + 2] * 2 + ... + nums[i + n - 1] * (n - 1) +nums[i + 1] \times 1 + nums[i + 2] \times 2 + ... + nums[i + n - 1] \times (n - 1) $$ 变为 $$ -nums[i + 1] * 0 + nums[i + 2] * 1 + ... + nums[i + n - 1] * (n - 2) +nums[i + 1] \times 0 + nums[i + 2] \times 1 + ... + nums[i + n - 1] \times (n - 2) $$ 因此,公共部分的差值为 $\sum_{idx = i + 1}^{i + n - 1}nums[idx]$,这引导我们可以使用前缀和进行优化。 From cc36e273e4faf903599af00b1d9eaf7f040b5edd Mon Sep 17 00:00:00 2001 From: AC_Oier Date: Wed, 10 May 2023 08:57:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8feat:=20add=201210=E3=80=811786?= =?UTF-8?q?=E3=80=811797=E3=80=812335=E3=80=81456=E3=80=81629=E3=80=81648?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...10\345\233\260\351\232\276\357\274\211.md" | 122 +++++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 115 +++++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 36 ++-- ...10\344\270\255\347\255\211\357\274\211.md" | 160 ++++++++++++++++++ ...10\347\256\200\345\215\225\357\274\211.md" | 123 ++++++++++++++ ...10\344\270\255\347\255\211\357\274\211.md" | 61 +++++-- ...10\345\233\260\351\232\276\357\274\211.md" | 4 +- ...10\344\270\255\347\255\211\357\274\211.md" | 2 +- 8 files changed, 587 insertions(+), 36 deletions(-) create mode 100644 "LeetCode/1201-1210/1210. \347\251\277\350\277\207\350\277\267\345\256\253\347\232\204\346\234\200\345\260\221\347\247\273\345\212\250\346\254\241\346\225\260\357\274\210\345\233\260\351\232\276\357\274\211.md" create mode 100644 "LeetCode/1221-1230/1233. \345\210\240\351\231\244\345\255\220\346\226\207\344\273\266\345\244\271\357\274\210\344\270\255\347\255\211\357\274\211.md" create mode 100644 "LeetCode/1791-1800/1797. \350\256\276\350\256\241\344\270\200\344\270\252\351\252\214\350\257\201\347\263\273\347\273\237\357\274\210\344\270\255\347\255\211\357\274\211.md" create mode 100644 "LeetCode/2331-2340/2335. \350\243\205\346\273\241\346\235\257\345\255\220\351\234\200\350\246\201\347\232\204\346\234\200\347\237\255\346\200\273\346\227\266\351\225\277\357\274\210\347\256\200\345\215\225\357\274\211.md" diff --git "a/LeetCode/1201-1210/1210. \347\251\277\350\277\207\350\277\267\345\256\253\347\232\204\346\234\200\345\260\221\347\247\273\345\212\250\346\254\241\346\225\260\357\274\210\345\233\260\351\232\276\357\274\211.md" "b/LeetCode/1201-1210/1210. \347\251\277\350\277\207\350\277\267\345\256\253\347\232\204\346\234\200\345\260\221\347\247\273\345\212\250\346\254\241\346\225\260\357\274\210\345\233\260\351\232\276\357\274\211.md" new file mode 100644 index 00000000..1fb0a915 --- /dev/null +++ "b/LeetCode/1201-1210/1210. \347\251\277\350\277\207\350\277\267\345\256\253\347\232\204\346\234\200\345\260\221\347\247\273\345\212\250\346\254\241\346\225\260\357\274\210\345\233\260\351\232\276\357\274\211.md" @@ -0,0 +1,122 @@ +### 题目描述 + +这是 LeetCode 上的 **[1210. 穿过迷宫的最少移动次数]()** ,难度为 **困难**。 + +Tag : 「BFS」 + + + +你还记得那条风靡全球的贪吃蛇吗? + +我们在一个 `n*n` 的网格上构建了新的迷宫地图,蛇的长度为 `2`,也就是说它会占去两个单元格。蛇会从左上角(`(0, 0)` 和 `(0, 1)`)开始移动。我们用 `0` 表示空单元格,用 `1` 表示障碍物。蛇需要移动到迷宫的右下角(`(n-1, n-2)` 和 `(n-1, n-1)`)。 + +每次移动,蛇可以这样走: + +* 如果没有障碍,则向右移动一个单元格。并仍然保持身体的水平/竖直状态。 +* 如果没有障碍,则向下移动一个单元格。并仍然保持身体的水平/竖直状态。 +* 如果它处于水平状态并且其下面的两个单元都是空的,就顺时针旋转 `90` 度。蛇从(`(r, c)`、`(r, c+1)`)移动到 (`(r, c)`、`(r+1, c)`)。 + ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/09/28/image-2.png) +* 如果它处于竖直状态并且其右面的两个单元都是空的,就逆时针旋转 `90` 度。蛇从(`(r, c)`、`(r+1, c)`)移动到(`(r, c)`、`(r, c+1)`)。 + ![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/09/28/image-1.png) + +返回蛇抵达目的地所需的最少移动次数。 + +如果无法到达目的地,请返回 `-1`。 + +示例 1: +![](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/09/28/image.png) +``` +输入:grid = [[0,0,0,0,0,1], + [1,1,0,0,1,0], +  [0,0,0,0,1,1], +  [0,0,1,0,1,0], +  [0,1,1,0,0,0], +  [0,1,1,0,0,0]] + +输出:11 + +解释: +一种可能的解决方案是 [右, 右, 顺时针旋转, 右, 下, 下, 下, 下, 逆时针旋转, 右, 下]。 +``` +示例 2: +``` +输入:grid = [[0,0,1,1,1,1], +  [0,0,0,0,1,1], +  [1,1,0,0,0,1], +  [1,1,1,0,0,1], +  [1,1,1,0,0,1], +  [1,1,1,0,0,0]] + +输出:9 +``` + +提示: +* $2 <= n <= 100$ +* $0 <= grid[i][j] <= 1$ +* 蛇保证从空单元格开始出发。 + +--- + +### BFS + +题目要我们求从特定起点到特定终点的最少步数,由于我们蛇的长度固定为 $2$,因此我们可用三元组 $(x, y, cd)$ 来代表蛇的实际位置。其中 $(x, y)$ 代表蛇尾位置,$cd$ 代表当前蛇的方向状态,$0$ 代表水平状态,$1$ 代表竖直状态。 + +蛇尾加上方向状态可确定其蛇头位置 :`tx = cd == 0 ? nx : nx + 1`、`ty = cd == 0 ? ny + 1 : ny`。 + +对四种移动规则所导致三元组变化进行分情况讨论: + +1. 往右移动:对于蛇尾而言,只有维度 $y$ 进行加一,其余维度不变。三元组变化总结为 $(0, 1, 0)$ +2. 往下移动:对于蛇尾而言,只有维度 $x$ 进行加一,其余维度不变。三元组变化总结为 $(1, 0, 0)$ +3. 旋转:对于蛇尾,只有 $cd$ 维度对进行翻转,其余维度不变。三元组变化总结定为 $(0, 0, 1)$ + +综上,所有移动规则可总结为 `int[][] dirs = new int[][]{{1,0,0},{0,1,0},{0,0,1}}`。 + +在进行 `BFS` 时,通过遍历 `dirs` 来得到新的三元组:原位置 `(x, y, cd)` 转换到新位置 `(x + dir[0], y + dir[1], cd ^ dir[2])`。 + +在得到新蛇尾位置 $(nx, ny)$ 之后,计算新蛇头的位置 $(tx, ty)$。需要确保整条蛇没有越界,没有碰到障碍物,并且旋转转移时,额外检查 $(x + 1, y + 1)$ 位置是否合法。 + + +代码: +```Java +class Solution { + int[][] dirs = new int[][]{{1,0,0},{0,1,0},{0,0,1}}; + public int minimumMoves(int[][] g) { + int n = g.length; + Deque d = new ArrayDeque<>(); + d.addLast(new int[]{0,0,0,0}); + boolean[][][] vis = new boolean[n][n][2]; + vis[0][0][0] = true; + while (!d.isEmpty()) { + int[] info = d.pollFirst(); + int x = info[0], y = info[1], cd = info[2], step = info[3]; + for (int[] dir : dirs) { + int nx = x + dir[0], ny = y + dir[1], nd = cd ^ dir[2]; // 新蛇尾位置和方向 + int tx = nd == 0 ? nx : nx + 1, ty = nd == 0 ? ny + 1 : ny; // 新蛇头 + if (nx >= n || ny >= n || tx >= n || ty >= n) continue; // 整条蛇不越界 + if (g[nx][ny] == 1 || g[tx][ty] == 1) continue; // 没有触及障碍物 + if (vis[nx][ny][nd]) continue; + if (cd != nd && g[x + 1][y + 1] == 1) continue; // 旋转时,额外检查多一个位置 + if (nx == n - 1 && ny == n - 2 && nd == 0) return step + 1; + d.addLast(new int[]{nx, ny, nd, step + 1}); + vis[nx][ny][nd] = true; + } + } + return -1; + } +} +``` +* 时间复杂度:$O(n^2)$ +* 空间复杂度:$O(n^2 \times C)$,其中 $C = 2$ 代表蛇可变状态方向 + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1210` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/1221-1230/1233. \345\210\240\351\231\244\345\255\220\346\226\207\344\273\266\345\244\271\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1221-1230/1233. \345\210\240\351\231\244\345\255\220\346\226\207\344\273\266\345\244\271\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..7344e621 --- /dev/null +++ "b/LeetCode/1221-1230/1233. \345\210\240\351\231\244\345\255\220\346\226\207\344\273\266\345\244\271\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,115 @@ +### 题目描述 + +这是 LeetCode 上的 **[1233. 删除子文件夹]()** ,难度为 **中等**。 + +Tag : 「字典树」 + + + +你是一位系统管理员,手里有一份文件夹列表 `folder`,你的任务是要删除该列表中的所有 子文件夹,并以 任意顺序 返回剩下的文件夹。 + +如果文件夹 `folder[i]` 位于另一个文件夹 `folder[j]` 下,那么 `folder[i]` 就是 `folder[j]` 的 子文件夹 。 + +文件夹的「路径」是由一个或多个按以下格式串联形成的字符串:`'/'` 后跟一个或者多个小写英文字母。 + +例如,`"/leetcode"` 和 `"/leetcode/problems"` 都是有效的路径,而空字符串和 "/" 不是。 + +示例 1: +``` +输入:folder = ["/a","/a/b","/c/d","/c/d/e","/c/f"] + +输出:["/a","/c/d","/c/f"] + +解释:"/a/b" 是 "/a" 的子文件夹,而 "/c/d/e" 是 "/c/d" 的子文件夹。 +``` +示例 2: +``` +输入:folder = ["/a","/a/b/c","/a/b/d"] + +输出:["/a"] + +解释:文件夹 "/a/b/c" 和 "/a/b/d" 都会被删除,因为它们都是 "/a" 的子文件夹。 +``` +示例 3: +``` +输入: folder = ["/a/b/c","/a/b/ca","/a/b/d"] + +输出: ["/a/b/c","/a/b/ca","/a/b/d"] +``` + +提示: +* $1 <= folder.length <= 4 \times 10^4$ +* $2 <= folder[i].length <= 100$ +* `folder[i]` 只包含小写字母和 `'/'` +* `folder[i]` 总是以字符 `'/'` 起始 +* 每个文件夹名都是 唯一 的 + +--- + +### 字典树 + +一道字典树裸题,不熟悉字典树的同学可以看前置 🧀 : [【设计数据结构】实现 Trie (前缀树)](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488490&idx=1&sn=db2998cb0e5f08684ee1b6009b974089)。 + +定义类 `Trie` 代表字典树节点,对应物理含义为某个文件夹。该节点含有属性 `s`、`isEnd` 和 `stries`,分别代表「当前节点所代表文件夹名」、「是否为当前路径的最终文件夹」以及「当前文件夹下的子文件夹集合」。 + +并且由于每个文件夹名的长度不定,我们使用 `Map` 结构来构建 `stries`。 + +起始先将所有的 $folder[i]$ 加入字典树,随后查询每个 $folder[i]$ 是否为子文件夹,将所有非子文件夹加入答案。 + +代码: +```Java +class Solution { + class Trie { + String s; + boolean isEnd = false; + Map stries = new HashMap<>(); + Trie (String _s) { + s = _s; + } + } + void add(String f) { + String[] ss = f.split("/"); + Trie p = root; + for (int i = 1; i < ss.length; i++) { + String s = ss[i]; + if (!p.stries.containsKey(s)) p.stries.put(s, new Trie(s)); + p = p.stries.get(s); + } + p.isEnd = true; + } + boolean isSubFolder(String f) { + String[] ss = f.split("/"); + Trie p = root; + for (int i = 1; i < ss.length - 1; i++) { + String s = ss[i]; + if (p.stries.get(s).isEnd) return true; + p = p.stries.get(s); + } + return false; + } + Trie root = new Trie(""); + public List removeSubfolders(String[] folder) { + for (String f : folder) add(f); + List ans = new ArrayList<>(); + for (String f : folder) { + if (!isSubFolder(f)) ans.add(f); + } + return ans; + } +} +``` +* 时间复杂度:$O(\sum_{i = 0}^{n - 1} folder[i].length)$ +* 空间复杂度:$O(\sum_{i = 0}^{n - 1} folder[i].length)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1223` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/1781-1790/1786. \344\273\216\347\254\254\344\270\200\344\270\252\350\212\202\347\202\271\345\207\272\345\217\221\345\210\260\346\234\200\345\220\216\344\270\200\344\270\252\350\212\202\347\202\271\347\232\204\345\217\227\351\231\220\350\267\257\345\276\204\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1781-1790/1786. \344\273\216\347\254\254\344\270\200\344\270\252\350\212\202\347\202\271\345\207\272\345\217\221\345\210\260\346\234\200\345\220\216\344\270\200\344\270\252\350\212\202\347\202\271\347\232\204\345\217\227\351\231\220\350\267\257\345\276\204\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" index c2b60df3..b60bec69 100644 --- "a/LeetCode/1781-1790/1786. \344\273\216\347\254\254\344\270\200\344\270\252\350\212\202\347\202\271\345\207\272\345\217\221\345\210\260\346\234\200\345\220\216\344\270\200\344\270\252\350\212\202\347\202\271\347\232\204\345\217\227\351\231\220\350\267\257\345\276\204\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/1781-1790/1786. \344\273\216\347\254\254\344\270\200\344\270\252\350\212\202\347\202\271\345\207\272\345\217\221\345\210\260\346\234\200\345\220\216\344\270\200\344\270\252\350\212\202\347\202\271\347\232\204\345\217\227\351\231\220\350\267\257\345\276\204\346\225\260\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -7,13 +7,13 @@ Tag : 「最短路」、「线性 DP」 -现有一个加权无向连通图。给你一个正整数 n ,表示图中有 n 个节点,并按从 1 到 n 给节点编号;另给你一个数组 edges ,其中每个 edges[i] = [ui, vi, weighti] 表示存在一条位于节点 ui 和 vi 之间的边,这条边的权重为 weighti 。 +现有一个加权无向连通图。给你一个正整数 `n` ,表示图中有 `n` 个节点,并按从 `1` 到 `n` 给节点编号;另给你一个数组 `edges`,其中每个 $edges[i] = [u_{i}, v_{i}, weight_{i}]$ 表示存在一条位于节点 $u_{i}$ 和 $v_{i}$ 之间的边,这条边的权重为 $weight_{i}$ 。 -从节点 start 出发到节点 end 的路径是一个形如 [z0, z1, z2, ..., zk] 的节点序列,满足 z0 = start 、zk = end 且在所有符合 0 <= i <= k-1 的节点 zi 和 zi+1 之间存在一条边。 +从节点 start 出发到节点 `end` 的路径是一个形如 $[z_{0}, z_{1}, z_{2}, ..., z_{k}]$ 的节点序列,满足 $z_{0} = start$ 、$z_{k} = end$ 且在所有符合 $0 <= i <= k-1$ 的节点 $z_{i}$ 和 $z_{i}+1$ 之间存在一条边。 -路径的距离定义为这条路径上所有边的权重总和。用 distanceToLastNode(x) 表示节点 n 和 x 之间路径的最短距离。受限路径 为满足 distanceToLastNode(zi) > distanceToLastNode(zi+1) 的一条路径,其中 0 <= i <= k-1 。 +路径的距离定义为这条路径上所有边的权重总和。用 `distanceToLastNode(x)` 表示节点 `n` 和 `x` 之间路径的最短距离。受限路径 为满足 $distanceToLastNode(z_{i}) > distanceToLastNode(z_{i}+1)$ 的一条路径,其中 $0 <= i <= k-1$ 。 -返回从节点 1 出发到节点 n 的 受限路径数 。由于数字可能很大,请返回对 109 + 7 取余 的结果。 +返回从节点 `1` 出发到节点 `n` 的 受限路径数 。由于数字可能很大,请返回对 $10^9 + 7$ 取余 的结果。 示例 1: @@ -38,12 +38,12 @@ Tag : 「最短路」、「线性 DP」 ``` 提示: -* 1 <= n <= 2 * $10^4$ -* n - 1 <= edges.length <= 4 * $10^4$ -* edges[i].length == 3 -* 1 <= ui, vi <= n -* ui != vi -* 1 <= weighti <= $10^5$ +* $1 <= n <= 2 \times 10^4$ +* $n - 1 <= edges.length <= 4 \times 10^4$ +* $edges[i].length == 3$ +* $1 <= ui, vi <= n$ +* $u_i != v_i$ +* $1 <= weighti <= 10^5$ * 任意两个节点之间至多存在一条边 * 任意两个节点之间至少存在一条路径 @@ -51,35 +51,35 @@ Tag : 「最短路」、「线性 DP」 ### 堆优化 Dijkstra + 动态规划 -n 为点的数量,m 为边的数量。 +`n` 为点的数量,`m` 为边的数量。 -为了方便理解,我们将第 n 个点称为「起点」,第 1 个点称为「结尾」。 +为了方便理解,我们将第 `n` 个点称为「起点」,第 `1` 个点称为「结尾」。 按照题意,我们需要先求每个点到结尾的「最短路」,求最短路的算法有很多,通常根据「有无负权边」& 「稠密图还是稀疏图」进行选择。 -该题只有正权变,而且”边“和”点“的数量在一个数量级上,属于稀疏图。 +该题只有正权变,而且“边”和“点”的数量在一个数量级上,属于稀疏图。 因此我们可以采用「最短路」算法:堆优化的 Dijkstra,复杂度为 $O(m\log{n})$。 -*PS. 通常会优先选择 SPFA,SPFA 通常情况下复杂度为 $O(m)$,但最坏情况下复杂度为 $O(n*m)$。从数据上来说 SPFA 也会超,而且本题还结合了 DP,因此可能会卡掉图论部分的 SPFA。出于这些考虑,我直接使用堆优化 Dijkstra。* +> PS. 通常会优先选择 SPFA,SPFA 通常情况下复杂度为 $O(m)$,但最坏情况下复杂度为 $O(n \times m)$。从数据上来说 SPFA 也会超,而且本题还结合了 DP,因此可能会卡掉图论部分的 SPFA。出于这些考虑,我直接使用堆优化 Dijkstra。 当我们求得了每个点到结尾的「最短路」之后,接下来我们需要求得从「起点」到「结尾」的**受限路径数量**。 这显然可以用 DP 来做。 -我们定义 f(i) 为从第 i 个点到结尾的受限路径数量,f(1) 就是我们的答案,而 f(n) = 1 是一个显而易见的起始条件。 +我们定义 `f(i)` 为从第 `i` 个点到结尾的受限路径数量,`f(1)` 就是我们的答案,而 `f(n) = 1` 是一个显而易见的起始条件。 因为题目的**受限路径数**的定义,我们需要找的路径所包含的点,必须是其距离结尾的最短路越来越近的。 -举个🌰,对于示例 1,其中一条符合要求的路径为 1 --> 2 --> 3 --> 5。 +举个🌰,对于示例 `1`,其中一条符合要求的路径为 `1 --> 2 --> 3 --> 5`。 这条路径的搜索过程可以看做,从结尾(第 5 个点)出发,逆着走,每次选择一个点(例如 a)之后,再选择下一个点(例如 b)时就必须**满足最短路距离比上一个点(点 a)要远**,如果最终能选到起点(第一个点),说明统计出一条有效路径。 我们的搜索方式决定了需要先按照最短路距离进行从小到大排序。 -**不失一般性,当我们要求 f(i) 的时候,其实找的是 i 点可以到达的点 j,并且 j 点到结尾的最短路要严格小于 i 点到结尾的最短路。** +**不失一般性,当我们要求 `f(i)` 的时候,其实找的是 `i` 点可以到达的点 `j`,并且 `j` 点到结尾的最短路要严格小于 `i` 点到结尾的最短路。** -符合条件的点 j 有很多个,将所有的 f(j) 累加即是 f(i)。 +符合条件的点 `j` 有很多个,将所有的 `f(j)` 累加即是 `f(i)`。 代码: ```java diff --git "a/LeetCode/1791-1800/1797. \350\256\276\350\256\241\344\270\200\344\270\252\351\252\214\350\257\201\347\263\273\347\273\237\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/1791-1800/1797. \350\256\276\350\256\241\344\270\200\344\270\252\351\252\214\350\257\201\347\263\273\347\273\237\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..63e9e54f --- /dev/null +++ "b/LeetCode/1791-1800/1797. \350\256\276\350\256\241\344\270\200\344\270\252\351\252\214\350\257\201\347\263\273\347\273\237\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,160 @@ +### 题目描述 + +这是 LeetCode 上的 **[1797. 设计一个验证系统]()** ,难度为 **中等**。 + +Tag : 「链表」、「哈希表」 + + + +你需要设计一个包含验证码的验证系统。每一次验证中,用户会收到一个新的验证码,这个验证码在 `currentTime` 时刻之后 `timeToLive` 秒过期。如果验证码被更新了,那么它会在 `currentTime` (可能与之前的 `currentTime` 不同)时刻延长 `timeToLive` 秒。 + +请你实现 `AuthenticationManager` 类: + +* `AuthenticationManager(int timeToLive)` 构造 `AuthenticationManager` 并设置 `timeToLive` 参数。 +* `generate(string tokenId, int currentTime)` 给定 `tokenId`,在当前时间 `currentTime` 生成一个新的验证码。 +* `renew(string tokenId, int currentTime)` 将给定 `tokenId` 且 未过期 的验证码在 `currentTime` 时刻更新。如果给定 `tokenId` 对应的验证码不存在或已过期,请你忽略该操作,不会有任何更新操作发生。 +* `countUnexpiredTokens(int currentTime)` 请返回在给定 `currentTime` 时刻,未过期 的验证码数目。 + +如果一个验证码在时刻 `t` 过期,且另一个操作恰好在时刻 `t` 发生(`renew` 或者 `countUnexpiredTokens` 操作),过期事件 优先于 其他操作。 + +示例 1: +![](https://assets.leetcode.com/uploads/2021/02/25/copy-of-pc68_q2.png) +``` +输入: +["AuthenticationManager", "renew", "generate", "countUnexpiredTokens", "generate", "renew", "renew", "countUnexpiredTokens"] +[[5], ["aaa", 1], ["aaa", 2], [6], ["bbb", 7], ["aaa", 8], ["bbb", 10], [15]] + +输出: +[null, null, null, 1, null, null, null, 0] + +解释: +AuthenticationManager authenticationManager = new AuthenticationManager(5); // 构造 AuthenticationManager ,设置 timeToLive = 5 秒。 +authenticationManager.renew("aaa", 1); // 时刻 1 时,没有验证码的 tokenId 为 "aaa" ,没有验证码被更新。 +authenticationManager.generate("aaa", 2); // 时刻 2 时,生成一个 tokenId 为 "aaa" 的新验证码。 +authenticationManager.countUnexpiredTokens(6); // 时刻 6 时,只有 tokenId 为 "aaa" 的验证码未过期,所以返回 1 。 +authenticationManager.generate("bbb", 7); // 时刻 7 时,生成一个 tokenId 为 "bbb" 的新验证码。 +authenticationManager.renew("aaa", 8); // tokenId 为 "aaa" 的验证码在时刻 7 过期,且 8 >= 7 ,所以时刻 8 的renew 操作被忽略,没有验证码被更新。 +authenticationManager.renew("bbb", 10); // tokenId 为 "bbb" 的验证码在时刻 10 没有过期,所以 renew 操作会执行,该 token 将在时刻 15 过期。 +authenticationManager.countUnexpiredTokens(15); // tokenId 为 "bbb" 的验证码在时刻 15 过期,tokenId 为 "aaa" 的验证码在时刻 7 过期,所有验证码均已过期,所以返回 0 。 +``` + +提示: +* $1 <= timeToLive <= 10^8$ +* $1 <= currentTime <= 10^8$ +* $1 <= tokenId.length <= 5$ +* `tokenId` 只包含小写英文字母。 +* 所有 `generate` 函数的调用都会包含独一无二的 `tokenId` 值。 +* 所有函数调用中,`currentTime` 的值 严格递增 。 +* 所有函数的调用次数总共不超过 `2000` 次。 + +--- + +### 哈希表 + +数据范围只有 `20`,我们使用哈希表记录每个 `id` 的过期时间 `ct`,在每次查询时遍历整个哈希表来统计未过期的验证码数量。 + +代码: +```Java +class AuthenticationManager { + int d; + Map map = new HashMap<>(); + public AuthenticationManager(int timeToLive) { + d = timeToLive; + } + public void generate(String id, int ct) { + map.put(id, ct + d); + } + public void renew(String id, int ct) { + if (!map.containsKey(id) || map.get(id) <= ct) return ; + map.put(id, ct + d); + } + public int countUnexpiredTokens(int ct) { + int ans = 0; + for (String id : map.keySet()) { + if (map.get(id) > ct) ans++; + } + return ans; + } +} +``` +* 时间复杂度:`generate` 操作和 `renew` 操作的复杂度为 $O(1)$;`countUnexpiredTokens` 操作的复杂度为 $O(n)$ +* 空间复杂度:$O(n)$ + +--- + +### 双向链表 + +在所有函数的调用过程中,`timeToLive` 都在单调递增。 + +在哈希表的做法里,我们没有清理旧验证码的操作,同时每次执行 `countUnexpiredTokens` 时,需要对整个哈希表进行遍历。 + +实际上,如果我们引入 **双向链表**,并将哈希表的键值对定义从 `{验证码:过期时间值}` 调整为 `{验证码:链表节点}` 时(链表节点属性仅包含验证码字符串 `id` 及其过期时间 `t`),我们便能实现如下优化: + +* **减少统计未过期验证码时的无效遍历**:由于构建的双向链表严格按照 `timeToLive` 递增,因此可以从尾部出发,从后往前利用链表节点的 `prev` 指针进行遍历统计。 + + 如此一来,有多少未过期的验证码,我们就会遍历多少个链表节点,其余已过期的节点对象并不会被访问; + +* **引入清理时机**:由于上述的统计过程,我们会找到最靠前的一个未过期节点。可以将其作为新的双向链表新头结点,从而将整段的过期节点从双向链表中删除 + +最后,我们像对其他链表题目一样,为了方便,引入 `he` 和 `ta` 的头尾链表哨兵节点以减少边界处理。 + + +代码: +```Java +class AuthenticationManager { + class Node { + String id; + int t; + Node prev, next; + Node (String _id, int _t) { + id = _id; t = _t; + } + } + int d; + Node he, ta; + Map map = new HashMap<>(); + public AuthenticationManager(int timeToLive) { + he = new Node("", -1); ta = new Node("", (int)1e9); + he.next = ta; ta.prev = he; + d = timeToLive; + } + public void generate(String id, int ct) { + Node node = new Node(id, ct + d); + node.prev = ta.prev; + node.next = ta; + ta.prev.next = node; + ta.prev = node; + map.put(id, node); + } + public void renew(String id, int ct) { + if (!map.containsKey(id) || map.get(id).t <= ct) return ; + Node node = map.get(id); + node.prev.next = node.next; + node.next.prev = node.prev; + generate(id, ct); + } + public int countUnexpiredTokens(int ct) { + int ans = 0; + Node cur = ta.prev; + while (cur.t > ct && ++ans >= 0) cur = cur.prev; + he.next = cur.next; + cur.next.prev = he; + return ans; + } +} +``` +* 时间复杂度:`generate` 操作和 `renew` 操作的复杂度为 $O(1)$;`countUnexpiredTokens` 操作的复杂度为 $O(n)$ +* 空间复杂度:$O(n)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.1797` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/2331-2340/2335. \350\243\205\346\273\241\346\235\257\345\255\220\351\234\200\350\246\201\347\232\204\346\234\200\347\237\255\346\200\273\346\227\266\351\225\277\357\274\210\347\256\200\345\215\225\357\274\211.md" "b/LeetCode/2331-2340/2335. \350\243\205\346\273\241\346\235\257\345\255\220\351\234\200\350\246\201\347\232\204\346\234\200\347\237\255\346\200\273\346\227\266\351\225\277\357\274\210\347\256\200\345\215\225\357\274\211.md" new file mode 100644 index 00000000..2e81894d --- /dev/null +++ "b/LeetCode/2331-2340/2335. \350\243\205\346\273\241\346\235\257\345\255\220\351\234\200\350\246\201\347\232\204\346\234\200\347\237\255\346\200\273\346\227\266\351\225\277\357\274\210\347\256\200\345\215\225\357\274\211.md" @@ -0,0 +1,123 @@ +### 题目描述 + +这是 LeetCode 上的 **[2335. 装满杯子需要的最短总时长]()** ,难度为 **简单**。 + +Tag : 「递归」、「贪心」、「数学」、「排序」 + + + +现有一台饮水机,可以制备冷水、温水和热水。每秒钟,可以装满 `2` 杯 不同 类型的水或者 `1` 杯任意类型的水。 + +给你一个下标从 `0` 开始、长度为 `3` 的整数数组 `amount`,其中 `amount[0]`、`amount[1]` 和 `amount[2]` 分别表示需要装满冷水、温水和热水的杯子数量。 + +返回装满所有杯子所需的 **最少** 秒数。 + +示例 1: +``` +输入:amount = [1,4,2] + +输出:4 + +解释:下面给出一种方案: +第 1 秒:装满一杯冷水和一杯温水。 +第 2 秒:装满一杯温水和一杯热水。 +第 3 秒:装满一杯温水和一杯热水。 +第 4 秒:装满一杯温水。 +可以证明最少需要 4 秒才能装满所有杯子。 +``` +示例 2: +``` +输入:amount = [5,4,4] + +输出:7 + +解释:下面给出一种方案: +第 1 秒:装满一杯冷水和一杯热水。 +第 2 秒:装满一杯冷水和一杯温水。 +第 3 秒:装满一杯冷水和一杯温水。 +第 4 秒:装满一杯温水和一杯热水。 +第 5 秒:装满一杯冷水和一杯热水。 +第 6 秒:装满一杯冷水和一杯温水。 +第 7 秒:装满一杯热水。 +``` +示例 3: +``` +输入:amount = [5,0,0] + +输出:5 + +解释:每秒装满一杯冷水。 +``` + +提示: +* $amount.length = 3$ +* $0 <= amount[i] <= 100$ + +--- + +### 递归 + +水的种类固定为 `3`,且每种水的数据范围只有 $100$,可直接使用递归进行求解。 + +为了尽可能的凑成多的对数,我们可以每次取剩余数量最多且不为 `0` 的两类水进行成组(因此每次处理前需要先对当前 `amount` 进行排序),直到没有水剩余,或只有一类水的剩余数据量不为 `0`(剩下的水只能独自生成)。 + +代码: +```Java +class Solution { + public int fillCups(int[] amount) { + Arrays.sort(amount); + if (amount[1] == 0) return amount[2]; + amount[1] -= 1; amount[2] -= 1; + return 1 + fillCups(amount); + } +} +``` +* 时间复杂度:由于 `amount` 的长度固定为 `3`,因此排序消耗可视为常量;每次至少会消耗两杯水,直到递归结束或只剩下一种水。复杂度为 $O(\sum_{i = 0}^{2} amount[i])$ +* 空间复杂度:忽略递归和排序带来的额外空间开销,复杂度为 $O(1)$ + +--- + +### 贪心 + 数学 + +我们已经知道可以通过「每次取剩余数量最多且不为 `0` 的水成对」来实现最少生成次数。 + +那么是否存在直接计算最少生成次数的方法,而不是采用逐回合模拟。 + +考虑对 `amount` 进行排序,分别使用 `a`、`b` 和 `c` 代表剩余次数递增的三种种类。 + +根据 $a + b$ 和 $c$ 的大小关系进行分情况讨论: + +* $a + b \leq c$,此时每消耗一个 $c$ 都可以顺带消除一个 $a$ 或 $b$,因此最少生成次数为 $c$; + +* $a + b > c$,此时考虑其之间的差值 $t = a + b - c$,若能顺利通过 $\frac{t}{2}$ 次对 $a$ 和 $b$ 的合并,可以将局面转成情况一,最少生成次数为 $\frac{t}{2} + c$; + + 考虑起始是否能先对 $a$ 和 $b$ 进行 $\frac{t}{2}$ 个回合,由于我们有 $a \leq b$,我们只需考虑是否必然有 $a \geq \frac{t}{2}$ 即可,可通过反证法证明 $a < \frac{t}{2}$ 必然不成立,若有 $a < \frac{t}{2}$,则有 $2a < a + b - c$ => $a < b - c$,由于 $b \leq c$,则有 $a < 0$,与原条件冲突。 + + 最后,为了处理 $t$ 的奇偶性问题,先用 $a$ 和 $b$ 进行 $\left \lceil \frac{a + b - c}{2} \right \rceil$ 抵消操作,再与 $c$ 进行抵消 + +代码: +```Java +class Solution { + public int fillCups(int[] amount) { + Arrays.sort(amount); + int a = amount[0], b = amount[1], c = amount[2]; + if (a + b <= c) return c; + else return (a + b - c + 1) / 2 + c; + } +} +``` +* 时间复杂度:$O(1)$ +* 空间复杂度:$O(1)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.2335` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/451-460/456. 132 \346\250\241\345\274\217\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/451-460/456. 132 \346\250\241\345\274\217\357\274\210\344\270\255\347\255\211\357\274\211.md" index af4c2e89..6ae4f7cd 100644 --- "a/LeetCode/451-460/456. 132 \346\250\241\345\274\217\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/451-460/456. 132 \346\250\241\345\274\217\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -6,37 +6,41 @@ Tag : 「单调栈」 -给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。 +给你一个整数数组 `nums`,数组中共有 `n` 个整数。`132` 模式的子序列 由三个整数 `nums[i]`、`nums[j]` 和 `nums[k]` 组成,并同时满足:$i < j < k$ 和 $nums[i] < nums[k] < nums[j]$ 。 -如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。 - - -进阶:很容易想到时间复杂度为 $O(n^2)$ 的解决方案,你可以设计一个时间复杂度为 $O(n logn)$ 或$ O(n)$ 的解决方案吗? +如果 `nums` 中存在 `132` 模式的子序列 ,返回 `true`;否则,返回 `false`。 +进阶:很容易想到时间复杂度为 $O(n^2)$ 的解决方案,你可以设计一个时间复杂度为 $O(n \log{n})$ 或$ O(n)$ 的解决方案吗? 示例 1: ``` 输入:nums = [1,2,3,4] + 输出:false + 解释:序列中不存在 132 模式的子序列。 ``` 示例 2: ``` 输入:nums = [3,1,4,2] + 输出:true + 解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。 ``` 示例 3: ``` 输入:nums = [-1,3,2,0] + 输出:true + 解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。 ``` 提示: -* n == nums.length -* 1 <= n <= $10^4$ -* -$10^9$ <= nums[i] <= $10^9$ +* $n = nums.length$ +* $1 <= n <= 10^4$ +* $-10^9 <= nums[i] <= 10^9$ --- @@ -62,7 +66,7 @@ Tag : 「单调栈」 也许你还不理解是什么意思。没关系,我们一边证明一边说。 -*** +--- ### 过程 & 证明 @@ -91,12 +95,7 @@ Tag : 「单调栈」 **综上,由于「单调递减」的性质,我们至少能找到「遍历过程中」所有符合条件的 `ijk` 中 `k` 最大的那个组合。** -*** - -### 单调栈 - -代码: - +Java 代码: ```Java class Solution { public boolean find132pattern(int[] nums) { @@ -115,6 +114,38 @@ class Solution { } } ``` +Python3 代码: +```Python3 +class Solution: + def find132pattern(self, nums: List[int]) -> bool: + stack = [] + k = -(10 ** 9 + 7) + for i in range(len(nums) - 1,-1,-1): + if nums[i] < k: + return True + while stack and stack[-1] < nums[i]: + k = max(k,stack.pop()) + stack.append(nums[i]) + return False +``` +C++ 代码: +```C++ +class Solution { +public: + bool find132pattern(vector& nums) { + stack st; + int n = nums.size(), k = INT_MIN; + for(int i = n - 1; i >= 0; i--){ + if(nums[i] < k) return true; + while(!st.empty() and st.top() < nums[i]) { + k = max(k,st.top()); st.pop(); + } + st.push(nums[i]); + } + return false; + } +}; +``` * 时间复杂度:$O(n)$ * 空间复杂度:$O(n)$ diff --git "a/LeetCode/621-630/629. K\344\270\252\351\200\206\345\272\217\345\257\271\346\225\260\347\273\204\357\274\210\345\233\260\351\232\276\357\274\211.md" "b/LeetCode/621-630/629. K\344\270\252\351\200\206\345\272\217\345\257\271\346\225\260\347\273\204\357\274\210\345\233\260\351\232\276\357\274\211.md" index 92ec30a9..485331fa 100644 --- "a/LeetCode/621-630/629. K\344\270\252\351\200\206\345\272\217\345\257\271\346\225\260\347\273\204\357\274\210\345\233\260\351\232\276\357\274\211.md" +++ "b/LeetCode/621-630/629. K\344\270\252\351\200\206\345\272\217\345\257\271\346\225\260\347\273\204\357\274\210\345\233\260\351\232\276\357\274\211.md" @@ -111,8 +111,8 @@ class Solution { } } ``` -* 时间复杂度:$O(n * k)$ -* 空间复杂度:$O(n * k)$ +* 时间复杂度:$O(n \times k)$ +* 空间复杂度:$O(n \times k)$ --- diff --git "a/LeetCode/641-650/648. \345\215\225\350\257\215\346\233\277\346\215\242\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/641-650/648. \345\215\225\350\257\215\346\233\277\346\215\242\357\274\210\344\270\255\347\255\211\357\274\211.md" index 46845469..ef47179b 100644 --- "a/LeetCode/641-650/648. \345\215\225\350\257\215\346\233\277\346\215\242\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/641-650/648. \345\215\225\350\257\215\346\233\277\346\215\242\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -55,7 +55,7 @@ Tag : 「字典树」 考虑两个 `Trie` 的基本操作: * `add` 操作:变量入参字符串 `s`,将字符串中的每位字符映射到 $[0, 25]$,同时为了能够方便查询某个字符串(而不只是某个前缀)是否曾经存入过 `Trie` 中,额外使用一个布尔数组 `isEnd` 记录某个位置是否为单词结尾。 -* `query` 操作: +* `query` 操作:对变量入参字符串 `s` 进行遍历,如果在字典树不存在该字符串的任何前缀,直接返回 `s`,否则返回首个出现(最短)的前缀。 至于二维数组的大小估算,可以直接开成 $N \times C$,其中 $N$ 为要插入到 `Trie` 中的字符总数,$C$ 为对应的字符集大小。在 $N \times C$ 没有 `MLE` 风险时,可以直接开这么多;而当 $N \times C$ 较大(超过 $1e7$,甚至 $1e8$ 时),可以适当将 $N \times C$ 中的 $N$ 减少,使得总空间在 $1e7$ 左右,因为实际上由于二维数组中的某些行中会存储一个字符以上,实际上我们用不到这么多行。