⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。
学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。
本文是 LeetCode 上分之旅系列的第 49 篇文章,往期回顾请移步到文章末尾~
T1. 有序三元组中的最大值 I(Easy)
T2. 有序三元组中的最大值 II(Medium)
T3. 无限数组的最短子数组(Medium)
T4. 有向图访问计数(Hard)
https://leetcode.cn/problems/maximum-value-of-an-ordered-triplet-i/description/
同 T2。
https://leetcode.cn/problems/maximum-value-of-an-ordered-triplet-ii/description/
初步分析:
思考实现:
思考优化:
为了使得计算结果尽可能大,显然应该让乘法的左右两部分尽可能大。对于存在多个变量的问题,一个重要的技巧是 「固定一个,思考另一个」 ,这就容易多了。
枚举所有方案,记录最优解。
class Solution { fun maximumTripletValue(nums: IntArray): Long { var ret = 0L val n = nums.size for (i in 0 until n) { for (j in i + 1 until n) { for (k in j + 1 until n) { ret = max(ret, 1L * (nums[i] - nums[j]) * nums[k]) } } } return ret } }
复杂度分析:
预处理出每个位置前后的最大值,再枚举 nums[j]nums[j]nums[j] 记录最优解。
class Solution { fun maximumTripletValue(nums: IntArray): Long { val n = nums.size val preMax = IntArray(n) var sufMax = IntArray(n) for (i in 1 until n) { preMax[i] = max(preMax[i - 1], nums[i - 1]) } for (i in n - 2 downTo 0) { sufMax[i] = max(sufMax[i + 1], nums[i + 1]) } return max(0, (1 .. n - 2).maxOf { 1L * (preMax[it] - nums[it]) * sufMax[it] }) } }
复杂度分析:
线性遍历 nums[k]nums[k]nums[k] 并记录 (nums[i]−nums[j])(nums[i] - nums[j])(nums[i]−nums[j]) 的最大值,记录最优解。
class Solution { fun maximumTripletValue(nums: IntArray): Long { val n = nums.size var ret = 0L var maxDelta = 0 var maxI = 0 for (e in nums) { ret = max(ret, 1L * maxDelta * e) maxDelta = max(maxDelta, maxI - e) maxI = max(maxI, e) } return ret } }
class Solution: def maximumTripletValue(self, nums: List[int]) -> int: ret = maxDelta = maxI = 0 for e in nums: ret = max(ret, maxDelta * e) maxDelta = max(maxDelta, maxI - e) maxI = max(maxI, e) return ret
class Solution { public: long long maximumTripletValue(vector<int> &nums) { long long ret = 0; int max_delta = 0, max_i = 0; for (int e : nums) { ret = max(ret, (long long) max_delta * e); max_delta = max(max_delta, max_i - e); max_i = max(max_i, e); } return ret; } };
复杂度分析:
https://leetcode.cn/problems/minimum-size-subarray-in-infinite-array/description/
令 numsnumsnums 数组的整体元素和为 sss,考虑 targettargettarget 的两种情况:
class Solution { fun minSizeSubarray(nums: IntArray, t: Int): Int { val n = nums.size val s = nums.sum() val k = t % s // 同向双指针 var left = 0 var sum = 0 var len = n for (right in 0 until 2 * n) { sum += nums[right % n] while (sum > k) { sum -= nums[left % n] left ++ } if (sum == k) len = min(len, right - left + 1) } return if (len == n) -1 else n * (t / s) + len } }
复杂度分析:
https://leetcode.cn/problems/count-visited-nodes-in-a-directed-graph/description/
初步分析:
对于 nnn 个点 nnn 条边的有向弱连通图,图中每个点的出度都是 111,因此它是一棵 「内向基环树」。那么,对于每个点有 222 种情况:
图片不记得出处了~
思考实现:
拓扑排序减去树链很容易实现,考虑到我们这道题在找到基环后需要反向遍历树链,因此我们考虑构造反向图(外向基环树);
在拓扑排序后,树链上节点的入度都是 000,因此入度大于 000 的节点就位于基环上。枚举未访问的基环节点走 DFS,就可以找到该连通分量的基环。
class Solution { fun countVisitedNodes(edges: List<Int>): IntArray { // 内向基环树 val n = edges.size val degree = IntArray(n) val graph = Array(n) { LinkedList<Int>() } for ((x,y) in edges.withIndex()) { graph[y].add(x) degree[y]++ // 入度 } // 拓扑排序 val ret = IntArray(n) var queue = LinkedList<Int>() for (i in 0 until n) { if (0 == degree[i]) queue.offer(i) } while(!queue.isEmpty()) { val x = queue.poll() val y = edges[x] if (0 == -- degree[y]) queue.offer(y) } // 反向 DFS fun rdfs(i: Int, depth: Int) { for (to in graph[i]) { if (degree[to] == -1) continue ret[to] = depth rdfs(to, depth + 1) } } // 枚举连通分量 for (i in 0 until n) { if (degree[i] <= 0) continue val ring = LinkedList<Int>() var x = i while (true) { degree[x] = -1 ring.add(x) x = edges[x] if (x == i) break } for (e in ring) { ret[e] = ring.size rdfs(e, ring.size + 1) } } return ret } }
复杂度分析:
思路参考小羊的题解。
我们发现这道题的核心在于 「找到每个基环的节点」 ,除了拓扑排序剪枝外,对于内向基环树来,从任何一个节点走 DFS 走到的最后一个节点一定是基环上的节点。
在细节上,对于每个未访问过的节点走 DFS 的结果会存在 333 种情况:
class Solution { fun countVisitedNodes(edges: List<Int>): IntArray { val n = edges.size val ret = IntArray(n) val visit = BooleanArray(n) for (i in 0 until n) { if (visit[i]) continue // DFS val link = LinkedList<Int>() var x = i while (!visit[x]) { visit[x] = true link.add(x) x = edges[x] } if (ret[x] == 0) { val depth = link.size - link.indexOf(x) // (此时 x 位于基环入口) repeat(depth) { ret[link.pollLast()] = depth } } var depth = ret[x] while (!link.isEmpty()) { ret[link.pollLast()] = ++depth } } return ret } }
复杂度分析:
推荐阅读
LeetCode 上分之旅系列往期回顾:
- LeetCode 单周赛第 364 场 · 前后缀分解结合单调栈的贡献问题
- LeetCode 单周赛第 363 场 · 经典二分答案与质因数分解
- LeetCode 双周赛第 114 场 · 一道简单的树上动态规划问题
- LeetCode 双周赛第 113 场 · 精妙的 O(lgn) 扫描算法与树上 DP 问题
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~