首页 > 其他分享 >LeetCode 周赛上分之旅 #38 结合排序不等式的动态规划

LeetCode 周赛上分之旅 #38 结合排序不等式的动态规划

时间:2023-08-07 18:33:44浏览次数:51  
标签:周赛 38 val 复杂度 next nums2 上分 nums1 dp

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

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

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

双周赛 110

T1. 取整购买后的账户余额(Easy)

  • 标签:模拟

T2. 在链表中插入最大公约数(Medium)

  • 标签:链表、数学

T3. 使循环数组所有元素相等的最少秒数(Medium)

  • 标签:贪心、散列表

T4. 使数组和小于等于 x 的最少时间(Hard)

  • 标签:排序不等式、动态规划、贪心


T1. 取整购买后的账户余额(Easy)

https://leetcode.cn/problems/insert-greatest-common-divisors-in-linked-list/

题解(模拟)

阅读理解题。

其实就是将 purchaseAmount 向最近的 10 的倍数四舍五入,再用 100 减去它。

class Solution {
    fun accountBalanceAfterPurchase(purchaseAmount: Int): Int {
        return 100 - (purchaseAmount + 5) / 10 * 10 
    }
}

复杂度分析:

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

T2. 在链表中插入最大公约数(Medium)

https://leetcode.cn/problems/insert-greatest-common-divisors-in-linked-list/

题解(数学)

久违的链表题。

题目相对简单,其实就是依次处理每两个节点,并插入一个新的最大公约数节点。以下提供两个写法:

  • 构造新链表:
class Solution {
    fun insertGreatestCommonDivisors(head: ListNode?): ListNode? {
        val dummy = ListNode(-1)
        var rear = dummy
        var p = head
        while (null != p) {
            rear.next = p
            rear = p
            val next = p.next
            if (null != next) {
                val newNode = ListNode(gcb(p.`val`, next.`val`))
                newNode.next = next
                p.next = newNode
                rear.next = newNode
                rear = newNode
            }
            p = next
        }
        return dummy.next
    }

    private fun gcb(a:Int, b:Int) :Int {
        var x = a
        var y = b
        while (y != 0) {
            val temp = x % y
            x = y
            y = temp
        }
        return x
    }
}
  • 在原链表上插入:
class Solution {
    fun insertGreatestCommonDivisors(head: ListNode?): ListNode? {
        var p = head
        while (null != p?.next) {
            val next = p.next
            val newNode = ListNode(gcb(p.`val`, next.`val`))
            newNode.next = next
            p.next = newNode
            p = next
        }
        return head
    }

    private fun gcb(a:Int, b:Int) :Int {
        var x = a
        var y = b
        while (y != 0) {
            val temp = x % y
            x = y
            y = temp
        }
        return x
    }
}

复杂度分析:

  • 时间复杂度:$O(nlgU)$ 其中单次最大公约数的计算时间复杂度为 $O(lgU)$,U 为数值上界;
  • 空间复杂度:$O(1)$ 不考虑输出空间。

T3. 使循环数组所有元素相等的最少秒数(Medium)

https://leetcode.cn/problems/minimum-seconds-to-equalize-a-circular-array/

题解(贪心 + 散列表)

根据题目要求,我们可以通过将数字复制到相邻位置上,以实现数组中所有元素都相等。因此,如果我们选择数字 x 为最终元素,那么决定替换秒数的关键在与数组中不等于 x 的最长子数组长度。

所以,我们的算法是计算以每种数字 x 为目标的方案中,最短的不等于 x 的最长子数组长度,并除以 2 向上取整的到结果。

