LeetCode 周赛上分之旅 #44 同余前缀和问题与经典倍增 LCA 算法

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

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

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

T1. 统计对称整数的数目(Easy)

  • 标签:模拟

T2. 生成特殊数字的最少操作(Medium)

  • 标签:思维、回溯、双指针

T3. 统计趣味子数组的数目(Medium)

  • 标签:同余定理、前缀和、散列表

T4. 边权重均等查询(Hard)

  • 标签:图、倍增、LCA、树上差分


T1. 统计对称整数的数目(Easy)

https://leetcode.cn/problems/count-symmetric-integers/

题解(模拟)

根据题意模拟,亦可以使用前缀和预处理优化。

class Solution {fun countSymmetricIntegers(low: Int, high: Int): Int {var ret = 0for (x in low..high) {val s = "$x"val n = s.lengthif (n % 2 != 0) continuevar diff = 0for (i in 0 until n / 2) {diff += s[i] - '0'diff -= s[n - 1 - i] - '0'}if (diff == 0) ret += 1}return ret}
}

复杂度分析:

  • 时间复杂度: O ( ( h i g h − l o w ) l g h i g h ) O((high-low)lg^{high}) O((highlow)lghigh) 单次检查时间为 O ( l g h i g h ) O(lg^{high}) O(lghigh)
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

T2. 生成特殊数字的最少操作(Easy)

https://leetcode.cn/problems/minimum-operations-to-make-a-special-number/

题解一(回溯)

思维题,这道卡了多少人。

  • 阅读理解: 在一次操作中,您可以选择 n u m num num 的任意一位数字并将其删除,求最少需要多少次操作可以使 n u m num num 变成 25 25 25 的倍数;
  • 规律: 对于 25 25 25 的倍数,当且仅当结尾为「00、25、50、75」这 4 4 4 种情况时成立,我们尝试构造出尾部符合两个数字能被 25 25 25 整除的情况。

可以用回溯解决:

class Solution {fun minimumOperations(num: String): Int {val memo = HashMap<String, Int>()fun count(x: String): Int {val n = x.lengthif (n == 1) return if (x == "0") 0 else 1if (((x[n - 2] - '0') * 10 + (x[n - 1]- '0')) % 25 == 0) return 0if(memo.containsKey(x))return memo[x]!!val builder1 = StringBuilder(x)builder1.deleteCharAt(n - 1)val builder2 = StringBuilder(x)builder2.deleteCharAt(n - 2)val ret = 1 + min(count(builder1.toString()), count(builder2.toString()))memo[x]=retreturn ret}return count(num)}
}

复杂度分析:

  • 时间复杂度: O ( n 2 ⋅ m ) O(n^2·m) O(n2m) 最多有 n 2 n^2 n2 种子状态,其中 m m m 是字符串的平均长度, O ( m ) O(m) O(m) 是构造中间字符串的时间;
  • 空间复杂度: O ( n ) O(n) O(n) 回溯递归栈空间。

题解二(双指针)

初步分析:

  • 模拟: 事实上,问题的方案最多只有 4 种,回溯的中间过程事实在尝试很多无意义的方案。我们直接枚举这 4 种方案,删除尾部不属于该方案的字符。以 25 为例,就是删除 5 后面的字符以及删除 2 与 5 中间的字符;
  • 抽象: 本质上是一个最短匹配子序列的问题,即 「找到 nums 中最靠后的匹配的最短子序列」问题,可以用双指针模拟。

具体实现:

  • 双指针: 我们找到满足条件的最靠左的下标 i,并删除末尾除了目标数字外的整段元素,即 r e t = n − i − 2 ret = n - i - 2 ret=ni2
  • 特殊情况: 在 4 种构造合法的特殊数字外,还存在删除所有非 0 数字后构造出 0 的方案;
  • 是否要验证数据含有前导零: 对于构造「00」的情况,是否会存在删到最后剩下多个 0 的情况呢?其实是不存在的。因为题目说明输入数据 num 本身是不包含前导零的,如果最后剩下多个 0 ,那么在最左边的 0 左侧一定存在非 0 数字,否则与题目说明矛盾。
