本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。

本文是 LeetCode 上分之旅系列的第 49 篇文章,往期回顾请移步到文章末尾~

LeetCode 周赛 365

T1. 有序三元组中的最大值 I(Easy)

  • 标签:模拟、前后缀分解、线性遍历

T2. 有序三元组中的最大值 II(Medium)

  • 标签:模拟、前后缀分解、线性遍历

T3. 无限数组的最短子数组(Medium)

  • 标签:滑动窗口

T4. 有向图访问计数(Hard)

  • 标签:内向基环树、拓扑排序、DFS


T1. 有序三元组中的最大值 I(Easy)

https://leetcode.cn/problems/maximum-value-of-an-ordered-triplet-i/description/

同 T2。


T2. 有序三元组中的最大值 II(Medium)

https://leetcode.cn/problems/maximum-value-of-an-ordered-triplet-ii/description/

问题分析

初步分析:

  • 问题目标: 构造满足条件的合法方案,使得计算结果最大;
  • 问题条件: 数组下标满足 $i < j < k$ 的三位数;
  • 计算结果: $(nums[i] - nums[j]) * nums[k]$。

思考实现:

思考优化:

为了使得计算结果尽可能大,显然应该让乘法的左右两部分尽可能大。对于存在多个变量的问题,一个重要的技巧是 「固定一个,思考另一个」 ,这就容易多了。

  • 固定 $j$: 为了让结果更大,应该找到 $nums[j]$ 左边最大的 $nums[i]$ 和右边最大的 $nums[k]$ 组合,时间复杂度是 $O(n^2)$。我们也可以使用前后缀分解预处理出来,这样时间复杂度就是 $O(n)$;
  • 固定 $k$: 同理,固定 $k$ 寻找应该找到左边使得 $nums[i] - nums[j]$ 最大的方案,这可以实现线性时间和常量空间。

题解一(枚举)

枚举所有方案,记录最优解。

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
}
}

复杂度分析:

  • 时间复杂度:$O(n^3)$
  • 空间复杂度:$O(1)$

题解二(前后缀分解)

预处理出每个位置前后的最大值,再枚举 $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] })
}
}

复杂度分析:

  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(n)$

题解三(线性遍历)

线性遍历 $nums[k]$ 并记录 $(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;
}
};

复杂度分析:

  • 时间复杂度:$O(n)$
  • 空间复杂度:$O(1)$

T3. 无限数组的最短子数组(Medium)

https://leetcode.cn/problems/minimum-size-subarray-in-infinite-array/description/

问题分析

令 $nums$ 数组的整体元素和为 $s$,考虑 $target$ 的两种情况:

  • 对于 $target$ 很小的情况(小于数组整体和 $s$):这是很简单的滑动窗口问题;
  • 对于 $target$ 较大的情况(大于等于数组的整体和 $s$):那么最小长度中一定包含整数倍的 $s$,以及某个 $nums$ 的子数组。
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
}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 最大扫描 $2$ 倍数组长度;
  • 空间复杂度:仅使用常量级别空间。

T4. 有向图访问计数(Hard)

https://leetcode.cn/problems/count-visited-nodes-in-a-directed-graph/description/

问题分析

初步分析:

对于 $n$ 个点 $n$ 条边的有向弱连通图,图中每个点的出度都是 $1$,因此它是一棵 「内向基环树」。那么,对于每个点有 $2$ 种情况:

  • 环上节点:绕环行走一圈后就会回到当前位置,因此最长访问路径就是环长;
  • 树链节点:那么从树链走到环上后也可以绕环行走一圈,因此最长访问路径就是走到环的路径 + 环长。

图片不记得出处了~

思考实现:

  • 只有一个连通分量的情况: 那么问题就相对简单,我们用拓扑排序剪去树链,并记录链上节点的深度(到环上的距离),最后剩下的部分就是基环;
  • 有多个连通分量的情况: 我们需要枚举每个连通分量的基环,再将基环的长度累加到该连通分量的每个节点。

题解(拓扑排序 + DFS)

  • 第一个问题:将基环的长度累加到该连通分量的每个节点

拓扑排序减去树链很容易实现,考虑到我们这道题在找到基环后需要反向遍历树链,因此我们考虑构造反向图(外向基环树);

  • 第二个问题:找到基环长度

在拓扑排序后,树链上节点的入度都是 $0$,因此入度大于 $0$ 的节点就位于基环上。枚举未访问的基环节点走 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
}
}

复杂度分析:

  • 时间复杂度:$O(n)$ 拓扑排序和 DFS 都是线性时间;
  • 空间复杂度:$O(n)$ 图空间和队列空间。

题解二(朴素 DFS)

思路参考小羊的题解。

我们发现这道题的核心在于 「找到每个基环的节点」 ,除了拓扑排序剪枝外,对于内向基环树来,从任何一个节点走 DFS 走到的最后一个节点一定是基环上的节点。

在细节上,对于每个未访问过的节点走 DFS 的结果会存在 $3$ 种情况:

  • 环上节点:刚好走过基环;
  • 树链节点:走过树链 + 基环。
  • 还有 $1$ 种情况:DFS 起点是从树链的末端走的,而前面树链的部分和基环都被走过,此时 DFS 终点就不一定是基环节点了。这种情况就同理从终点直接反向遍历就好了,等于说省略了处理基环的步骤。
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
}
}

