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

大家好,我是小彭。

上周末是 LeetCode 第 100 场双周赛,你参加了吗?这场周赛整体没有 Hard 题,但是也没有 Easy 题。第一题国服前百名里超过一半人 wa,很少见。


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



周赛概览

  • 2591. 将钱分给最多的儿童(Easy)

    • 题解一:模拟 $O(1)$
    • 题解二:完全背包 $O(children·money^2)$
  • 2592. 最大化数组的伟大值(Medium)
    • 题解一:贪心 / 田忌赛马 $O(nlgn)$
    • 题解二:最大重复计数 $O(n)$
  • 2593. 标记所有元素后数组的分数(Medium)
    • 题解一:排序 O$(nlgn)$
    • 题解二:按照严格递减字段分组 $O(n)$
  • 2594. 修车的最少时间(Medium)
    • 题解一:二分查找 $O(n + U·log(mc^2))$
    • 题解二:二分查找 + 计数优化 $O(n·log(mc^2))$

2591. 将钱分给最多的儿童(Easy)

题目地址

https://leetcode.cn/problems/distribute-money-to-maximum-children/description/

题目描述

给你一个整数 money ,表示你总共有的钱数(单位为美元)和另一个整数 children ,表示你要将钱分配给多少个儿童。

你需要按照如下规则分配:

  • 所有的钱都必须被分配。
  • 每个儿童至少获得 1 美元。
  • 没有人获得 4 美元。

请你按照上述规则分配金钱,并返回 最多 有多少个儿童获得 恰好 **8 美元。如果没有任何分配方案,返回 -1 。

题解一(模拟)

这道题搞数字迷信?发发发 888?

简单模拟题,但是错误率很高,提交通过率仅 19%。

  1. class Solution {
  2. fun distMoney(money: Int, children: Int): Int {
  3. var left = money
  4. // 每人至少分配 1 元
  5. left -= children
  6. // 违反规则 2
  7. if (left < 0) return -1
  8. // 1、完美:正好所有人可以分配 8 元
  9. if (left == children * 7) return children
  10. // 2、溢出:所有人可以分配 8 元有结余,需要选择 1 个人分配结余的金额
  11. if (left > children * 7) return children - 1
  12. // 3、不足:尽可能分配 8 元
  13. var sum = left / 7
  14. // 结余金额
  15. left -= sum * 7
  16. // 如果结余 3 元,并且剩下 1 人分配了 1 元,需要破坏一个 8 元避免出现分配 4 美元
  17. if (left == 3 && children - sum == 1) return sum - 1
  18. return sum
  19. }
  20. }

复杂度分析:

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

题解二(完全背包问题)

竞赛中脑海闪现过背包问题的思路,但第一题暴力才是王道,赛后验证可行。

  • 包裹:最多有 children 人;
  • 物品:每个金币价值为 1 且不可分割,最多物品数为 money 个;
  • 目标:包裹价值恰好为 8 的最大个数;
  • 限制条件:不允许包裹价值为 4,每个包裹至少装 1 枚金币。

dp[i][j] 表示分配到 i 个人为止,且分配总金额为 j 元时的最大价值,则有:

  • 递推关系:

$$

dp[i][j]=\sum_{k=1}^{j,k!=4} dp[i-1][j-k] + I(k==8)

$$

  • 初始状态 dp[0][0] = 0
  • 终止状态 dp[children][money]
  1. class Solution {
  2. fun distMoney(money: Int, children: Int): Int {
  3. var left = money
  4. // 每人至少分配 1 元
  5. left -= children
  6. // 违反规则 2
  7. if (left < 0) return -1
  8. val dp = Array(children + 1) { IntArray(left + 1) { -1 } }
  9. dp[0][0] = 0
  10. // i:枚举包裹
  11. for (i in 1..children) {
  12. // j:枚举金额
  13. for (j in 0..left) {
  14. // k:枚举选项
  15. for (k in 0..j) {
  16. // 不允许选择 3
  17. if (k == 3) continue
  18. // 子状态违反规则
  19. if (-1 == dp[i - 1][j - k]) continue
  20. // 子状态 + 当前包裹状态
  21. val cnt = dp[i - 1][j - k] + if (k == 7) 1 else 0
  22. dp[i][j] = Math.max(dp[i][j], cnt)
  23. }
  24. }
  25. }
  26. return dp[children][left]
  27. }
  28. }