class Solution {fun minimumOperations(num: String): Int {val n = num.lengthvar ret = nfor (choice in arrayOf("00", "25", "50", "75")) {// 双指针var j = 1for (i in n - 1 downTo 0) {if (choice[j] != num[i]) continueif (--j == -1) {ret = min(ret, n - i - 2)break}}}// 特殊情况ret = min(ret, n - num.count { it == '0'})return ret}
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 4 种方案和特殊方案均是线性遍历;
  • 空间复杂度: O ( 1 ) O(1) O(1) 仅使用常量级别空间。

T3. 统计趣味子数组的数目(Medium)

https://leetcode.cn/problems/count-of-interesting-subarrays/

题解(同余 + 前缀和 + 散列表)

初步分析:

  • 问题目标: 统计数组中满足目标条件的子数组;
  • 目标条件: 在子数组范围 [ l , r ] [l, r] [l,r] 内,设 c n t cnt cnt 为满足 n u m s [ i ] nums[i] % m == k nums[i] 的索引 i i i 的数量,并且 c n t cnt % m == k cnt。大白话就是算一下有多少数的模是 k k k,再判断个数的模是不是也是 k k k
  • 权重: 对于满足 n u m s [ i ] nums[i] % m == k nums[i] 的元素,它对结果的贡献是 1 1 1,否则是 0 0 0

分析到这里,容易想到用前缀和实现:

  • 前缀和: 记录从起点到 [ i ] [i] [i] 位置的 [ 0 , i ] [0, i] [0,i] 区间范围内满足目标的权重数;
  • 两数之和: 从左到右枚举 [ i ] [i] [i],并寻找已经遍历的位置中满足 ( p r e S u m [ i ] − p r e S u m [ j ] ) % m = = k (preSum[i] - preSum[j]) \% m == k (preSum[i]preSum[j])%m==k 的方案数记入结果;
  • 公式转换: 上式带有取模运算,我们需要转换一下:
    • 原式 ( p r e S u m [ i ] − p r e S u m [ j ] ) % m = = k (preSum[i] - preSum[j]) \% m == k (preSum[i]preSum[j])%m==k
    • 考虑 p r e S u m [ i ] % m − p r e S u m [ j ] % m preSum[i] \% m - preSum[j] \% m preSum[i]%mpreSum[j]%m 是正数数的的情况,原式等价于: p r e S u m [ i ] % m − p r e S u m [ j ] % m = = k preSum[i] \% m - preSum[j] \% m == k preSum[i]%mpreSum[j]%m==k
    • 考虑 p r e S u m [ i ] % m − p r e S u m [ j ] % m preSum[i] \% m - preSum[j] \% m preSum[i]%mpreSum[j]%m 是负数的的情况,我们在等式左边增加补数: ( p r e S u m [ i ] % m − p r e S u m [ j ] % m + m ) (preSum[i] \% m - preSum[j] \% m + m) %m == k (preSum[i]%mpreSum[j]%m+m)
    • 联合正数和负数两种情况,即我们需要找到前缀和为 ( p r e S u m [ i ] % m − k + m ) % m (preSum[i] \% m - k + m) \% m (preSum[i]%mk+m)%m 的元素;
  • 修正前缀和定义: 最后,我们修改前缀和的定义为权重 % m \% m %m

组合以上技巧:

class Solution {fun countInterestingSubarrays(nums: List<Int>, m: Int, k: Int): Long {val n = nums.sizevar ret = 0Lval preSum = HashMap<Int, Int>()preSum[0] = 1 // 注意空数组的状态var cur = 0for (i in 0 until n) {if (nums[i] % m == k) cur ++ // 更新前缀和val key = cur % mval target = (key - k + m) % mret += preSum.getOrDefault(target, 0) // 记录方案preSum[key] = preSum.getOrDefault(key, 0) + 1 // 记录前缀和}return ret}
}

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n) 线性遍历,单次查询时间为 O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( m ) O(m) O(m) 散列表空间。

相似题目:

  • 560. 和为 K 的子数组
  • 974. 和可被 K 整除的子数组
  • 523. 连续的子数组和
  • 525. 连续数组

T4. 边权重均等查询(Hard)

https://leetcode.cn/problems/minimum-edge-weight-equilibrium-queries-in-a-tree/

题解(倍增求 LCA、树上差分)

初步分析:

