diff --git "a/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" "b/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" index 8b1eb06d..66e31cd4 100644 --- "a/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" +++ "b/Index/\345\233\276\350\256\272 \346\234\200\347\237\255\350\267\257.md" @@ -3,6 +3,7 @@ | [407. 接雨水 II](https://leetcode-cn.com/problems/trapping-rain-water-ii/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/trapping-rain-water-ii/solution/gong-shui-san-xie-jing-dian-dijkstra-yun-13ik/) | 困难 | 🤩🤩🤩🤩 | | [743. 网络延迟时间](https://leetcode-cn.com/problems/network-delay-time/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/network-delay-time/solution/gong-shui-san-xie-yi-ti-wu-jie-wu-chong-oghpz/) | 中等 | 🤩🤩🤩🤩🤩 | | [787. K 站中转内最便宜的航班](https://leetcode-cn.com/problems/cheapest-flights-within-k-stops/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/cheapest-flights-within-k-stops/solution/gong-shui-san-xie-xiang-jie-bellman-ford-dc94/) | 中等 | 🤩🤩🤩🤩🤩 | +| [882. 细分图中的可到达节点](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/) | [LeetCode 题解链接](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/solution/by-ac_oier-yrhg/) | 困难 | 🤩🤩🤩🤩 | | [1631. 最小体力消耗路径](https://leetcode-cn.com/problems/path-with-minimum-effort/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/path-with-minimum-effort/solution/fan-zheng-fa-zheng-ming-si-lu-de-he-fa-x-ohby/) | 中等 | 🤩🤩🤩 | | [1786. 从第一个节点出发到最后一个节点的受限路径数](https://leetcode-cn.com/problems/number-of-restricted-paths-from-first-to-last-node/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/number-of-restricted-paths-from-first-to-last-node/solution/xiang-jie-dui-you-hua-dijkstra-dong-tai-i6j0d/) | 中等 | 🤩🤩🤩 | | [1976. 到达目的地的方案数](https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination/) | [LeetCode 题解链接](https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination/solution/by-ac_oier-4ule/) | 中等 | 🤩🤩🤩🤩 | diff --git "a/LeetCode/801-810/809. \346\203\205\346\204\237\344\270\260\345\257\214\347\232\204\346\226\207\345\255\227\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/801-810/809. \346\203\205\346\204\237\344\270\260\345\257\214\347\232\204\346\226\207\345\255\227\357\274\210\344\270\255\347\255\211\357\274\211.md" index 434be79d..a5d89963 100644 --- "a/LeetCode/801-810/809. \346\203\205\346\204\237\344\270\260\345\257\214\347\232\204\346\226\207\345\255\227\357\274\210\344\270\255\347\255\211\357\274\211.md" +++ "b/LeetCode/801-810/809. \346\203\205\346\204\237\344\270\260\345\257\214\347\232\204\346\226\207\345\255\227\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -44,7 +44,7 @@ words = ["hello", "hi", "helo"] * 当 `a` 和 `b` 长度相同,定义为可扩张; * 当 `a` 和 `b` 长度不同,根据「`a` 和 `b` 长度对比」以及「`a` 的长度大小」分情况讨论: * 当 `b` 长度大于 `a`,不可扩张; - * 当 `a` 长度大于 `b`,**我们不一定要拿整一段的 `b` 进行扩张,可以拿 `b` 中的一个字符进行扩张。** 因此只需要满足扩张后的长度(`a` 的长度)大于 $3$ 即可定义为可扩张。 + * 当 `a` 长度大于 `b`,**我们不一定要拿整一段的 `b` 进行扩张,可以拿 `b` 中的一个字符进行扩张。** 因此只需要满足扩张后的长度(`a` 的长度)大于等于 $3$ 即可定义为可扩张。 搞明白何为 “扩张” 后,剩余的则是简单的「双指针 + 模拟」做法。 diff --git "a/LeetCode/811-820/813. \346\234\200\345\244\247\345\271\263\345\235\207\345\200\274\345\222\214\347\232\204\345\210\206\347\273\204\357\274\210\344\270\255\347\255\211\357\274\211.md" "b/LeetCode/811-820/813. \346\234\200\345\244\247\345\271\263\345\235\207\345\200\274\345\222\214\347\232\204\345\210\206\347\273\204\357\274\210\344\270\255\347\255\211\357\274\211.md" new file mode 100644 index 00000000..bf6c4220 --- /dev/null +++ "b/LeetCode/811-820/813. \346\234\200\345\244\247\345\271\263\345\235\207\345\200\274\345\222\214\347\232\204\345\210\206\347\273\204\357\274\210\344\270\255\347\255\211\357\274\211.md" @@ -0,0 +1,131 @@ +### 题目描述 + +这是 LeetCode 上的 **[813. 最大平均值和的分组](https://leetcode.cn/problems/largest-sum-of-averages/solution/by-ac_oier-yfnt/)** ,难度为 **中等**。 + +Tag : 「序列 DP」、「前缀和」、「动态规划」、「数学」 + + + +给定数组 `nums` 和一个整数 `k` 。我们将给定的数组 `nums` 分成 最多 `k` 个相邻的非空子数组 。 分数 由每个子数组内的平均值的总和构成。 + +注意我们必须使用 `nums` 数组中的每一个数进行分组,并且分数不一定需要是整数。 + +返回我们所能得到的最大 分数 是多少。答案误差在 $10^{-6}$ 内被视为是正确的。 + +示例 1: +``` +输入: nums = [9,1,2,3,9], k = 3 + +输出: 20.00000 + +解释: +nums 的最优分组是[9], [1, 2, 3], [9]. 得到的分数是 9 + (1 + 2 + 3) / 3 + 9 = 20. +我们也可以把 nums 分成[9, 1], [2], [3, 9]. +这样的分组得到的分数为 5 + 2 + 6 = 13, 但不是最大值. +``` +示例 2: +``` +输入: nums = [1,2,3,4,5,6,7], k = 4 + +输出: 20.50000 +``` + +提示: +* $1 <= nums.length <= 100$ +* $1 <= nums[i] <= 10^4$ + +--- + +### 前缀和 + 序列 DP + +题意可整理为一句话:将 $n$ 个元素划分为「最多」$m$ 个连续段,最大化连续段的平均值之和。 + +为了方便,我们令所有数组下标从 $1$ 开始。 + +定义 $f[i][j]$ 为考虑将前 $i$ 个元素划分成 $j$ 份的最大平均和,答案为 $f[n][k]$,其中 $1 \leq k \leq m$。 + +不失一般性考虑 $f[i][j]$ 该如何计算,由于划分出来的子数组不能是空集,因此我们可以根据 $j$ 的大小分情况讨论: + +* 当 $j = 1$,此时有 $f[i][j] = \frac{\sum_{idx = 1}^{i} nums[idx - 1]}{i}$ +* 当 $j > 1$,此时枚举最后一个子数组的起点 $k$,其中 $2 \leq k \leq i$,此时有平均值之和为 $f[k - 1][j - 1] + \frac{\sum_{idx = k}^{i} nums[idx]}{i - k + 1}$,最终 $f[i][j]$ 为枚举所有 $k$ 值的最大值 + +其中求解连续段之和可以用「前缀和」进行优化。同时,想要简化代码,还可以利用一个简单的数学结论:划分份数越多,平均值之和越大,因此想要取得最大值必然是恰好划分成 $m$ 份。 + +Java 代码: +```Java +class Solution { + public double largestSumOfAverages(int[] nums, int m) { + int n = nums.length; + double[] sum = new double[n + 10]; + for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1]; + double[][] f = new double[n + 10][m + 10]; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= Math.min(i, m); j++) { + if (j == 1) { + f[i][1] = sum[i] / i; + } else { + for (int k = 2; k <= i; k++) { + f[i][j] = Math.max(f[i][j], f[k - 1][j - 1] + (sum[i] - sum[k - 1]) / (i - k + 1)); + } + } + } + } + return f[n][m]; + } +} +``` +TypeScript 代码: +```TypeScript +function largestSumOfAverages(nums: number[], m: number): number { + const n = nums.length + const sum = new Array(n + 10).fill(0) + for (let i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1] + const f = new Array>() + for (let i = 0; i < n + 10; i++) f[i] = new Array(m + 10).fill(0) + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= Math.min(i, m); j++) { + if (j == 1) { + f[i][j] = sum[i] / i + } else { + for (let k = 2; k <= i; k++) { + f[i][j] = Math.max(f[i][j], f[k - 1][j - 1] + (sum[i] - sum[k - 1]) / (i - k + 1)) + } + } + } + } + return f[n][m] +} +``` +Python 代码: +```Python +class Solution: + def largestSumOfAverages(self, nums: List[int], m: int) -> float: + n = len(nums) + psum = [0] * (n + 10) + for i in range(1, n + 1): + psum[i] = psum[i - 1] + nums[i - 1] + f = [[0] * (m + 10) for _ in range(n + 10)] + for i in range(1, n + 1): + for j in range(1, min(i, m) + 1): + if j == 1: + f[i][j] = psum[i] / i + else: + for k in range(2, i + 1): + f[i][j] = max(f[i][j], f[k - 1][j - 1] + (psum[i] - psum[k - 1]) / (i - k + 1)) + return f[n][m] +``` +* 时间复杂度:$O(n^2 \times m)$ +* 空间复杂度:$O(n \times m)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.813` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 + diff --git "a/LeetCode/881-890/882. \347\273\206\345\210\206\345\233\276\344\270\255\347\232\204\345\217\257\345\210\260\350\276\276\350\212\202\347\202\271\357\274\210\345\233\260\351\232\276\357\274\211.md" "b/LeetCode/881-890/882. \347\273\206\345\210\206\345\233\276\344\270\255\347\232\204\345\217\257\345\210\260\350\276\276\350\212\202\347\202\271\357\274\210\345\233\260\351\232\276\357\274\211.md" new file mode 100644 index 00000000..53435567 --- /dev/null +++ "b/LeetCode/881-890/882. \347\273\206\345\210\206\345\233\276\344\270\255\347\232\204\345\217\257\345\210\260\350\276\276\350\212\202\347\202\271\357\274\210\345\233\260\351\232\276\357\274\211.md" @@ -0,0 +1,195 @@ +### 题目描述 + +这是 LeetCode 上的 **[882. 细分图中的可到达节点](https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/solution/by-ac_oier-yrhg/)** ,难度为 **困难**。 + +Tag : 「最短路」、「单源最短路」、「Dijkstra」、「SPFA」 + + + +给你一个无向图(原始图),图中有 `n` 个节点,编号从 `0` 到 `n - 1` 。你决定将图中的每条边 细分 为一条节点链,每条边之间的新节点数各不相同。 + +图用由边组成的二维数组 `edges` 表示,其中 $edges[i] = [u_{i}, v_{i}, cnt_{i}]$ 表示原始图中节点 $u_{i}$ 和 $v_{i}$ 之间存在一条边,$cnt_{i}$ 是将边 细分 后的新节点总数。注意,$cnt_{i} = 0$ 表示边不可细分。 + +要 细分边 $[u_{i}, v_{i}]$ ,需要将其替换为 $(cnt_{i} + 1)$ 条新边,和 $cnt_{i}$ 个新节点。新节点为 $x_1, x_2, ..., x_{cnt_{i}}$ ,新边为 $[u_{i}, x_{1}], [x_{1}, x_{2}], [x_{2}, x_{3}], ..., [x_{cnt_{i}}+1, x_{cnt_{i}}], [x_{cnt_{i}}, v_{i}]$ 。 + +现在得到一个 新的细分图 ,请你计算从节点 `0` 出发,可以到达多少个节点?如果节点间距离是 `maxMoves` 或更少,则视为 可以到达 。 + +给你原始图和 `maxMoves` ,返回 新的细分图中从节点 `0` 出发 可到达的节点数 。 + +示例 1: +``` +输入:edges = [[0,1,10],[0,2,1],[1,2,2]], maxMoves = 6, n = 3 + +输出:13 + +解释:边的细分情况如上图所示。 +可以到达的节点已经用黄色标注出来。 +``` +示例 2: +``` +输入:edges = [[0,1,4],[1,2,6],[0,2,8],[1,3,1]], maxMoves = 10, n = 4 + +输出:23 +``` +示例 3: +``` +输入:edges = [[1,2,4],[1,4,5],[1,3,1],[2,3,4],[3,4,5]], maxMoves = 17, n = 5 + +输出:1 + +解释:节点 0 与图的其余部分没有连通,所以只有节点 0 可以到达。 +``` + +提示: +* $0 <= edges.length <= \min(n * (n - 1) / 2, 10^4)$ +* $edges[i].length = 3$ +* $0 <= u_{i} < v_{i} < n$ +* 图中 不存在平行边 +* $0 <= cnt_{i} <= 10^4$ +* $0 <= maxMoves <= 10^9$ +* $1 <= n <= 3000$ + +--- + +### 朴素 Dijkstra + +为了方便,我们将原始图边的数量记为 `m`,因此对于原始图而言,点的数量 $3000$,边的数量为 $10000$。 + +题目要我们求新图上,从 `0` 点出发可到达的点的数量,我们将原图上存在的点称为「原点」,细分边上增加的点称为「细分点」,两类点中可达点的数量即是答案。 + +在分别考虑如何统计两类点之前,我们需要重新定义一下边的权重:**若原点 `u` 和原点 `v` 的边上存在 `c` 个细分点,我们将原点 `u` 和原点 `v` 之间的边看作是一条权重为 `c + 1` 的无向边(结合题意,`c` 个点存在 `c + 1` 个分段/距离)**。 + +重新定义边的权重后,因为该图是「稠密图」,我们可以使用「朴素 Dijkstra」来求解最短路,得到 $dist$ 数组,其中 $dist[x] = t$ 含义为从原点 `0` 点出发,到达原点 `x` 的最短距离为 `t`。 + +> **不了解最短路的同学可以看前置 🧀 : [涵盖所有的「存图方式」与「最短路算法(详尽注释)」](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d)** + +随后考虑如何统计答案(可达点的数量),根据统计点的类型分情况讨论: + +1. 对于原点:若有 $dist[x] \leq max$ 的话,说明原点 `x` 可达,累加到答案中; + +2. 对于细分点:由于所有的细分点都在原图边上,因此我们可以统计所有原图边上有多少细分点可达。 + 对于任意一条边 $e(u, v)$ 而言,该边上可达点数量包含「经过原点 `u` 可达」以及「经过原点 `v` 可达」的并集,其中原点 `0` 到达原点 `u` 以及原点 `v` 的距离,我们是已知的。因此经过原点 `u` 可达的数量为 $\max(0, max - dist[u])$,经过原点 `v` 可达的数量为 $\max(0, max - dist[v])$,两者之和与该边上细分点的总数取 `min` 即是这条边可达点的数量。 + +代码: +```Java +class Solution { + static int N = 3010, INF = 0x3f3f3f3f; + static int[][] g = new int[N][N]; + static int[] dist = new int[N]; + static boolean[] vis = new boolean[N]; + public int reachableNodes(int[][] edges, int max, int n) { + // 建图 + for (int i = 0; i < n; i++) Arrays.fill(g[i], INF); + for (int[] info : edges) { + int a = info[0], b = info[1], c = info[2] + 1; + g[a][b] = g[b][a] = c; + } + // 朴素 Dijkstra + Arrays.fill(dist, INF); + Arrays.fill(vis, false); + dist[0] = 0; + for (int i = 0; i < n; i++) { + int t = -1; + for (int j = 0; j < n; j++) { + if (!vis[j] && (t == -1 || dist[j] < dist[t])) t = j; + } + vis[t] = true; + for (int j = 0; j < n; j++) dist[j] = Math.min(dist[j], dist[t] + g[t][j]); + } + // 统计答案 + int ans = 0; + for (int i = 0; i < n; i++) { + if (dist[i] <= max) ans++; + } + for (int[] info : edges) { + int a = info[0], b = info[1], c = info[2]; + int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]); + ans += Math.min(c, c1 + c2); + } + return ans; + } +} +``` +* 时间复杂度:建图复杂度为 $O(m)$;使用朴素 Dijkstra 求最短路复杂度为 $O(n^2)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(m + n^2)$ +* 空间复杂度:$O(n^2)$ + +--- + +### SPFA + +从数据范围来看,无论是朴素 Dijkstra 还是堆优化版的 Dijkstra 都可以过,复杂度分别为 $O(n^2)$ 和 $O(m\log{n})$。 + +那 Bellman Ford 类的单源最短路就无法通过了吗? + +理论上,无论是 Bellman Ford 还是 SPFA 复杂度均为 $O(n \times m)$,均无法通过本题。但实际上 SPFA 由于使用队列对松弛顺序进行了调整,因此在应对「非菊花图」时均表现良好,复杂度可视为 $O(k \times m)$,近似 $O(m)$。 + +代码: +```Java +class Solution { + static int N = 3010, M = 20010, INF = 0x3f3f3f3f, idx = 0; + static int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M]; + static int[] dist = new int[N]; + static boolean[] vis = new boolean[N]; + void add(int a, int b, int c) { + e[idx] = b; + ne[idx] = he[a]; + w[idx] = c; + he[a] = idx++; + } + public int reachableNodes(int[][] edges, int max, int n) { + // 建图 + Arrays.fill(he, -1); + idx = 0; + for (int[] info : edges) { + int a = info[0], b = info[1], c = info[2] + 1; + add(a, b, c); add(b, a, c); + } + // SPFA + Arrays.fill(dist, INF); + Arrays.fill(vis, false); + Deque d = new ArrayDeque<>(); + d.addLast(0); + dist[0] = 0; + vis[0] = true; + while (!d.isEmpty()) { + int t = d.pollFirst(); + vis[t] = false; + for (int i = he[t]; i != -1; i = ne[i]) { + int j = e[i]; + if (dist[j] > dist[t] + w[i]) { + dist[j] = dist[t] + w[i]; + if (vis[j]) continue; + d.addLast(j); + vis[j] = true; + } + } + } + // 统计答案 + int ans = 0; + for (int i = 0; i < n; i++) { + if (dist[i] <= max) ans++; + } + for (int[] info : edges) { + int a = info[0], b = info[1], c = info[2]; + int c1 = Math.max(0, max - dist[a]), c2 = Math.max(0, max - dist[b]); + ans += Math.min(c, c1 + c2); + } + return ans; + } +} +``` +* 时间复杂度:建图复杂度为 $O(m)$;使用 SPFA 求最短路复杂度为 $O(n \times m)$;统计答案复杂度为 $O(n + m)$。整体复杂度为 $O(n \times m)$ +* 空间复杂度:$O(n + m)$ + +--- + +### 最后 + +这是我们「刷穿 LeetCode」系列文章的第 `No.882` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 + +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 + +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 + +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 +