复杂度分析:

  • 时间复杂度:$O(n)$ DFS 都是线性时间;
  • 空间复杂度:$O(n)$ 图空间和队列空间。

推荐阅读

LeetCode 上分之旅系列往期回顾:

️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

LeetCode 周赛上分之旅 #49 再探内向基环树的更多相关文章

  1. 集合类再探:不可变类的好处,Collector接口详解,使用内部迭代

    集合类再探 注:本文使用的pom依赖见文末. 集合类的基础 - Iterable.Iterator java语言层面支持对实现了Iterable接口的对象使用for-each语句.Iterator可以 ...

  2. 刷爆 LeetCode 周赛 337,位掩码/回溯/同余/分桶/动态规划·打家劫舍/贪心

    本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 上周末是 LeetCode 第 337 场周赛,你参加了吗?这场周赛第三题有点放水,如果 ...

  3. LeetCode 周赛 342(2023/04/23)容斥原理、计数排序、滑动窗口、子数组 GCB

    本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 前天刚举办 2023 年力扣杯个人 SOLO 赛,昨天周赛就出了一场 Easy - Ea ...

  4. 【再探backbone 02】集合-Collection

    前言 昨天我们一起学习了backbone的model,我个人对backbone的熟悉程度提高了,但是也发现一个严重的问题!!! 我平时压根没有用到model这块的东西,事实上我只用到了view,所以昨 ...

  5. 再探jQuery

    再探jQuery 前言:在使用jQuery的时候发现一些知识点记得并不牢固,因此希望通过总结知识点加深对jQuery的应用,也希望和各位博友共同分享. jQuery是一个JavaScript库,它极大 ...

  6. [老老实实学WCF] 第五篇 再探通信--ClientBase

    老老实实学WCF 第五篇 再探通信--ClientBase 在上一篇中,我们抛开了服务引用和元数据交换,在客户端中手动添加了元数据代码,并利用通道工厂ChannelFactory<>类创 ...

  7. Spark Streaming揭秘 Day7 再探Job Scheduler

    Spark Streaming揭秘 Day7 再探Job Scheduler 今天,我们对Job Scheduler再进一步深入一下,对一些更加细节的源码进行分析. Job Scheduler启动 在 ...

  8. 再探ASP.NET 5(转载)

    就在最近一段时间,微软又有大动作了,在IDE方面除了给我们发布了Viausl Studio 2013 社区版还发布了全新的Visual Studio 2015 Preview. Visual Stud ...

  9. 再探java基础——break和continue的用法

    再探java基础——break和continue的用法 break break可用于循环和switch...case...语句中. 用于switch...case中: 执行完满足case条件的内容内后 ...

  10. 第四节:SignalR灵魂所在Hub模型及再探聊天室样例

    一. 整体介绍 本节:开始介绍SignalR另外一种通讯模型Hub(中心模型,或者叫集线器模型),它是一种RPC模式,允许客户端和服务器端各自自定义方法并且相互调用,对开发者来说相当友好. 该节包括的 ...

随机推荐

  1. Hbase的JavaAPI和数据存储

    导入Maven依赖 <dependencies> <dependency> <groupId>org.apache.zookeeper</groupId> ...

  2. celery笔记四之在Django中使用celery

    本文首发于公众号:Hunter后端 原文链接:celery笔记四之在Django中使用celery 这一篇笔记介绍一下如何在 Django 系统中使用 celery. 如果是想纯粹使用 celery, ...

  3. [ARM 汇编]高级部分—性能优化与调试—3.4.3 使用模拟器进行调试与测试

    在ARM汇编程序开发过程中,使用模拟器(emulator)进行调试和测试是一种非常有效的方法.模拟器可以在不同的处理器上测试代码,帮助我们发现潜在的问题,并提供丰富的调试功能.本节将介绍如何使用QEM ...

  4. 使用部分写时复制提升Lakehouse的 ACID Upserts性能

    使用部分写时复制提升Lakehouse的 ACID Upserts性能 译自:Fast Copy-On-Write within Apache Parquet for Data Lakehouse A ...

  5. redis雪崩问题解决

    缓存雪崩 出现的场景 缓存服务器宕机,没有设置持久化 介绍:缓存服务器宕机,没有设置持久化,导致缓存数据全部丢失,请求全部转发到数据库,造成数据库短时间内承受大量请求而崩掉. 缓存集中失效 缓存的ke ...

  6. 分享一个Java功能小案例(代码已开源哦)

    工程合集 工程列表 地址预览 B站 抖音

  7. ZEGO全新语音聊天室方案,2小时复刻 Clubhouse

    真的火了! 新晋带货王马斯克在 Clubhouse"开房"之后,直接让 Clubhouse 爆火出圈,据说,Clubhouse 平台邀请码现在在ebay上已经卖到了快200刀一个. ...

  8. Auto.Core

    Auto Core (基于AspectCore) 介绍 AutoCore是基于 .Net Standard 2.1用于简化 ASP.NET Core开发,AutoCore 在 AspectCore 的 ...

  9. Flutter系列文章-Flutter进阶

    在前两篇文章中,我们已经了解了Flutter的基础知识,包括Flutter的设计理念.框架结构.Widget系统.基础Widgets以及布局.在本文中,我们将进一步探讨Flutter的高级主题,包括处 ...

  10. 2021-8-2 Mysql个人练习题

    创建学生表 CREATE TABLE student( id int, uname VARCHAR(20), chinese FLOAT, english FLOAT, math FLOAT ); I ...