  • 问题目标: 给定若干个查询 [ x , y ] [x, y] [x,y],要求计算将 < x , y > <x, y> <x,y> 的路径上的每条边修改为相同权重的最少操作次数;
  • 问题要件: 对于每个查询 [ x , y ] [x, y] [x,y],我们需要计算 < x , y > <x, y> <x,y> 的路径长度 l l l,以及边权重的众数的出现次数 c c c,而要修改的操作次数就是 l − c l - c lc
  • 技巧: 对于 “树上路径” 问题有一种经典技巧,我们可以把 < x , y > <x, y> <x,y> 的路径转换为从 < x , l c a > <x, lca> <x,lca> 的路径与 < l c a , y > <lca, y> <lca,y> 的两条路径;

思考实现:

  • 长度: 将问题转换为经过 l c a lca lca 中转的路径后,路径长度 l l l 可以用深度来计算: l = d e p t h [ x ] + d e p t h [ y ] − 2 ∗ d e p t h [ l c a ] l = depth[x] + depth[y] - 2 * depth[lca] l=depth[x]+depth[y]2depth[lca]
  • 权重: 同理,权重 w [ x , y ] w[x,y] w[x,y] 可以通过 w [ x , l c a ] w[x, lca] w[x,lca] w [ l c a , y ] w[lca, y] w[lca,y] 累加计算;

现在的关键问题是,如何快速地找到 < x , y > <x, y> <x,y> 的最近公共祖先 LCA?

对于单次 LCA 操作来说,我们可以走 DFS 实现 O ( n ) O(n) O(n) 时间复杂度的算法,而对于多次 LCA 操作可以使用 倍增算法 预处理以空间换时间,单次 LCA 操作的时间复杂度进位 O ( l g n ) O(lgn) O(lgn)

在 LeetCode 有倍增的模板题 1483. 树节点的第 K 个祖先。

在求 LCA 时,我们先把 < x , y > <x, y> <x,y> 跳到相同高度,再利用倍增算法向上跳 2 j 2^j 2j 个父节点,直到到达相同节点即为最近公共祖先。

class Solution {fun minOperationsQueries(n: Int, edges: Array<IntArray>, queries: Array<IntArray>): IntArray {val U = 26// 建图val graph = Array(n) { LinkedList<IntArray>() }for (edge in edges) {graph[edge[0]].add(intArrayOf(edge[1], edge[2] - 1))graph[edge[1]].add(intArrayOf(edge[0], edge[2] - 1))}// 预处理深度、倍增祖先节点、倍增路径信息val m = 32 - Integer.numberOfLeadingZeros(n - 1)val depth = IntArray(n)val parent = Array(n) { IntArray(m) { -1 }} // parent[i][j] 表示 i 的第 2^j 个父节点val cnt = Array(n) { Array(m) { IntArray(U) }} // cnt[i][j] 表示 <i - 2^j> 个父节点的路径信息fun dfs(i: Int, par: Int) {for ((to, w) in graph[i]) {if (to == par) continue // 避免回环depth[to] = depth[i] + 1parent[to][0] = icnt[to][0][w] = 1dfs(to, i)}}dfs(0, -1) // 选择 0 作为根节点// 预处理倍增for (j in 1 until m) {for (i in 0 until n) {val from = parent[i][j - 1]if (-1 != from) {parent[i][j] = parent[from][j - 1]cnt[i][j] = cnt[i][j - 1].zip(cnt[from][j - 1]) { e1, e2 -> e1 + e2 }.toIntArray()}}}// 查询val q = queries.sizeval ret = IntArray(q)for ((i, query) in queries.withIndex()) {var (x, y) = query// 特判if (x == y || parent[x][0] == y || parent[y][0] == x) {ret[i] = 0}val w = IntArray(U) // 记录路径信息var path = depth[x] + depth[y] // 记录路径长度// 先跳到相同高度if (depth[y] > depth[x]) {val temp = xx = yy = temp}var k = depth[x] - depth[y]while (k > 0) {val j = Integer.numberOfTrailingZeros(k) // 二进制分解w.indices.forEach { w[it] += cnt[x][j][it] } // 记录路径信息x = parent[x][j] // 向上跳 2^j 个父节点k = k and (k - 1)}// 再使用倍增找 LCAif (x != y) {for (j in m - 1 downTo 0) { // 最多跳 m - 1 次if (parent[x][j] == parent[y][j]) continue // 跳上去相同就不跳w.indices.forEach { w[it] += cnt[x][j][it] } // 记录路径信息w.indices.forEach { w[it] += cnt[y][j][it] } // 记录路径信息x = parent[x][j]y = parent[y][j] // 向上跳 2^j 个父节点}// 最后再跳一次就是 lcaw.indices.forEach { w[it] += cnt[x][0][it] } // 记录路径信息w.indices.forEach { w[it] += cnt[y][0][it] } // 记录路径信息x = parent[x][0]}// 减去重链长度ret[i] = path - 2 * depth[x] - w.max()}return ret}
}

复杂度分析:

