• LeetCode 周赛 335,纯纯手速场!


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

    大家好,我是小彭。

    昨晚是 LeetCode 第 335 场周赛,你参加了吗?这场周赛整体难度不高,有两道模板题,第三题和第四题应该调换一下位置。


    小彭的 Android 交流群 02 群来了,公众号回复 “加群” 加入我们~



    2582. 递枕头(Easy)

    题目地址

    https://leetcode.cn/problems/pass-the-pillow/

    题目描述

    n 个人站成一排,按从 1 到 n 编号。

    最初,排在队首的第一个人拿着一个枕头。每秒钟,拿着枕头的人会将枕头传递给队伍中的下一个人。一旦枕头到达队首或队尾,传递方向就会改变,队伍会继续沿相反方向传递枕头。

    • 例如,当枕头到达第 n 个人时,TA 会将枕头传递给第 n - 1 个人,然后传递给第 n - 2 个人,依此类推。

    给你两个正整数 n 和 time ,返回 t

    题解一(模拟)

    简单模拟题。

    class Solution {
        fun passThePillow(n: Int, time: Int): Int {
            var index = 1
            var flag = true
            for (count in 0 until time) {
                if (flag) {
                    if (++index == n) flag = !flag
                } else {
                    if (--index == 1) flag = !flag
                }
            }
            return index
        }
    }
    

    复杂度分析:

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

    题解二(数学)

    以 n = 4 为例,显然每 n - 1 次传递为一轮,则有 time % (n - 1) 分辨出奇数轮 / 偶数轮。其中偶数轮是正向传递,奇数轮是逆向传递。

    • 偶数轮:2 → 3 → 4,time = 1 时传递到 2 号;
    • 奇数轮:3 → 2 → 1。
    class Solution {
        fun passThePillow(n: Int, time: Int): Int {
            val mod = n - 1
            return if (time / mod % 2 == 0) {
                (time % mod) + 1
            } else {
                n - (time % mod)
            }
        }
    }
    

    复杂度分析:

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

    2583. 二叉树中的第 K 大层和(Medium)

    题目地址

    https://leetcode.cn/problems/kth-largest-sum-in-a-binary-tree/

    题目描述

    给你一棵二叉树的根节点 root 和一个正整数 k 。

    树中的 层和 是指 同一层 上节点值的总和。

    返回树中第 k 大的层和(不一定不同)。如果树少于 k 层,则返回 -1 。

    注意,如果两个节点与根节点的距离相同,则认为它们在同一层。

    题解(BFS + 堆)

    BFS 模板题,使用小顶堆记录最大的 k 个数。

    class Solution {
        fun kthLargestLevelSum(root: TreeNode?, k: Int): Long {
            if (null == root) return 0L
            val heap = PriorityQueue<Long>()
    
            // BFS
            val queue = LinkedList()
            queue.offer(root)
            while (!queue.isEmpty()) {
                var levelSum = 0L
                for (count in 0 until queue.size) {
                    val node = queue.poll()
                    levelSum += node.`val`
                    if (null != node.left) {
                        queue.offer(node.left)
                    }
                    if (null != node.right) {
                        queue.offer(node.right)
                    }
                }
                if (heap.size < k) {
                    heap.offer(levelSum)
                } else if (heap.peek() < levelSum) {
                    heap.poll()
                    heap.offer(levelSum)
                }
            }
    
            return if (heap.size >= k) heap.peek() else -1L
        }
    }
    

    复杂度分析:

    • 时间复杂度:$O(nlgk)$ 其中 $n$ 是节点数。二叉树每个节点最多入队一次,二叉树最大有 $n$ 层,小顶堆维护 $k$ 个数的时间复杂度为 $O(nlgk)$;
    • 空间复杂度:$O(n)$ 小顶堆空间 $O(k)$,递归栈空间最大 $O(n)$。

    2584. 分割数组使乘积互质(Medium)

    题目地址

    https://leetcode.cn/problems/split-the-array-to-make-coprime-products/

    题目描述

    给你一个长度为 n 的整数数组 nums ,下标从 0 开始。

    如果在下标 i 处 分割 数组,其中 0 <= i <= n - 2 ,使前 i + 1 个元素的乘积和剩余元素的乘积互质,则认为该分割 有效 。

    • 例如,如果 nums = [2, 3, 3] ,那么在下标 i = 0 处的分割有效,因为 2 和 9 互质,而在下标 i = 1 处的分割无效,因为 6 和 3 不互质。在下标 i = 2 处的分割也无效,因为 i == n - 1 。

    返回可以有效分割数组的最小下标 i ,如果不存在有效分割,则返回 -1 。

    当且仅当 gcd(val1, val2) == 1 成立时,val1 和 val2 这两个值才是互质的,其中 gcd(val1, val2) 表示 val1 和 val2 的最大公约数。

    题解(质因子分解)

    这道题是这场周赛中最复杂的题目,应该放在 T4。

    因为多个数相乘的结果会溢出(如果题目中存在 0 还会干扰),所以这道题不能用前后缀分解的思路。 比较容易想到的思路是做质因子分解:显然合法分割数点的左右两边不能有公共质因子,否则子集的乘积必然是非互质的。

    举个例子,在数组 [1, 2, 3, 2, 5] 中,将质因子 2 划分到不同子集的方案是错误的:

    • [1 | 2, 3, 2, 5]:错误分割
    • [1 , 2 | 3, 2, 5]:正确分割
    • [1 , 2, 3 | 2, 5]:正确分割
    • [1 , 2, 3, 2 | 5]:错误分割

    脑海中有闪现过状态压缩,但题目输入数据较大无法实现,只能有散列表记录质因子信息。因此我们的算法是:先对 nums 数组中的每个元素做质因数分解,然后枚举所有分割点,统计左右子集中质因子的出现次数。如果出现同一个质因子再左右子集中的出现次数同时大于 1,说明分割点不成立。

    class Solution {
        fun findValidSplit(nums: IntArray): Int {
            val n = nums.size
            // 质因子计数
            val leftCount = HashMap<Int, Int>()
            val rightCount = HashMap<Int, Int>()
            // 质因子分解
            val primeMap = HashMap<Int, HashSet<Int>>()
            for (num in nums) {
                // 对 num 做质因数分解
                primeMap[num] = HashSet<Int>()
                var x = num
                var prime = 2
                while (prime * prime <= x) {
                    if (x % prime == 0) {
                        // 发现质因子
                        primeMap[num]!!.add(prime)
                        rightCount[prime] = rightCount.getOrDefault(prime, 0) + 1
                        // 消除所有 prime 因子
                        while (x % prime == 0) x /= prime
                    }
                    prime++
                }
                if(x > 1) {
                    // 剩下的质因子
                    primeMap[num]!!.add(x)
                    rightCount[x] = rightCount.getOrDefault(x, 0) + 1 
                }
            }
            // 枚举分割点
            outer@ for (index in 0..n - 2) {
                for (prime in primeMap[nums[index]]!!) {
                    leftCount[prime] = leftCount.getOrDefault(prime, 0) + 1
                    rightCount[prime] = rightCount[prime]!! - 1
                }
                for ((prime, count) in leftCount) {
                    if (rightCount[prime]!! != 0) continue@outer
                }
                return index
            }
            return -1
        }
    }
    

    复杂度分析:

    • 时间复杂度:$O(n\sqrt{U}+n·m)$ 其中 $n$ 是 $nums$ 数组的长度,U 是数组元素的最大值,$m$ 是 $U$ 范围内的质数个数 $\frac{U}{logU}$ 。时间复杂度分为两部分,质因数分解占用 $O(n\sqrt{U})$,枚举分割点的每轮循环需要枚举所有质数,占用 $O(n·m)$;
    • 空间复杂度:$O(n·m + m)$ 质因子分解映射表和计数表。

    题解二(质因数分解 + 合并区间)

    思路来源:灵茶山艾符的题解

    统计每种质因子在数组中出现的起始位置 left 和终止位置 right,如果分割点位于 [left, right) 区间,那么左右两子集一定会存在公共质因子。

    因此我们的算法是:将质数的分布看成一个连续区间,按照区间起始位置对所有区间排序。遍历区间并维护最大区间终止位置 preEnd,如果当前区间与 preEnd 不连续,则说明以当前位置为分割点的方案不会拆分区间,也就找到目标答案。

    如果按照这个思路理解,这道题本质上和 55. 跳跃游戏 类似。

    class Solution {
        fun findValidSplit(nums: IntArray): Int {
            // 质因子区间 <首次出现位置,末次出现位置>
            val primeMap = HashMap<Int, IntArray>()
            // 质因数分解
            for ((index, num) in nums.withIndex()) {
                // 对 num 做质因数分解
                var x = num
                var prime = 2
                while (prime * prime <= x) {
                    if (x % prime == 0) {
                        // 发现质因子
                        primeMap.getOrPut(prime) { intArrayOf(index, index) }[1] = index
                        // 消除所有 prime 因子
                        while (x % prime == 0) x /= prime
                    }
                    prime++
                }
                if (x > 1) {
                    // 剩下的质因子
                    primeMap.getOrPut(x) { intArrayOf(index, index) }[1] = index
                }
            }
            // 区间排序
            val areaList = primeMap.values.toMutableList()
            Collections.sort(areaList) { e1, e2 ->
                e1[0] - e2[0]
            }
            // 枚举区间
            var preEnd = 0
            for (area in areaList) {
                if (area[0] > preEnd) return area[0] - 1
                preEnd = Math.max(preEnd, area[1])
            }
            return -1
        }
    }
    

    复杂度分析:

    • 时间复杂度:$O(n\sqrt{U}+mlgm+m)$ 质因数分解时间 $O(n\sqrt{U})$,排序时间 $O(mlgm)$,枚举区间时间 $O(m)$;
    • 空间复杂度:$O(m + lgm)$ 质因子区间数组占用 $O(m)$,排序递归栈空间 $O(lgm)$。

    题解三(合并区间 + 排序优化)

    题解二中的排序时间可以优化。

    由于我们是从前往后分解 nums 数组,每分解一个质因子 prime 时,它一定可以更新该质数区间的末次出现位置。所以我们不用等到最后再做一次区间排序,直接在做质因数分解时维护 preEnd。在题解二中,我们是从区间的维度维护 preEnd,现在我们直接从 nums 数组的维度维护 preEnd。

    class Solution {
        fun findValidSplit(nums: IntArray): Int {
            val n = nums.size
            // start[p] 表示质数 p 首次出现为止
            val start = HashMap<Int, Int>()
            // end[i] 表示以 i 为左端点的区间的最大右端点
            val end = IntArray(n)
            // 质因数分解
            for ((index, num) in nums.withIndex()) {
                // 对 num 做质因数分解
                var x = num
                var prime = 2
                while (prime * prime <= x) {
                    if (x % prime == 0) {
                        // 发现质因子
                        if (!start.containsKey(prime)) {
                            start[prime] = index
                        } else {
                            end[start[prime]!!] = index
                        }
                        // 消除所有 prime 因子
                        while (x % prime == 0) x /= prime
                    }
                    prime++
                }
                if (x > 1) {
                    // 剩下的质因子
                    if (!start.containsKey(x)) {
                        start[x] = index
                    } else {
                        end[start[x]!!] = index
                    }
                }
            }
            var preEnd = 0
            for (index in 0 until n) {
                if (index > preEnd) return index - 1
                preEnd = Math.max(preEnd, end[index])
            }
            return -1
        }
    }
    

    复杂度分析:

    • 时间复杂度:$O(n\sqrt{U}+m)$ 质因数分解时间 $O(n\sqrt{U})$,枚举数组时间 $O(n)$;
    • 空间复杂度:$O(n)$ $end$ 数组空间。

    2585. 获得分数的方法数(Hard)

    题目地址

    https://leetcode.cn/problems/number-of-ways-to-earn-points/

    题目描述

    考试中有 n 种类型的题目。给你一个整数 target 和一个下标从 0 开始的二维整数数组 types ,其中 types[i] = [counti, marksi] 表示第 i 种类型的题目有 counti 道,每道题目对应 marksi 分。

    返回你在考试中恰好得到 target 分的方法数。由于答案可能很大,结果需要对 109 +7 取余。

    注意,同类型题目无法区分。

    • 比如说,如果有 3 道同类型题目,那么解答第 1 和第 2 道题目与解答第 1 和第 3 道题目或者第 2 和第 3 道题目是相同的。

    题解(背包问题)

    这是分组背包模板题,OIWiki-背包 DP

    定义 $dp[i][j]$ 表示以物品 $[i]$ 为止且分数为 $j$ 的方案数,则有:

    $dp[i][j] = dp[i - 1][j] + \sum_{k=0}^{k=j/count_i}dp[i - 1][j - k*·marks_{si}]$

    class Solution {
        fun waysToReachTarget(target: Int, types: Array<IntArray>): Int {
            val MOD = 1000000007
            // 背包问题
            val n = types.size
            // dp[i][j] 表示以 [i] 为止且分数为 j 的方案数
            val dp = Array(n + 1) { IntArray(target + 1) }.apply {
                // 不选择且分数为 0 的方案数为 1
                this[0][0] = 1
            }
            // 枚举物品
            for (i in 1..n) {
                val count = types[i - 1][0]
                val mark = types[i - 1][1]
                for (j in target downTo 0) {
                    dp[i][j] += dp[i - 1][j]
                    for (k in 1..Math.min(count, j / mark)) {
                        dp[i][j] = (dp[i][j] + dp[i - 1][j - k * mark]) % MOD
                    }
                }
            }
            return dp[n][target]
        }
    }
    

    完全背包可以取消物品维度优化空间:

    class Solution {
        fun waysToReachTarget(target: Int, types: Array<IntArray>): Int {
            val MOD = 1000000007
            // 背包问题
            val n = types.size
            // dp[i][j] 表示以 [i] 为止且分数为 j 的方案数
            val dp = IntArray(target + 1).apply {
                // 不选择且分数为 0 的方案数为 1
                this[0] = 1
            }
            // 枚举物品
            for (i in 1..n) {
                val count = types[i - 1][0]
                val mark = types[i - 1][1]
                for (j in target downTo 0) {
                    for (k in 1..Math.min(count, j / mark)) {
                        dp[j] = (dp[j] + dp[j - k * mark]) % MOD
                    }
                }
            }
            return dp[target]
        }
    }
    

    复杂度分析:

    • 时间复杂度:$O(target·C)$ 其中 $C$ 是所有 $count_i$ 之和。
    • 空间复杂度:$O(target)$
  • 相关阅读:
    简述一下伪共享的概念以及如何避免
    XML配置文件
    DIV布局个人介绍网页模板代码 家乡海阳个人简介网页制作 简单个人静态HTML网页设计作品 DW个人网站制作成品 web网页制作与实现
    笔试刷题Day—1
    第一个Shader Graph
    C++学习 --queue
    VMware中安装Ubuntu(2023年)
    TCP、UDP API调用(实时聊天)
    实体链指(3)EL:End-to-End
    ShowDoc远程下载导致文件上传漏洞复现 CVE-2021-36440&CVE-2022-1034
  • 原文地址:https://www.cnblogs.com/pengxurui/p/17182106.html