class Solution {
    fun minimumSeconds(nums: List<Int>): Int {
        // 最大间隔的最小值
        val n = nums.size
        // lens:记录每种数字的最长间隔
        val lens = HashMap<Int, Int>()
        // preIndexs:记录每种数字的上次出现位置
        val preIndexs = HashMap<Int, Int>()
        // 记录最后出现位置(环形数组逻辑)
        for ((i, e) in nums.withIndex()) {
            preIndexs[e] = i
        }
        for ((i, e) in nums.withIndex()) {
            lens[e] = Math.max(lens.getOrDefault(e, 0), (i - preIndexs[e]!! - 1 + n) % n)
            preIndexs[e] = i
        }
        var ret = n
        for ((_, len) in lens) {
            ret = Math.min(ret, (len + 1) / 2)
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:$O(n)$ 线性遍历;
  • 空间复杂度:$O(n)$ 散列表空间。

T4. 使数组和小于等于 x 的最少时间(Hard)

https://leetcode.cn/problems/minimum-time-to-make-array-sum-at-most-x/

题解(DP + 排序不等式)

  • 时间的上界: 假设题目的最少时间超过数组长度 n,那么根据抽屉原理必然有某个位置重复置零两次,那么第一次操作的贡献就丢失了,因此,题目的时间上界不应该超过 n,即每个位置最多置零一次;
  • 二分答案(X): 数组元素和小于等于 x 与操作时间 t 不具备单调性,因此不能使用二分答案的思路;
  • 逆向思维: 令 s1 = sum(nums1), s2 = sum(nums2),假设经过 t 时间且不进行任何操作,那么元素总和将变成 s1 + s2 *t。现在需要从 [0, n-1] 中非重复地选择 t 个位置,假设在第 x 秒选择位置 [i],那么对最终元素总和减少的贡献度为 nums1[i] + x·nums2[i]。
  • 排序不等式: 现在的问题是「选择哪些数」以及「如何分配选择时间」使得减少的贡献度尽可能大:假设选择位置 [i]、[j] 和 [k],那么贡献度为:
    • nums1[i] + nums2[i] * x
    • nums1[j] + nums2[j] * y
    • nums1[k] + nums2[k] * z
    • 无论如何分配,加法左边的贡献度是恒定的,问题关键在与如何使得加法右边的贡献度尽可能大;
    • 直观地观察,容易想到应该将元素值更大的元素分配到更靠后的位置上,使其置零时贡献更多;
    • 验证证明可以根据 排序不等式 ,假设有两组有序序列 a 和 b,每一项正序相乘并累加的和是最大的。
  • 动态规划(选哪个): 定义 dp[i][j] 表示到第 [i] 个元素为止操作 j 次时的最大贡献度
    • 目标:满足 dp[n][j] 小于等于 x 的最小 j 值
    • 状态转移方程(选和不选):$dp[i][j] = max{dp[i - 1][j], dp[i - 1][j - 1] + nums1[i] + nums2[i] * j}$
  • 排序: 将元素按照 nums2 正序排序,对于选择 [i] 位置且选择 j 次的方案,分配在第 j 次选择上的贡献度是最大的。
class Solution {
    fun minimumTime(nums1: List<Int>, nums2: List<Int>, x: Int): Int {
        val INF = -0x3F3F3F3F // 减少判断
        val n = nums1.size
        // 排序
        val ids = Array<Int>(n) {it}
        Arrays.sort(ids) {i1, i2 ->
            nums2[i1] - nums2[i2]
        } 
        // 动态规划
        val dp = Array(n + 1) { IntArray(n + 1) { INF }}
        dp[0][0] = 0 // 初始状态
        for (i in 1 .. n) { // 枚举物品
            for (j in 0 .. i) { // 枚举次数
                dp[i][j] = dp[i - 1][j]
                if (j > 0) dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + nums1[ids[i - 1]] + nums2[ids[i - 1]] * j)
            }
        }
        // println(dp[n].joinToString())
        // 输出
        val s1 = nums1.sum()
        val s2 = nums2.sum()
        for (t in 0 .. n) {
            if (s1 + s2 * t - dp[n][t] <= x) return t
        }
        return -1
    }
}

滚动数组优化:

class Solution {
    fun minimumTime(nums1: List<Int>, nums2: List<Int>, x: Int): Int {
        val INF = -0x3F3F3F3F // 减少判断
        val n = nums1.size
        // 排序
        val ids = Array<Int>(n) {it}
        Arrays.sort(ids) {i1, i2 ->
            nums2[i1] - nums2[i2]
        } 
        // 动态规划
        val dp = IntArray(n + 1) { INF }
        dp[0] = 0 // 初始状态
        for (i in 1 .. n) { // 枚举物品
            for (j in i downTo 1) { // 枚举次数(逆序)
                dp[j] = Math.max(dp[j], dp[j - 1] + nums1[ids[i - 1]] + nums2[ids[i - 1]] * j)
            }
        }
        // println(dp[n].joinToString())
        // 输出
        val s1 = nums1.sum()
        val s2 = nums2.sum()
        for (t in 0 .. n) {
            if (s1 + s2 * t - dp[t] <= x) return t
        }
        return -1
    }
}

复杂度分析:

  • 时间复杂度:$O(n^2)$ 其中排序时间为 $O(nlgn)$,动态规划时间为 $O(n^2)$;
  • 空间复杂度:$O(n)$ DP 数组空间。

推荐阅读

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

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

标签:周赛,38,val,复杂度,next,nums2,上分,nums1,dp
From: https://www.cnblogs.com/pengxurui/p/17612232.html

相关文章

  • 算法练习-day38
    动态规划121.买卖股票的最佳时机题意:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中......
  • LeetCode 周赛上分之旅 # 37 多源 BFS 与连通性问题
    ⭐️本文已收录到AndroidFamily,技术和职场问题,请关注公众号[彭旭锐]和BaguTreePro知识星球提问。学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场LeetCode周赛的解题报告,一......
  • 【W的AC企划 - 第六期】树上分治
    往期浏览树上分治点分治讲解每次选取树的重心进行递归分治,中阶算法,大部分模板不需要理解,但是每一题都需要对维护函数进行修改。复杂度\(\mathcalO(N\logN)\)。个人封装由于需要进行一定程度的修改,不符合结构体封装的原则,故没有使用结构体。introot=0,MaxTree=1e1......
  • c/cpp: g++ 设置(fedora38)
    c/cpp: g++设置(fedora38)    一、基本配置信息[wit@fedoranull]$cat/etc/bashrc#/etc/bashrc#Systemwidefunctionsandaliases#Environmentstuffgoesin/etc/profile#It'sNOTagoodideatochangethisfileunlessyouknowwhatyou#ared......
  • Leetcode第357场周赛
    https://leetcode.cn/contest/weekly-contest-357/C寻找不安全路径以所有小偷点为源点,跑多源点BFS,求出每个点到最近小偷点的曼哈顿距离,记为w[i,j]二分答案Mid,只允许走w[i,j]>=mid的点,从源店跑DFS/BFS,看是否能抵达汇点。D子序列最大优雅度反悔贪心,首先将所有项目按照利......
  • 357周赛
    故障键盘水题。classSolution{public:stringfinalString(strings){stringres;for(charc:s){if(c=='i'){reverse(res.begin(),res.end());}else{res+=c;......
  • 熊猫B7PRO主板3865U3965U输出HDMI音频及视频解码能力
    之前有人咨询过,最近又有一些人咨询,经常发嘛太麻烦,找了个以前截的图。如图采用的USBHDMI采集卡,名称为HDMITOUSB,如果是普通显示器或者电视机,会显示正常的显示器名称之类的。有一些人连接了之后,并没有这个设备,就是因为没有BIOS启用功能。BIOS的设置https://blog.51cto.com/infrado/......
  • 【动态规划】【力扣357次周赛】6953. 判断是否能拆分数组
    【力扣357次周赛】6953.判断是否能拆分数组给你一个长度为n的数组nums和一个整数m。请你判断能否执行一系列操作,将数组拆分成n个非空数组。在每一步操作中,你可以选择一个长度至少为2的现有数组(之前步骤的结果)并将其拆分成2个子数组,而得到的每个子数组,至少需......
  • MySQL之InnoDB存储结构 转载 https://juejin.cn/post/7253816086679846972
    1InnoDB存储引擎InnoDB存储引擎最早由InnobaseOy公司开发(属第三方存储引擎)。从MySQL5.5版本开始作为表的默认存储引擎。该存储引擎是第一个完整支持ACID事务的MySQL存储引擎,特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,非常适合OLTP场景的应用使用。目前也是应用......
  • 代码随想录算法训练营第六天|力扣454.四数相加II、力扣383.赎金信、力扣15.三数之和、
    四数相加II(力扣454.)前两个数组的值直接遍历,并将和存入map中,key为和,value为出现次数后两个数组再次遍历,在map中寻找是否存在0-(c+d),若存在,count+=valuefor(a:A){//遍历ABfor(b:B){map[a+b]++;}}//insert操作for(c:C){for(d:D){target=0-(c+d);if(map.containsKey(t......