  • 时间复杂度: O ( n l g n ⋅ U ) O(nlgn·U) O(nlgnU) 预处理的时间复杂度是 O ( n l g n ⋅ U ) O(nlgn·U) O(nlgnU),单次查询的时间是 O ( l g n ⋅ U ) O(lgn·U) O(lgnU)
  • 空间复杂度: O ( n l g n ⋅ U ) O(nlgn·U) O(nlgnU) 预处理倍增信息空间。

推荐阅读

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

  • LeetCode 单周赛第 360 场 · 当 LeetCode 考树上倍增,出题的趋势在变化吗
  • LeetCode 单周赛第 359 场 · 结合离散化的线性 DP 问题
  • LeetCode 双周赛第 112 场 · 计算机科学本质上是数学吗?
  • LeetCode 双周赛第 111 场 · 按部就班地解决动态规划问题

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

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

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

相关文章

成都瀚网科技有限公司:抖店怎么开通直播?

随着互联网和移动支付的快速发展&#xff0c;越来越多的人选择开设自己的抖音商店。抖音作为国内最受欢迎的短视频平台之一&#xff0c;拥有庞大的用户基础&#xff0c;成为众多创业者青睐的平台。那么&#xff0c;如何经营自己的抖音店铺呢&#xff1f;下面将从几个方面为您介…

Si24R2F+畜牧 耳标测体温开发资料

Si24R2F是针对IOT应用领域推出的新款超低功耗2.4G内置NVM单发射芯片。广泛应用于2.4G有源活体动物耳标&#xff0c;带实时测温计步功能。相较于Si24R2E&#xff0c;Si24R2F增加了温度监控、自动唤醒间隔功能&#xff1b;发射功率由7dBm增加到12dBm&#xff0c;距离更远&#xf…

k8s 搭建基于session模式的flink集群

1.flink集群搭建 不废话直接上代码&#xff0c;都是基于官网的&#xff0c;在此记录一下 Kubernetes | Apache Flink flink-configuration-configmap.yaml apiVersion: v1 kind: ConfigMap metadata:name: flink-configlabels:app: flink data:flink-conf.yaml: |jobmanager…

postgresql-条件表达式

postgresql-条件表达式 简单Case表达式搜索Case表达式缩写函数总结 简单Case表达式 select e.first_name , e.last_name , e.department_id , case e.department_id when 90 then 管理when 60 then 开发else 其他end as "部门" from cps.public.employees e ;-- 统…

Vue笔记

第一章&#xff1a;Vue环境搭建 1.搭建Vue环境 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><!-- 1.引入Vue.js--><script src"1.vue.js"></scr…

软件生命周期及流程

软件生命周期&#xff1a; 软件生命周期(SDLC&#xff0c;Systems Development Life Cycle)是软件开始研制到最终被废弃不用所经历的各个阶段. 需求分析阶段--输出需求规格说明书&#xff08;原型图&#xff09; 测试介入的晚--回溯成本高 敏捷开发模型&#xff1a; 从1990年…

STM32CUBEMX_创建时间片轮询架构的软件框架

STM32CUBEMX_创建时间片轮询架构的软件框架 说明&#xff1a; 1、这种架构避免在更新STM32CUBEMX配置后把用户代码清除掉 2、利用这种时间片的架构可以使得代码架构清晰易于维护 创建步骤&#xff1a; 1、使用STM32CUBEMX创建基础工程 2、新建用户代码目录 3、构建基础的代码框…

OpenLdap +PhpLdapAdmin + Grafana docker-compose部署安装

目录 一、OpenLdap介绍 二、PhpLdapAdmin介绍 三、使用docker-compose进行安装 1. docker-compose.yml 2. grafana配置文件 3. provisioning 四、安装openldap、phpldapadmin、grafana 五、配置OpenLDAP 1. 登陆PhpLdapAdmin web管理 2. 需要注意的细节 内容介绍参考…

Java作业3

1.下面代码的运行结果是&#xff08;C&#xff09; public static void main(String[] args){String s;System.out.println("s"s);}A.代码编程成功&#xff0c;并输出”s” B.代码编译成功&#xff0c;并输出”snull” C.由于String s没有初始化&#xff0c;代码不…

python基础运用例子

python基础运用例子 1、⼀⾏代码交换 a , b &#xff1a;a, b b, a2、⼀⾏代码反转列表 l[::-1]3、合并两个字典 res {**dict1, **dict2}**操作符合并两个字典for循环合并dict(a, **b) 的方式dict(a.items() b.items()) 的方式dict.update(other_dict) 的方式 4、⼀⾏代码列…

【多尺度双域引导网络:Pan-sharpening】

Multi-Scale Dual-Domain Guidance Network for Pan-sharpening &#xff08;用于泛锐化的多尺度双域引导网络&#xff09; 全色锐化的目标是在纹理丰富的全色图像的指导下&#xff0c;通过超分辨低空间分辨率多光谱图像&#xff08;LRMS&#xff09;的对应物产生高空间分辨率…

语音特征提取与预处理

导入相关包 import librosa import librosa.display import soundfile as sf import numpy as np import matplotlib.pyplot as plt from playsound import playsound 语音读取与显示 file_path test1.wav data, fs librosa.load(file_path, srNone, monoTrue) librosa.d…

数学建模--整数规划匈牙利算法的Python实现

目录 1.算法流程简介 2.算法核心代码 3.算法效果展示 1.算法流程简介 #整数规划模型--匈牙利算法求解 """ 整数规划模型及概念&#xff1a;规划问题的数学模型一般由三个因素构成 决策变量 目标函数 约束条件&#xff1b;线性规划即以线性函数为目标函数&a…

2024腾讯校招后端面试真题汇总及其解答(三)

21【算法题】反转链表 题目: 给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。 示例 1: 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]示例 2: 输入:head = [1,2] 输出:[2,1]示例 3: 输入:head = [] 输出:[]提示: 链表中节点的数目范围是 [0, 5…

GPT转换工具:轻松将MBR转换为GPT磁盘

为什么需要将MBR转换为GPT&#xff1f; 众所周知&#xff0c;Windows 11已经发布很长时间了。在此期间&#xff0c;许多老用户已经从Windows 10升级到Windows 11。但有些用户仍在运行Windows 10。对于那些想要升级到Win 11的用户来说&#xff0c;他们可能不确定Win 11应该使…

LeetCode 热题 100——找到字符串中所有字母异位词(滑动窗口)

题目链接 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题目解析 该题目的意思简而言之就是说&#xff0c;从s字符串中寻找与p字符串含有相同字符(次数和种类均相同)的子串&#xff0c;并且将他们的首字符下标集合进数组中进行返回。 滑动窗口解…

大数据Flink(七十三):SQL的滚动窗口(TUMBLE)

文章目录 SQL的滚动窗口(TUMBLE) SQL的滚动窗口(TUMBLE) 滚动窗口定义:滚动窗口将每个元素指定给指定窗口大小的窗口。滚动窗口具有固定大小,且不重叠。例如,指定一个大小为 5 分钟的滚动窗口。在这种情况下,Flink 将每隔 5 分钟开启一个新的窗口,其中每一条数都会划…

如何使用蚂蚁集团自动化混沌工程 ChaosMeta 做 OceanBase 攻防演练?

当前&#xff0c;业界主流的混沌工程项目基本只关注如何制造故障的问题&#xff0c;而经常做演练相关工作的工程师应该明白&#xff0c;每次演练时还会遇到以下痛点&#xff1a; 检测当前环境是否符合演练预设条件&#xff08;演练准入&#xff09;&#xff1b; 业务流量是否满…

Vue基础1:生命周期汇总(vue2)

Description 生命周期图&#xff1a; 可以理解vue生命周期就是指vue实例从创建到销毁的过程&#xff0c;在vue中分为9个阶段&#xff1a;创建前/后&#xff0c;载入前/后&#xff0c;更新前/后&#xff0c;销毁前/后&#xff0c;其他&#xff1b;常用的有&#xff1a;created&…

C#常用多线程(线程同步,事件触发,信号量,互斥锁,共享内存,消息队列)

using System; using System.Threading; using System.Windows.Forms; using UtilForm.Util;namespace UtilForm {// 线程同步&#xff0c;事件触发&#xff0c;信号量&#xff0c;互斥锁&#xff0c;共享内存&#xff0c;消息队列public partial class frmUIThread : Form{ Sy…