滚动数组优化:

  1. class Solution {
  2. fun distMoney(money: Int, children: Int): Int {
  3. var left = money
  4. // 每人至少分配 1 元
  5. left -= children
  6. // 违反规则 2
  7. if (left < 0) return -1
  8. val dp = IntArray(left + 1) { -1 }
  9. dp[0] = 0
  10. // i:枚举包裹
  11. for (i in 1..children) {
  12. // j:枚举金额
  13. for (j in left downTo 0) {
  14. // k:枚举选项
  15. for (k in 0..j) {
  16. // 不允许选择 3
  17. if (k == 3) continue
  18. // 子状态违反规则
  19. if (-1 == dp[j - k]) continue
  20. // 子状态 + 当前包裹状态
  21. val cnt = dp[j - k] + if (k == 7) 1 else 0
  22. dp[j] = Math.max(dp[j], cnt)
  23. }
  24. }
  25. }
  26. return dp[left]
  27. }

复杂度分析:

  • 时间复杂度:$O(children·money^2)$
  • 空间复杂度:$O(money)$

近期周赛背包问题:


2592. 最大化数组的伟大值(Medium)

题目地址

https://leetcode.cn/problems/maximize-greatness-of-an-array/

题目描述

给你一个下标从 0 开始的整数数组 nums 。你需要将 nums 重新排列成一个新的数组 perm 。

定义 nums 的 伟大值 为满足 0 <= i < nums.length 且 perm[i] > nums[i] 的下标数目。

请你返回重新排列 nums 后的 最大 伟大值。

题解一(贪心 / 田忌赛马)

贪心思路:田忌赛马,以下赛马策略最优:

  • 田忌的中等马对齐威王的下等马,田忌胜;
  • 田忌的上等马对齐威王的中等马,田忌胜;
  • 田忌的下等马对齐威王的下等马,齐威王胜。

回到本题,考虑一组贡献伟大值的配对 $(p, q)$,其中 $p < q$。由于越小的值越匹配到更大值,为了让结果最优,应该让 p 尽可能小,即优先匹配 nums 数组的较小值。那么 $q$ 如何选择呢?有 2 种策略:

  • 策略 1 - 优先匹配最大值:无法得到最优解,因为会消耗了较大的 q 值,可能导致部分 p 值无法匹配(如果田忌用上等马对齐威王的下等马,最终将是齐威王生出);
  • 策略 2- 优先匹配最接近的更大值:最优解,即田忌赛马策略,以 [1,1,1,2,3,3,5] 为例:
    • 初始状态 i = 0,j = 0;
    • i = 0,j = 0,无法贡献伟大值,j 自增 1(寻找最接近的更大值);
    • i = 0,j = 1, 无法贡献伟大值,j 自增 1;
    • i = 0,j = 2, 无法贡献伟大值,j 自增 1;
    • i = 0,j = 3, 贡献伟大值,j 自增 1,i 自增 1;
    • i = 1,j = 4, 贡献伟大值,j 自增 1,i 自增 1;
    • i = 2,j = 5, 贡献伟大值,j 自增 1,i 自增 1;
    • i = 3,j = 6, 贡献伟大值,j 自增 1,i 自增 1;
    • 退出循环,i = 4;正好等于伟大值 4。
  1. class Solution {
  2. fun maximizeGreatness(nums: IntArray): Int {
  3. nums.sort()
  4. // i:参与匹配的指针
  5. var i = 0
  6. for (num in nums) {
  7. // 贡献伟大值
  8. if (num > nums[i]) i++
  9. }
  10. return i
  11. }
  12. }

复杂度分析:

  • 时间复杂度:$O(nlgn + n)$ 排序 + 线性遍历,其中 $n$ 是 $nums$ 数组长度;
  • 空间复杂度:$O(lgn)$ 排序递归栈空间。

题解二(最大重复计数)

竞赛中从测试用例中观察到题解与最大重复数存在关系,例如:

  • 用例 [1,1,1,2,3,3,5]:最大重复数为 3,一个最优方案为 [2,3,3,5,x,x,x],最大伟大值为 7 - 3 = 4,其中 7 是数组长度;
  • 用例 [1,2,2,2,2,3,5]:最大重复数为 4,一个最优方案为 [2,3,5,x,x,x,x],最大伟大值为 7 - 4 = 3,其中 7 是数组长度;
  • 用例 [1,1,2,2,2,2,3,3,5],最大重复数为 4,一个最优方案为 [2,2,3,3,5,x,x,x,x],最大伟大值为 9 - 4 = 5,其中 9 是数组长度。

我们发现题目的瓶颈在于数字最大重复出现计数。最大伟大值正好等于 数组长度 - 最大重复计数。

如何证明?关键在于 i 指针和 j 指针的最大距离:

当 i 指针指向重复元素的首个元素时(例如下标为 0、2、6 的位置),j 指针必须移动到最接近的较大元素(例如下标为 2,6,8 的位置)。而 i 指针和 j 指针的最大错开距离取决于数组重复出现次数最多的元素,只要错开这个距离,无论数组后续部分有多长,都能够匹配上。

  1. class Solution {
  2. fun maximizeGreatness(nums: IntArray): Int {
  3. var maxCnt = 0
  4. val cnts = HashMap<Int, Int>()
  5. for (num in nums) {
  6. cnts[num] = cnts.getOrDefault(num, 0) + 1
  7. maxCnt = Math.max(maxCnt, cnts[num]!!)
  8. }
  9. return nums.size - maxCnt
  10. }
  11. }

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 $n$ 是 $nums$ 数组的长度;
  • 空间复杂度:$O(n)$ 其中 $n$ 是 $cnts$ 散列表空间。

2593. 标记所有元素后数组的分数(Medium)

题目地址

https://leetcode.cn/problems/find-score-of-an-array-after-marking-all-elements/

题目描述

给你一个数组 nums ,它包含若干正整数。

一开始分数 score = 0 ,请你按照下面算法求出最后分数:

  • 从数组中选择最小且没有被标记的整数。如果有相等元素,选择下标最小的一个。
  • 将选中的整数加到 score 中。
  • 标记 被选中元素,如果有相邻元素,则同时标记 与它相邻的两个元素 。
  • 重复此过程直到数组中所有元素都被标记。

请你返回执行上述算法后最后的分数。

题解一(排序)

这道题犯了大忌,没有正确理解题意。一开始以为 “相邻的元素” 是指未标记的最相邻元素,花了很多时间思考如何快速找到左右未标记的数。其实题目没有这么复杂,就是标记数组上的相邻元素。

如此这道题只能算 Medium 偏 Easy 难度。

  1. class Solution {
  2. fun findScore(nums: IntArray): Long {
  3. // 小顶堆(索引)
  4. val heap = PriorityQueue<Int>() { i1, i2 ->
  5. if (nums[i1] != nums[i2]) nums[i1] - nums[i2] else i1 - i2
  6. }.apply {
  7. for (index in nums.indices) {
  8. offer(index)
  9. }
  10. }
  11. var sum = 0L
  12. while (!heap.isEmpty()) {
  13. val index = heap.poll()
  14. if (nums[index] == 0) continue
  15. // 标记
  16. sum += nums[index]
  17. nums[index] = 0
  18. // 标记相邻元素
  19. if (index > 0) nums[index - 1] = 0
  20. if (index < nums.size - 1) nums[index + 1] = 0
  21. }
  22. return sum
  23. }
  24. }

复杂度分析:

  • 时间复杂度:$O(nlgn)$ 堆排序时间,其中 $n$ 是 $nums$ 数组长度;
  • 空间复杂度:$O(n)$ 堆空间。

题解二(按照严格递减字段分组)

思路参考:灵茶山艾府的题解

按照严格递减字段分组,在找到坡底后间隔累加 nums[i],nums[i - 2],nums[i - 4],并从 i + 2 开始继续寻找坡底。

  1. class Solution {
  2. fun findScore(nums: IntArray): Long {
  3. val n = nums.size
  4. var sum = 0L
  5. var i = 0
  6. while (i < nums.size) {
  7. val i0 = i // 坡顶
  8. while (i + 1 < n && nums[i] > nums[i + 1]) i++ // 寻找坡底
  9. for (j in i downTo i0 step 2) { // 间隔累加
  10. sum += nums[j]
  11. }
  12. i += 2 // i + 1 不能选
  13. }
  14. return sum
  15. }
  16. }

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 $n$ 是 $nums$ 数组的长度,每个元素最多访问 2 次;
  • 空间复杂度:$O(1)$ 只使用常数空间。

2594. 修车的最少时间(Medium)

题目地址

https://leetcode.cn/problems/minimum-time-to-repair-cars/

题目描述

给你一个整数数组 ranks ,表示一些机械工的 能力值 。ranksi 是第 i 位机械工的能力值。能力值为 r 的机械工可以在 r * n2 分钟内修好 n 辆车。

同时给你一个整数 cars ,表示总共需要修理的汽车数目。

请你返回修理所有汽车 最少 需要多少时间。

注意: 所有机械工可以同时修理汽车。

题解(二分查找)

我们发现问题在时间 t 上存在单调性:

  • 假设可以在 t 时间内修完所有车,那么大于 t 的时间都能修完;
  • 如果不能在 t 时间内修完所有车,那么小于 t 的时间都无法修完。

因此,我们可以用二分查找寻找 “可以修完的最小时间”:

  • 二分的下界:1;
  • 二分的上界:将所有的车交给能力值排序最高的工人,因为他的效率最高。
  1. class Solution {
  2. fun repairCars(ranks: IntArray, cars: Int): Long {
  3. // 寻找能力值排序最高的工人
  4. var minRank = Integer.MAX_VALUE
  5. for (rank in ranks) {
  6. minRank = Math.min(minRank, rank)
  7. }
  8. var left = 1L
  9. var right = 1L * minRank * cars * cars
  10. // 二分查找
  11. while (left < right) {
  12. val mid = (left + right) ushr 1
  13. if (check(ranks, cars, mid)) {
  14. right = mid
  15. } else {
  16. left = mid + 1
  17. }
  18. }
  19. return left
  20. }
  21. // return 能否在 t 时间内修完所有车
  22. private fun check(ranks: IntArray, cars: Int, t: Long): Boolean {
  23. // 计算并行修车 t 时间能修完的车(由于 t 的上界较大,carSum 会溢出 Int)
  24. var carSum = 0L
  25. for (rank in ranks) {
  26. carSum += Math.sqrt(1.0 * t / rank).toLong()
  27. }
  28. return carSum >= cars
  29. }
  30. }

复杂度分析:

  • 时间复杂度:$O(n·log(mc^2))$ 其中 $n$ 是 $ranks$ 数组长度,$m$ 是 $ranks$ 数组的最小值,$c$ 是车辆数量,二分的次数是 $O(log(mc^2))$,每次 $check$ 操作花费 $O(n)$ 时间;
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

题解二(二分查找 + 计数优化)

我们发现 $ranks$ 的取值范围很小,所有可以用计数优化每次 $check$ 操作的时间复杂度:

  1. class Solution {
  2. fun repairCars(ranks: IntArray, cars: Int): Long {
  3. // 寻找能力值排序最高的工人
  4. val cnts = IntArray(101)
  5. var minRank = Integer.MAX_VALUE
  6. for (rank in ranks) {
  7. minRank = Math.min(minRank, rank)
  8. cnts[rank]++
  9. }
  10. var left = 1L
  11. var right = 1L * minRank * cars * cars
  12. // 二分查找
  13. while (left < right) {
  14. val mid = (left + right) ushr 1
  15. if (check(ranks, cars, cnts, minRank, mid)) {
  16. right = mid
  17. } else {
  18. left = mid + 1
  19. }
  20. }
  21. return left
  22. }
  23. // return 能否在 t 时间内修完所有车
  24. private fun check(ranks: IntArray, cars: Int, cnts: IntArray, minRank: Int, t: Long): Boolean {
  25. // 计算并行修车 t 时间能修完的车(由于 t 的上界较大,carSum 会溢出 Int)
  26. var carSum = 0L
  27. for (rank in minRank..100) {
  28. if (cnts[rank] == 0) continue
  29. carSum += cnts[rank] * Math.sqrt(1.0 * t / rank).toLong()
  30. }
  31. return carSum >= cars
  32. }
  33. }

复杂度分析:

  • 时间复杂度:$O(n + U·log(mc^2))$ 其中 $n$ 是 $ranks$ 数组长度,$m$ 是 $ranks$ 数组的最小值,$U$ 是 $ranks$ 数组的取值范围,$c$ 是车辆数量,二分的次数是 $O(log(mc^2))$,每次 $check$ 操作花费 $O(U)$ 时间;
  • 空间复杂度:$O(U)$ $cnts$ 计数数组空间。

近期周赛二分查找题目:

这场周赛就这么多,我们下周见。

刷爆 LeetCode 双周赛 100,单方面宣布第一题最难的更多相关文章

  1. leetcode 双周赛9 进击的骑士

    一个坐标可以从 -infinity 延伸到 +infinity 的 无限大的 棋盘上,你的 骑士 驻扎在坐标为 [0, 0] 的方格里. 骑士的走法和中国象棋中的马相似,走 “日” 字:即先向左(或右 ...

  2. leetcode 双周赛9 找出所有行中最小公共元素

    给你一个矩阵 mat,其中每一行的元素都已经按 递增 顺序排好了.请你帮忙找出在所有这些行中 最小的公共元素. 如果矩阵中没有这样的公共元素,就请返回 -1. 示例: 输入:mat = [[,,,,] ...

  3. [每日一题2020.06.16] leetcode双周赛T3 5423 找两个和为目标值且不重叠的子数组 DP, 前缀和

    题目链接 给你一个整数数组 arr 和一个整数值 target . 请你在 arr 中找 两个互不重叠的子数组 且它们的和都等于 target .可能会有多种方案,请你返回满足要求的两个子数组长度和的 ...

  4. LeetCode双周赛#36

    1604. 警告一小时内使用相同员工卡大于等于三次的人 题目链接 题意 给定两个字符串数组keyName和keyTime,分别表示名字为keytime[i]的人,在某一天内使用员工卡的时间(格式为24 ...

  5. LeetCode双周赛#35

    1589. 所有排列中的最大和 #差分 #贪心 题目链接 题意 给定整数数组nums,以及查询数组requests,其中requests[i] = [starti, endi] .第i个查询求 num ...

  6. LeetCode双周赛#34

    5492. 分割字符串的方案数 #组合公式 #乘法原理 #区间分割 题目链接 题意 给定01二进制串\(s\),可将\(s\)分割为三个非空 字符串\(s_1,s_2,s_3\),即(\(s_1+s_ ...

  7. LeetCode双周赛#33 题解

    5480. 可以到达所有点的最少点数目 #贪心 题目链接 题意 给定有向无环图,编号从0到n-1,一个边集数组edges(表示从某个顶点到另一顶点的有向边),现要找到最小的顶点集合,使得从这些点出发, ...

  8. Leetcode 双周赛#32 题解

    1540 K次操作转变字符串 #计数 题目链接 题意 给定两字符串\(s\)和\(t\),要求你在\(k\)次操作以内将字符串\(s\)转变为\(t\),其中第\(i\)次操作时,可选择如下操作: 选 ...

  9. C#刷遍Leetcode面试题系列连载(4) No.633 - 平方数之和

    上篇文章中一道数学问题 - 自除数,今天我们接着分析 LeetCode 中的另一道数学题吧~ 今天要给大家分析的面试题是 LeetCode 上第 633 号问题, Leetcode 633 - 平方数 ...

  10. C# 刷遍 Leetcode 面试题系列连载(3): No.728 - 自除数

    前文传送门: C#刷遍Leetcode面试题系列连载(1) - 入门与工具简介 C#刷遍Leetcode面试题系列连载(2): No.38 - 报数 系列教程索引 传送门:https://enjoy2 ...

随机推荐

  1. php中self和$this还有parent的区别

    1.self代表类,$this代表对象 2.能用$this的地方一定使用self,能用self的地方不一定能用$this 3.parent只能调用静态属性,并且可以调用父类中公有和受保护的方法 静态的 ...

  2. Linux常用指令1

    1.文件和目录的相关指令: ·cd:打开 ·pwd:查看当前所在的目录 ·mkdir:新建目录 ·rmdir:删除目录 ·ls:文件和目录查看 ·cp:复制 ·rm:删除 ·mv:移动 2.文本文件内 ...

  3. 计算机网络复习小结(3)-IPv4

    IPv4分组 一个IP分组由首部和数据两部分组成,首部前一部分的长度固定,共20B,是所有IP分组必须具有的.在IP数据报首部中有三个关于长度的标记,一个是首部长度,一个是总长度,一个是片偏移,基本单 ...

  4. sql 错误问题

    message: ### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: SELECT command denie ...

  5. @Scheduled不执行

    今天发现@Scheduled不执行,注释掉netty的初始化事件就能正常执行了 原因是@PostConstruct是在主线程执行,@PostConstruct不能堵塞,堵塞会导致整个应用挂起不可用

  6. Android中的特殊权限

    AndroidManifest中定义的权限分为普通权限,危险权限和特殊权限. 普通权限指的是不会威胁到用户的安全和隐私的权限,只需要在AndroidManifest中声明一下就能直接使用. 危险权限指 ...

  7. JetPack Compose 入门还得是官方

    官方写的真不错! 和那些所谓"教程"比真的简单高效不罗嗦! 所以还得是官方! 使用 Jetpack Compose 更快地打造更出色的应用 https://developer.an ...

  8. Jmeter一、开源软件的崛起

    一.jmeter自身特点: 1.开源,轻量级,更适合自动化和持续集成. 2.学习难度大. 3.资料少.多英文. 二.性能测试工具选型的原则 1.成本: a.工具成本 b.学习成本 2.通信协议: a. ...

  9. Java8中Stream的用法

    Java8中Stream的用法 1.概述 Stream APl ( java.util.stream)把真正的函数式编程风格引入到Java中.这是目前为止对Java类库最好的补充,因为Stream A ...

  10. 05 HDFS Java API应用实例

    一.在Ubuntu系统中安装和配置Eclipse 二.利用hadoop 的java api,向HDFS写一个文件. 三.从HDFS读取一个文件的内容.