LeetCode 周赛上分之旅 #49 再探内向基环树

⭐️ 本文已收录到 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 i < j < k i<j<k 的三位数;
  • 计算结果: ( n u m s [ i ] − n u m s [ j ] ) ∗ n u m s [ k ] (nums[i] - nums[j]) * nums[k] (nums[i]nums[j])nums[k]

思考实现:

思考优化:

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

  • 固定 j j j 为了让结果更大,应该找到 n u m s [ j ] nums[j] nums[j] 左边最大的 n u m s [ i ] nums[i] nums[i] 和右边最大的 n u m s [ k ] nums[k] nums[k] 组合,时间复杂度是 O ( n 2 ) O(n^2) O(n2)。我们也可以使用前后缀分解预处理出来,这样时间复杂度就是 O ( n ) O(n) O(n)
  • 固定 k k k 同理,固定 k k k 寻找应该找到左边使得 n u m s [ i ] − n u m s [ j ] nums[i] - nums[j] 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(n^3) O(n3)
  • 空间复杂度: O ( 1 ) O(1) O(1)

题解二(前后缀分解)

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

复杂度分析:

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

题解三(线性遍历)

线性遍历 n u m s [ k ] nums[k] nums[k] 并记录 ( n u m s [ i ] − n u m s [ 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;
    }
};

复杂度分析:

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

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

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

问题分析

n u m s nums nums 数组的整体元素和为 s s s,考虑 t a r g e t target target 的两种情况:

  • 对于 t a r g e t target target 很小的情况(小于数组整体和 s s s):这是很简单的滑动窗口问题;
  • 对于 t a r g e t target target 较大的情况(大于等于数组的整体和 s s s):那么最小长度中一定包含整数倍的 s s s,以及某个 n u m s nums 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 ) O(n) O(n) 最大扫描 2 2 2 倍数组长度;
  • 空间复杂度:仅使用常量级别空间。

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

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

问题分析

初步分析:

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

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

图片不记得出处了~

思考实现:

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

题解(拓扑排序 + DFS)

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

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

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

在拓扑排序后,树链上节点的入度都是 0 0 0,因此入度大于 0 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 ) O(n) O(n) 拓扑排序和 DFS 都是线性时间;
  • 空间复杂度: O ( n ) O(n) O(n) 图空间和队列空间。

题解二(朴素 DFS)

思路参考小羊的题解。

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

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

  • 环上节点:刚好走过基环;
  • 树链节点:走过树链 + 基环。
  • 还有 1 1 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 ) O(n) O(n) DFS 都是线性时间;
  • 空间复杂度: O ( n ) O(n) O(n) 图空间和队列空间。

推荐阅读

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

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

力扣(合并区间) 给定一组可能重叠或相邻的整数区间,合并所有重叠或端点相接的区间,返回不重叠的最小区间列表。例如输入 [[1,3],[2,6],[8,10],[15,18]],输出 [[1,6],[8,10],[15,18]]。核心是先按左端点升序排序,再单次线性扫描完成合并,避免暴力两两比较。 阅读详情

相关推荐

LeetCode 155. 最小栈|Python 解法详解

这道题的关键是:使用主栈保存数据,辅助栈在每一层同步保存“截至当前位置的最小值”。理解这一点后,再结合边界条件检查,代码就能保持清晰且稳定。

a1250467048的博客 207

LeetCode //MySQL - 1204. Last Person to Fit in the Bus

This SQL query solves the problem of finding the last person who can board a bus without exceeding the 1000 kg weight limit. The solution involves: Calculating a running total of weights ordered by boarding turn using a window function Filtering to only in

Made in Code 359

LeetCode 1. 两数之和 | 从暴力枚举到哈希表最优解 + 空间换时间思想全复盘

