LeetCode 周赛上分之旅 #39 结合中心扩展的单调栈贪心问题

news2024/12/26 0:46:43

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

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

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

周赛 358

T1. 数组中的最大数对和(Easy)

  • 标签:数学、分桶

T2. 翻倍以链表形式表示的数字(Medium)

  • 标签:链表

T3. 限制条件下元素之间的最小绝对差(Medium)

  • 标签:双指针、平衡树

T4. 操作使得分最大(Hard)

  • 标签:贪心、排序、中心扩展、单调栈、快速幂


T1. 数组中的最大数对和(Easy)

https://leetcode.cn/problems/max-pair-sum-in-an-array/

题解一(分桶 + 数学)

  • 枚举每个元素,并根据其最大数位分桶;
  • 枚举每个分桶,计算最大数对和。
class Solution {
public:
    int maxSum(vector<int>& nums) {
        int U = 10;
        // 分桶
        vector<int> buckets[U];
        for (auto& e: nums) {
            int x = e;
            int m = 0;
            while (x > 0) {
                m = max(m, x % 10);
                x /= 10;
            }
            buckets[m].push_back(e);
        }
        // 配对
        int ret = -1;
        for (int k = 0; k < U; k++) {
            if (buckets[k].size() < 2) continue;
            sort(buckets[k].rbegin(), buckets[k].rend());
            ret = max(ret, buckets[k][0] + buckets[k][1]);
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度: O ( n l g n ) O(nlgn) O(nlgn) 瓶颈在排序,最坏情况下所有元素进入同一个分桶;
  • 空间复杂度: O ( n ) O(n) O(n) 分桶空间;

题解二(一次遍历优化)

  • 最大数对和一定是分桶中的最大两个数,我们只需要维护每个分桶的最大值,并在将新元素尝试加入分桶尝试更新结果。
class Solution {
public:
    int maxSum(vector<int>& nums) {
        int U = 10;
        int ret = -1;
        int buckets[U];
        memset(buckets, -1, sizeof(buckets));
        for (auto& e: nums) {
            int x = e;
            int m = 0;
            while (x > 0) {
                m = max(m, x % 10);
                x /= 10;
            }
            if (-1 != buckets[m]) {
                ret = max(ret, buckets[m] + e);
            }
            buckets[m] = max(buckets[m], e);
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历;
  • 空间复杂度: O ( U ) O(U) O(U) 分桶空间。

T2. 翻倍以链表形式表示的数字(Medium)

https://leetcode.cn/problems/double-a-number-represented-as-a-linked-list/

题解一(模拟)

面试类型题,有 O ( 1 ) O(1) O(1) 空间复杂度的写法:

  • 先反转链表,再依次顺序翻倍,最后再反转回来;
  • 需要注意最后剩余一个进位的情况需要补足节点。
class Solution {
    fun doubleIt(head: ListNode?): ListNode? {
        // 反转
        val p = reverse(head)
        // 翻倍
        var cur = p
        var append = 0
        while (cur != null) {
            append += cur.`val` * 2
            cur.`val` = append % 10
            append = append / 10
            cur = cur.next
        }
        // 反转
        if (0 == append) return reverse(p)
        return ListNode(append).apply {
            next = reverse(p)
        }
    }
    
    fun reverse(head: ListNode?): ListNode? {
        var p: ListNode? = null
        var q = head
        while (null != q) {
            val next = q.next
            q.next = p
            p = q
            q = next
        }
        return p
    }
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 反转与翻倍是线性时间复杂度;
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

题解二(一次遍历优化)

我们发现进位只发生在元素值大于 4 的情况,我们可以提前观察当前节点的后继节点的元素值是否大于 4,如果是则增加进位 1。特别地,当首个元素大于 4 时需要补足节点。

class Solution {
    fun doubleIt(head: ListNode?): ListNode? {
        if (head == null) return null
        // 补足
        val newHead = if (head.`val` > 4) {
            ListNode(0).also { it.next = head}
        } else {
            head
        }
        // 翻倍
        var cur: ListNode? = newHead
        while (null != cur) {
            cur.`val` *= 2
            if ((cur?.next?.`val` ?: 0) > 4) cur.`val` += 1
            cur.`val` %= 10
            cur = cur.next
        }
        return newHead
    }
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历;
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

相似题目:

  • 445. 两数相加 II

T3. 限制条件下元素之间的最小绝对差(Medium)

https://leetcode.cn/problems/minimum-absolute-difference-between-elements-with-constraint/

题解(双指针 + 平衡树 )

  • 滑动窗口的变型题,常规的滑动窗口是限定在窗口大小在 x 内,而这道题是排除到窗口外。万变不离其宗,还得是双指针。
  • 其次,为了让元素配对的差值绝对值尽可能小,应该使用与其元素值相近最大和最小的两个数,可以用平衡树在 O(lgn) 时间复杂度内求得,整体时间复杂度是 O(ngln);
class Solution {
    fun minAbsoluteDifference(nums: List<Int>, x: Int): Int {
        if (x == 0) return 0 // 特判
        var ret = Integer.MAX_VALUE
        val n = nums.size
        val set = TreeSet<Int>()
        for (i in x until n) {
            // 滑动
            set.add(nums[i - x])
            val q = set.floor(nums[i])
            val p = set.ceiling(nums[i])
            if (p != null) ret = Math.min(ret, Math.abs(p - nums[i]))
            if (q != null) ret = Math.min(ret, Math.abs(nums[i] - q))
        }
        return ret 
    }
}

复杂度分析:

  • 时间复杂度: O ( m l g m ) O(mlgm) O(mlgm) 其中 m = n - x,内层循环二分搜索的时间复杂度是 O ( l g m ) O(lgm) O(lgm)
  • 空间复杂度: O ( m ) O(m) O(m) 平衡树空间。

T4. 操作使得分最大(Hard)

https://leetcode.cn/problems/apply-operations-to-maximize-score/

题解一(贪心 + 排序 + 中心扩展 + 单调栈 + 快速幂)

这道题难度不算高,但使用到的技巧还挺综合的。

  • 阅读理解: 可以得出影响结果 3 点关键信息,我们的目标是选择 k 个子数组,让其中质数分数最大的元素 nums[i] 尽量大:

    • 1、元素大小
    • 2、元素的质数分数
    • 3、左边元素的优先级更高
  • 预处理: 先预处理数据范围内每个数的质数分数,避免在多个测试用例间重复计算;

  • 质因数分解: 求解元素的质数分数需要质因数分解,有两种写法:

    • 暴力写法,时间复杂度 O ( n ⋅ n ) O(n·\sqrt{n}) O(nn )

      val scores = IntArray(U + 1)
      for (e in 1 .. U) {
          var cnt = 0
          var x = e
          var prime = 2
          while (prime * prime <= x) {
              if (x % prime == 0) {
                  cnt ++
                  while (x % prime == 0) x /= prime // 消除相同因子
              }
              prime++
          }
          if (x > 1) cnt ++ // 剩余的质因子
          scores[e] = cnt
      }
      
    • 基于质数筛写法,时间复杂度 O(n):

      val scores = IntArray(U + 1)
      for (i in 2 .. U) {
          if (scores[i] != 0) continue // 合数
          for (j in i .. U step i) {
              scores[j] += 1
          }
      }
      
  • 排序: 根据关键信息 「1、元素大小」 可知,我们趋向于选择包含较大元素值的子数组,且仅包含数组元素最大值的子数组是子数组分数的上界;

  • 中心扩展: 我们先对所有元素降序排序,依次枚举子数组,计算该元素对结果的贡献,直到该元素无法构造更多子数组。以位置 i 为中心向左右扩展,计算左右两边可以记入子数组的元素个数 leftCnt 和 rightCnt。另外,根据 「左边元素的优先级更高」 的元素,向左边扩展时不能包含质数分数相同的位置,向右边扩展时可以包含;

  • 乘法原理: 包含元素 nums[i] 的子数组个数满足乘法法则(leftCnt * rightCnt);

  • 单调栈: 在中心扩展时,我们相当于在求 「下一个更大值」元素,这是典型的 单调栈问题,可以在 O ( n ) O(n) O(n) 时间复杂度内求得所有元素的下一个更大值;

    val stack = ArrayDeque<Int>()
    for (i in 0 until n) {
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
            stack.pop()
        }
        stack.push(i)
    }
    
  • 快速幂: 三种写法:

    • 暴力写法,时间复杂度 O(n),由于题目 k 比较大会超出时间限制:

      fun pow(x: Int, n: Int, mod: Int): Int {
          var ret = 1L
          repeat (n){
              ret = (ret * x) % mod
          }
          return ret.toInt()
      }
      
    • 分治写法,时间复杂度是 O(lgn):

      fun pow(x: Int, n: Int, mod: Int): Int {
          if (n == 1) return x
          val subRet = pow(x, n / 2, mod)
          return if (n % 2 == 1) {
              1L * subRet * subRet % mod * x % mod
          } else {
              1L * subRet * subRet % mod
          }.toInt()
      }
      
    • 快速幂写法,时间复杂度 O©:

      private fun quickPow(x: Int, n: Int, mod: Int): Int {
          var ret = 1L
          var cur = n
          var k = x.toLong()
          while (cur > 0) {
              if (cur % 2 == 1) ret = ret * k % mod
              k = k * k % mod
              cur /= 2
          }
          return ret.toInt()
      }
      

组合以上技巧:

class Solution {
    companion object {
        private val MOD = 1000000007
        private val U = 100000
        private val scores = IntArray(U + 1)
        
        init {
            // 质数筛
            for (i in 2 .. U) {
                if (scores[i] != 0) continue // 合数
                for (j in i .. U step i) {
                    scores[j] += 1
                }
            }
        }
    }
    
    fun maximumScore(nums: List<Int>, k: Int): Int {
        val n = nums.size
        // 贡献(子数组数)
        val gains1 = IntArray(n) { n - it }
        val gains2 = IntArray(n) { it + 1}
        // 下一个更大的分数(单调栈,从栈底到栈顶单调递减)
        val stack = ArrayDeque<Int>()
        for (i in 0 until n) {
            while (!stack.isEmpty() && scores[nums[stack.peek()]] < scores[nums[i]]) {
                val j = stack.pop()
                gains1[j] = i - j
            }
            stack.push(i)
        }
        // 上一个更大元素(单调栈,从栈底到栈顶单调递减)
        stack.clear()
        for (i in n - 1 downTo 0) {
            while(!stack.isEmpty() && scores[nums[stack.peek()]] <= scores[nums[i]]) { // <=
                val j = stack.pop()
                gains2[j] = j - i
            }
            stack.push(i)
        }
        // 按元素值降序
        val ids = Array<Int>(n) { it }
        Arrays.sort(ids) { i1, i2 ->
            nums[i2] - nums[i1]
        }
        // 枚举每个元素的贡献度
        var leftK = k
        var ret = 1L
        for (id in ids.indices) {
            val gain = Math.min(gains1[ids[id]] * gains2[ids[id]], leftK)
            ret = (ret * quickPow(nums[ids[id]], gain, MOD)) % MOD
            leftK -= gain
            if (leftK == 0) break
        }
        return ret.toInt()
    }
    
    // 快速幂
    private fun quickPow(x: Int, n: Int, mod: Int): Int {
        var ret = 1L
        var cur = n
        var k = x.toLong()
        while (cur > 0) {
            if (cur % 2 == 1) ret = ret * k % mod
            k = k * k % mod
            cur /= 2
        }
        return ret.toInt()
    }
}

复杂度分析:

  • 时间复杂度: O ( n l g n ) O(nlgn) O(nlgn) 其中预处理时间为 O ( U ) O(U) O(U),单次测试用例中使用单调栈计算下一个更大质数分数的时间为 O ( n ) O(n) O(n),排序时间为 O ( n l g n ) O(nlgn) O(nlgn),枚举贡献度时间为 O ( n ) O(n) O(n),整体瓶颈在排序;
  • 空间复杂度: O ( n ) O(n) O(n) 预处理空间为 O ( U ) O(U) O(U),单次测试用例中占用 O ( n ) O(n) O(n) 空间。

题解二(一次遍历优化)

在计算下一个更大元素时,在使用 while 维护单调栈性质后,此时栈顶即为当前元素的前一个更大元素:

while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
    stack.pop()
}
// 此时栈顶即为当前元素的前一个更大元素
stack.push(i)

因此我们可以直接在一次遍历中同时计算出前一个更大元素和下一个更大元素:

val right = IntArray(n) { n } // 下一个更大元素的位置
val left = IntArray(n) { -1 } // 上一个更大元素的位置

计算贡献度的方法: ( i − l e f t [ i ] ) ∗ ( r i g h t [ i ] − i ) (i - left[i]) * (right[i] - i) (ileft[i])(right[i]i),其中 l e f t [ i ] left[i] left[i] r i g h t [ i ] right[i] right[i] 位置不包含在子数组中。

class Solution {
    ...
    fun maximumScore(nums: List<Int>, k: Int): Int {
        val n = nums.size
        // 贡献(子数组数)
        val right = IntArray(n) { n } // 下一个更大元素的位置
        val left = IntArray(n) { -1 } // 上一个更大元素的位置
        // 下一个更大的分数(单调栈,从栈底到栈顶单调递减)
        val stack = ArrayDeque<Int>()
        for (i in 0 until n) {
            while (!stack.isEmpty() && scores[nums[stack.peek()]] < scores[nums[i]]) {
                right[stack.pop()] = i // 下一个更大元素的位置
            }
            if (!stack.isEmpty()) left[i] = stack.peek() // 上一个更大元素的位置
            stack.push(i)
        }
        // 按元素值降序
        val ids = Array<Int>(n) { it }
        Arrays.sort(ids) { i1, i2 ->
            nums[i2] - nums[i1]
        }
        // 枚举每个元素的贡献度
        val gains = IntArray(n) { (it - left[it]) * (right[it] - it)}
        var leftK = k
        var ret = 1L
        for (id in ids.indices) {
            val gain = Math.min(gains[ids[id]], leftK)
            ret = (ret * quickPow(nums[ids[id]], gain, MOD)) % MOD
            leftK -= gain
            if (leftK == 0) break
        }
        return ret.toInt()
    }
    ...
}

复杂度分析:

  • 同上

相似题目:

  • 907. 子数组的最小值之和
  • 1856. 子数组最小乘积的最大值
  • 2104. 子数组范围和
  • 2281. 巫师的总力量和

推荐阅读

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

  • LeetCode 单周赛第 358 场 · 结合排序不等式的动态规划
  • LeetCode 单周赛第 357 场 · 多源 BFS 与连通性问题
  • LeetCode 双周赛第 109 场 · 按部就班地解决动态规划问题
  • LeetCode 双周赛第 107 场 · 很有意思的 T2 题

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/873745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

赴日IT培训 国内的程序员去日本做IT容易吗?

去日本当程序员的两大要素就是技术和日语。所以说容易也容易&#xff0c;如果学历过关&#xff08;统招大专以上&#xff09;&#xff0c;再加上在国内有经验&#xff0c;所以技术方面问题不大。要说难也难&#xff0c;你要克服语言关&#xff0c;去本本工作对日语的要求比较高…

NeuralNLP-NeuralClassifier的使用记录(一),训练预测自己的【英文文本多分类】

NeuralNLP-NeuralClassifier的使用记录&#xff0c;训练预测自己的英文文本多分类 NeuralNLP-NeuralClassifier是腾讯开发的一个多层多分类应用工具&#xff0c;支持的任务包括&#xff0c;文本分类中的二分类、多分类、多标签&#xff0c;以及层次多标签分类。支持的文本编码…

运维工程师常见面试题

1、http常见返回码 2、mysql的同步方式 1&#xff09;异步复制 MySQL默认的复制即是异步的&#xff0c;主库在执行完客户端提交的事务后会立即将结果返给给客户端&#xff0c;并不关心从库是否已经接收并处理&#xff0c;这样就会有一个问题&#xff0c;主如果crash掉了&a…

207、仿真-51单片机脉搏心率与血氧报警Proteus仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&a…

Docker部署rabbitmq遇到的问题 Stats in management UI are disabled on this node

1. Stats in management UI are disabled on this node #进入rabbitmq容器 docker exec -it {rabbitmq容器名称或者id} /bin/bash#进入容器后&#xff0c;cd到以下路径 cd /etc/rabbitmq/conf.d/#修改 management_agent.disable_metrics_collector false echo management_age…

ArcGIS Maps SDK for JavaScript系列之二:认识Map和MapView

目录 Map创建一个 Map 对象的示例代码&#xff1a;Map的常用属性Map的常用方法 MapViewMapView的常用属性MapView的常用方法 在 ArcGIS Maps SDK for JavaScript 中&#xff0c;Map 和 MapView 是两个重要的概念&#xff0c;用于创建和展示地图应用程序。 Map Map 表示一个地图…

2023-08-13 LeetCode每日一题(合并两个有序数组)

2023-08-13每日一题 一、题目编号 88. 合并两个有序数组二、题目链接 点击跳转到题目位置 三、题目描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 …

Error: EACCES: permission denied, rename ‘/usr/local/lib/node_modules/appium‘

在使用npm uninstall -g appium卸载appium的过程中报错 Error: EACCES: permission denied, rename /usr/local/lib/node_modules/appium -> /usr/local/lib/node_modules/.appium-cfBVovI6 npm ERR! code EACCES npm ERR! syscall rename npm ERR! path /usr/local/lib/n…

gcc/g++ 编译选项详解

gcc/g 编译选项详解 文章目录 gcc/g 编译选项详解编译步骤gcc 与 g 区别gcc 命令的常用选项编译优化选项-O 优化-O1优化-O2-O0-Os-Ofast-Og-Oz-O 选项控制特定的优化 WarningsReference>>>>> 欢迎关注公众号【三戒纪元】 <<<<< 编译步骤 gcc 、…

Codeforces Round 891 (Div. 3)ABC

Codeforces Round 891 (Div. 3) 目录 A. Array Coloring题目大意思路代码 B. Maximum Rounding题目大意思路代码 C. Assembly via Minimums题目大意思路代码 A. Array Coloring 题目大意 给你一个包含 n n n个数字的数组&#xff0c;你的任务是判断这个数组是否可以划分成两个…

TCP/IP 下的计算机网络江湖

〇、引言 在当今数字化时代,计算机网络宛如广袤江湖,涵盖着五大门派:物理层、数据链路层、网络层、传输层和应用层。每个门派独具技能,共同构筑着现代网络的框架。物理层宛如江湖基石,将比特流传输;数据链路层如武林传承,组织数据帧传递;网络层则像导航大师,寻找传送路…

Web framework-Gin

一、Gin Go Web--Go Module 软件框架&#xff08;software framework&#xff09;&#xff0c;通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范&#xff0c;也指为了实现某个软件组件规范时&#xff0c;提供规范所要求之基础功能的软件产品。 框架就是&#…

【报童模型】随机优化问题二次规划

面对需求的不确定性&#xff0c;报童模型是做库存优化的常见模型。而标准报童模型假设价格是固定的&#xff0c;此时求解一个线性规划问题&#xff0c;可以得到最优订货量&#xff0c;这种模型存在局限性。因为现实世界中价格与需求存在一定的关系&#xff0c;本文假设需求q是价…

FreeRTOS源码分析-12 低功耗管理

目录 1 STM32低功耗管理概念及应用 1.1睡眠模式 1.2 停止模式 1.3 待机模式 2 Tickless低功耗管理 2.1 Tickless低功耗模式介绍 2.2 FreeRTOS低功耗模式配置 2.3 FreeRTOS低功耗模式应用 3 低功耗管理实际项目开发 3.1 低功耗设计必须要掌握的硬件知识 …

leetcode 1049. 最后一块石头的重量 II

2023.8.13 与分割等和子集类似&#xff0c;可以转化为0-1背包问题。 本题也是需要将数组元素分成两堆&#xff0c;区别在于本题需要使这两堆的差值最小&#xff0c;而之前那题是需要两堆差值为0。 使用之前的一维dp数组的思路&#xff0c;代码如下&#xff1a; class Solution…

开发一个RISC-V上的操作系统(八)—— 抢占式多任务(Preemptive Multitasking)

目录 一、抢占式多任务 二、代码实现 三、上板测试 本节的代码在仓库的 06_Preemptive_Muti_Task 目录下&#xff0c;仓库链接&#xff1a;riscv_os: 一个RISC-V上的简易操作系统 本文代码的运行调试会在前面开发的RISC-V处理器上进行&#xff0c;仓库链接&#xff1a;cpu_…

heap pwn 入门大全 - 1:glibc heap机制与源码阅读(上)

本文为笔者学习heap pwn时&#xff0c;学习阅读glibc ptmalloc2源码时的笔记&#xff0c;与各位分享。可能存在思维跳跃或错误之处&#xff0c;敬请见谅&#xff0c;欢迎在评论中指出。本文也借用了部分外网和其他前辈的素材图片&#xff0c;向各位表示诚挚的感谢&#xff01;如…

MySQL修改编码

插入中文乱码,我电脑上没有出现&#xff0c;我只是来记录一下 MySQL版本: 8.0.34Windows 查看编码 mysql中输入下面的命令 show variables like ‘character_%’; 乱码的一般是图片中划红线的部分不是utf8 character_set_database是设置数据库的默认编码的 character_set_ser…

面试热题(回文链表)

给定一个链表的 头节点 head &#xff0c;请判断其是否为回文链表。 如果一个链表是回文&#xff0c;那么链表节点序列从前往后看和从后往前看是相同的。 回文链表类似于回文串&#xff0c;正读倒读的顺序是一样的&#xff0c;那么我们怎么去判断一个链表是否是回文链表呢&…

7.2.tensorRT高级(2)-学习深度学习中涉及的线程知识

目录 前言1. 多线程2. 问答环节2.1 线程启动相关问题2.2 线程启动函数参数相关问题 总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次…