文章摘要 题目要求在数组中找到两数之和等于目标值的下标。核心思路是优化查找效率: 暴力法:双重循环枚举所有数对,时间复杂度O(n²),效率低但易实现。 哈希表优化:利用哈希表O(1)查找特性,遍历时存储已访问元素,每次计算差值并查询,时间复杂度降至O(n)。关键细节是先查后存,避免自匹配。 代码实现:哈希表法通过一次遍历完成,空间换时间,边界处理需注意空数组和不足两元素的情况。 常见错误:内层循环起始点、哈希表键值设计、立即返回结果等易错点需警惕。 面试要点:优先选择哈希表解法,解释与暴力法的差异,强调时间

yc2284238579的博客 230

leetcode】153. 寻找旋转排序数组中的最小值js

本文介绍了在旋转排序数组中寻找最小值的二分查找算法。通过比较中间元素与数组末尾元素,可以确定最小值所在区间:若中间值大于末尾值,则最小值在右侧;否则在左侧或当前位置。文章提供了两种实现方式,第一种通过样例总结返回nums[l],第二种采用更标准的二分写法,确保循环结束时left==right。两种方法时间复杂度均为O(logn),适用于解决LeetCode相关题目。关键点在于利用旋转数组的特性缩小搜索范围。

2402_83244812的博客 146

LeetCode //C - 1203. Sort Items by Groups Respecting Dependencies

This problem involves sorting items with group constraints and dependency relations. The key steps are: Assign unique groups to items with no group (-1) Build dependency graphs for both items and groups Perform topological sorting on both graphs If both so

Made in Code 367

LeetCode hot100——最大子数组和

给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。是数组中的一个连续部分。6连续子数组 [4,-1,2,1] 的和最大,为 6。nums = [1]123如果你已经实现复杂度为O(n)的解法,尝试使用更为精妙的求解。

weixin_42927227的博客 207

10.LeetCode算法习题讲解--滑动窗口--最大连续为一的个数

利用滑动窗口解决最大连续1的个数

Jammy_1的博客 209

LeetCode 3597.分割字符串

时间复杂度:O(n) 或 O(n∣Σ∣),其中 n 是 s 的长度,∣Σ∣=26 是字符集合的大小。空间复杂度:O(n) 或 O(n∣Σ∣)。只要当前段是唯一的,就将其加入段列表,标记为已经出现过,并从下一个下标开始构建新的段。因此,最终输出为 [“a”, “b”, “bc”, “c”, “cc”, “d”]。输出: [“a”,“b”,“bc”,“c”,“cc”,“d”]因此,最终输出为 [“a”, “aa”]。输入: s = “abbccccd”输入: s = “aaaa”输出: [“a”,“aa”]

tus00000的博客 221

栈和队列专题(三):LeetCode 622. 设计循环队列|数组环形复用 + front/rear 边界详解

本文详细讲解了循环队列的设计与实现,重点分析了如何利用数组实现环形结构以避免空间浪费。文章首先通过示例说明普通数组队列的"假溢出"问题,然后提出循环队列的解决方案。核心内容包括:1. 采用牺牲一个存储位置的方案区分队列空满状态;2. 明确front和rear指针的语义(front指向队首元素,rear指向下一个可插入位置);3. 使用取模运算实现下标的循环移动。文章完整实现了LeetCode 622题要求的全部接口,并分析了各种边界情况,特别是队尾元素的获取方法。最后对比了数组实现与链式

Luminousone的博客 432

LeetCode //C - 1202. Smallest String With Swaps

Summary: This problem involves finding the lexicographically smallest string achievable by swapping characters at given indices any number of times. The key idea is to use Union-Find to group all swappable indices into connected components. Characters with

Made in Code 345

LeetCode 28. 找出字符串中第一个匹配项的下标(暴力匹配 + KMP 算法详解)

本文介绍了LeetCode 28题"找出字符串中第一个匹配项的下标"的两种解法:暴力匹配和KMP算法。暴力匹配通过逐个字符比较实现,时间复杂度O(m×n);KMP算法利用next数组跳过不必要匹配,时间复杂度O(m+n)。文章详细解释了KMP的核心思想,包括next数组的定义和构建过程,并提供了Python和Java的代码实现。两种方法各具特点:暴力匹配简单直观,KMP更高效但实现复杂,适合处理长字符串匹配问题。

2603_95532426的博客 38

leetcode】33.搜索旋转排序数组js

本文介绍了在旋转排序数组中查找目标值的二分搜索算法。通过分析四种可能情况(mid值和目标值分别位于左右两段的不同组合),将问题转化为常规二分搜索或特殊指针移动。当mid值和目标值位于同一段时按常规二分处理;位于不同段时根据相对位置调整搜索方向。算法时间复杂度为O(log n),适用于部分有序数组的快速查找。文章附有JavaScript实现代码和情况分类表格,清晰展示了处理逻辑。

2402_83244812的博客 307

LeetCode算法题精讲】二叉搜索树(BST)精讲:验证二叉搜索树 + 二叉搜索树的最近公共祖先

摘要: 本文深入解析二叉搜索树(BST)的核心特性——有序性,通过LeetCode 98(验证BST)和235(BST最近公共祖先)两道经典题目,揭示BST的解题逻辑。98题强调全局有序性,需通过中序遍历或递归区间法验证;235题则利用BST有序性实现高效自顶向下搜索。文章对比了BST与普通二叉树的操作差异,提供Python/Java/C++三语言代码实现,并延伸至BST家族题(搜索、插入、删除、第K小元素)。核心结论:BST的有序性是其高效操作的基础,理解并应用这一特性是解决相关问题的关键。

2401_88352165的博客 367

LeetCode 34:在排序数组中查找元素的第一个和最后一个位置(二分查找) —— 题解

二分查找经典题目——在排序数组中查找元素的第一个和最后一个位置

2601_96587588的博客 707

LeetCode 142. 环形链表 II|Python 解法详解

这道题的关键是:快慢指针相遇后,根据距离关系,从头节点和相遇点各放一个同速指针,它们再次相遇的位置就是环入口。理解这一点后,再结合边界条件检查,代码就能保持清晰且稳定。

a1250467048的博客 35

leetcode 240搜索二维矩阵

所以每比较一次,都能排除一整行或一整列。,说明当前这一列下面的数只会更大,所以。表示只要还没有走出矩阵,就继续寻找。更小,所以当前这一行不可能找到。:最后一个元素的后一个位置。因为这一行左边的数字只会比。表示直接读取矩阵中的一行,:表示这个位置上的值。:第一个元素的位置;

qq_63176310的博客 235

LeetCode hot 100 —41. 缺失的第一个正数

普通数组-5-困难

Canmido的博客 223

关于字符串【力扣151.反转字符串中的单词的思考】

【摘要】本文介绍了字符串反转问题的解决方案。通过三个关键步骤实现:首先去除多余空格,然后整体反转字符串,最后逐个反转单词。方法强调函数封装思想,将不同功能模块化处理。注意事项指出需特别关注函数封装在代码复用和维护中的重要性,这种分步处理方法能有效提高代码可读性和执行效率。

wasb666的博客 203

关于字符串【力扣541.反转字符串II的思考】

本文总结了编程题解中的关键要点:1)处理跳跃操作时可通过for循环条件控制;2)非专门考察反转逻辑时优先使用库函数。特别提醒使用reverse()函数时需注意其左闭右开区间特性(如reverse(begin,begin+3)反转前3个元素),且需引入<algorithm>头文件。底层原理上,C++的reverse()通过双向迭代器交换元素实现反转,通常具有线性时间复杂度。

wasb666的博客 49
上一篇: LeetCode 周赛上分之旅 #48 一道简单的树上动态规划问题
彭旭锐
博客等级 码龄12年 5913粉丝 · 122原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彭旭锐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值