📚 力扣每日一题–2025.7.17

📚 3202. 找出有效子序列的最大长度 II(中等)

今天我们要解决的是力扣上的第 3202 题——找出有效子序列的最大长度 II。这道题是昨天 3201 题的扩展,需要我们处理更一般化的情况。

⚠️ 注意:由于使用昨天的方法一直超时且有 bug,今天的内容参考了灵神的思路,前两种完全仿照参考,灵神太牛逼了真的,又简介又高效(原文链接)。

📝 题目描述

给你一个整数数组 nums 和一个 正 整数 k 。

nums 的一个 子序列 sub 的长度为 x ,如果其满足以下条件,则称其为 有效子序列 :

(sub[0] + sub[1]) % k == (sub[1] + sub[2]) % k == … == (sub[x - 2] + sub[x - 1]) % k

返回 nums 的 最长有效子序列 的长度。

一个 子序列 指的是从原数组中删除一些元素(也可以不删除任何元素),剩余元素保持原来顺序组成的新数组。

在这里插入图片描述

🤔 思路分析

核心需求推导过程 📝

题目要求子序列中所有相邻元素之和对 k 取模的结果必须相同,即:
(sub[0] + sub[1]) % k == (sub[1] + sub[2]) % k == ... == (sub[x-2] + sub[x-1]) % k

令这个共同的模值为 m,则对所有 i 从 0 到 x-2,都有:
(sub[i] + sub[i+1]) % k == m

这个条件比 3201 题中 k=2 的情况更一般化,也更复杂。我们需要分析这个条件背后的数学本质和序列特性。

🧮 数学本质分析

对于等式

(a+b)modk=(b+c)modk
根据 模运算的世界:当加减乘除遇上取模,可以移项,得

(a+b−(b+c))modk=0
化简得

(a−c)modk=0
这意味着 a 与 c 关于模 k 同余。即题目式子中的 sub[i] 与 sub[i+2] 关于模 k 同余。换句话说,有效子序列的偶数项 sub[0],sub[2],sub[4],… 都关于模 k 同余,奇数项 sub[1],sub[3],sub[5],… 都关于模 k 同余。

如果把每个 nums[i] 都改成 nums[i]modk,问题等价于:

求最长子序列的长度,该子序列的奇数项都相同,偶数项都相同。
在模 k 意义下,如果确定了子序列的最后两项,就确定了整个子序列。

🚀 解题方法

方法一:考察子序列的最后两项 🔄

核心思路

这种方法的核心思想是通过观察子序列的最后两项来动态构建有效子序列。我们发现,如果一个有效子序列的最后两项模 k 的结果分别为 y 和 x,那么在它前面添加一个模 k 结果为 y 的元素,就能形成一个新的有效子序列,其最后两项为 x 和 y。

实例解析

以 nums=[1,2,1,2,1,2],k=3 为例:

  • 模 3 后的数组为 [1,2,1,2,1,2]
  • 处理第一个元素 1(模 3 后为 1):
    我们可以创建一个以 1 结尾的子序列,此时可以认为它前面有一个虚拟的 2(模 3 后),形成「末尾为 2,1 的子序列」,长度为 1
    即 f[2][1] = 1
  • 处理第二个元素 2(模 3 后为 2):
    我们可以在「末尾为 1,2 的子序列」后添加 2,但目前不存在这样的子序列
    我们也可以在「末尾为 2,1 的子序列」后添加 2,形成「末尾为 1,2 的子序列」,长度为 2
    即 f[1][2] = f[2][1] + 1 = 2
  • 处理第三个元素 1(模 3 后为 1):
    我们可以在「末尾为 1,2 的子序列」后添加 1,形成「末尾为 2,1 的子序列」,长度为 3
    即 f[2][1] = f[1][2] + 1 = 3
  • 依此类推,我们不断交替更新 f[1][2] 和 f[2][1],最终得到 f[1][2] = 6
算法步骤
  1. 创建一个 k×k 的二维数组 f,f[y][x] 表示最后两项模 k 分别为 y 和 x 的子序列的长度
  2. 遍历数组中的每个元素 x,计算 x mod k
  3. 对于每个可能的前一项模 k 值 y(0 到 k-1):
    • 更新 f[y][x] = f[x][y] + 1
    • 这表示在以 x 和 y 结尾的子序列后添加当前元素,形成以 y 和 x 结尾的新子序列
  4. 跟踪 f 中的最大值作为答案
为什么这样可行?

当我们有一个子序列以 (x,y) 结尾,并且我们添加一个新元素 z,使得 (y+z) % k = m(共同的模值),那么新子序列以 (y,z) 结尾。根据前面的数学分析,x ≡ z (mod k),所以 x 和 z 本质上是相同的模值。这就是为什么我们可以用 f[x][y] + 1 来更新 f[y][x]。

答疑
问:如何理解这个递推?它和记忆化搜索的区别是什么?

答:对比二者的计算顺序。如果用记忆化搜索来做,需要单独计算「最左(或者最右)两项模 k 分别为 x 和 y 的子序列」的长度,这是「单线程」,必须查找下一个元素的位置。而递推的计算顺序是,(假设我们先遍历到了元素 2,然后遍历到了元素 4,两个元素属于不同的子序列)一会计算一下「最后两项模 k 分别为 y 和 2 的子序列」,一会又计算一下「最后两项模 k 分别为 y 和 4 的子序列」,这是「多线程」,没有查找元素位置的过程,遇到谁就处理谁。

class Solution {public int maximumLength(int[] nums, int k) {// 初始化最大长度为0int ans = 0;// 创建二维DP数组// f[y][x]表示最后两项模k分别为y和x的子序列的长度int[][] f = new int[k][k];// 遍历数组中的每个元素for (int x : nums) {// 计算当前元素模k的结果x %= k;// 尝试将当前元素添加到以各种可能值结尾的子序列中for (int y = 0; y < k; y++) {// 核心递推关系:// 以y和x结尾的子序列长度 = 以x和y结尾的子序列长度 + 1// 这表示在以x和y结尾的子序列后添加当前元素xf[y][x] = f[x][y] + 1;// 更新最大长度ans = Math.max(ans, f[y][x]);}}// 返回最长有效子序列的长度return ans;}
}

方法二:枚举余数,考察子序列的最后一项 🔍

核心思路

这种方法的核心思想是枚举所有可能的相邻元素之和模 k 的结果 m(从 0 到 k-1),然后对每个 m 分别寻找最长的有效子序列。对于固定的 m,如果子序列的最后一项模 k 为 x,那么倒数第二项模 k 必须为 (m-x) mod k。

实例解析

以 nums=[1,2,1,2,1,2],k=3,m=1 为例:

  • 模 3 后的数组为 [1,2,1,2,1,2]
  • 对于 m=1,相邻元素之和模 3 应为 1
  • 处理第一个元素 1(模 3 后为 1):
    需要找到倒数第二项模 3 为 (1-1) mod 3 = 0 的子序列
    不存在这样的子序列,所以 f[1] = 0 + 1 = 1
  • 处理第二个元素 2(模 3 后为 2):
    需要找到倒数第二项模 3 为 (1-2) mod 3 = 2 的子序列
    不存在这样的子序列,所以 f[2] = 0 + 1 = 1
  • 处理第三个元素 1(模 3 后为 1):
    需要找到倒数第二项模 3 为 (1-1) mod 3 = 0 的子序列
    不存在这样的子序列,所以 f[1] 保持为 1
  • 处理第四个元素 2(模 3 后为 2):
    需要找到倒数第二项模 3 为 (1-2) mod 3 = 2 的子序列
    存在这样的子序列(第三个元素),所以 f[2] = f[2] + 1 = 2
  • 依此类推,最终我们可以得到长度为 6 的有效子序列
算法步骤
  1. 初始化最大长度为 0
  2. 枚举所有可能的模值 m(从 0 到 k-1):
    a. 创建一个长度为 k 的一维数组 f,f[x] 表示最后一项模 k 为 x 的子序列的长度
    b. 遍历数组中的每个元素 x,计算 x mod k
    c. 计算倒数第二项应该有的模值:prev = (m - x + k) % k
    d. 更新 f[x] = f[prev] + 1
    e. 跟踪 f 中的最大值作为当前 m 下的答案
  3. 返回所有 m 下的最大答案
为什么这样可行?

对于固定的 m,有效子序列中每个相邻元素对 (a,b) 都满足 (a+b) mod k = m。当我们添加一个新元素 x 时,只需要找到以 (m-x) mod k 结尾的子序列,并将 x 添加到其末尾,就能形成一个新的有效子序列。

class Solution {public int maximumLength(int[] nums, int k) {// 初始化最大长度为0int ans = 0;// 枚举所有可能的相邻元素之和模k的结果mfor (int m = 0; m < k; m++) {// 创建一维DP数组// f[x]表示最后一项模k为x的子序列的长度int[] f = new int[k];// 遍历数组中的每个元素for (int x : nums) {// 计算当前元素模k的结果x %= k;// 计算倒数第二项应该有的模值// (m - x + k) % k 确保结果非负int prev = (m - x + k) % k;// 更新f[x]: 在以prev结尾的子序列后添加当前元素xf[x] = f[prev] + 1;// 更新最大长度ans = Math.max(ans, f[x]);}}// 返回最长有效子序列的长度return ans;}
}

方法三:贪心算法 + 哈希表 🚀

核心思路

这种方法的核心思想是对于每个可能的模值对 (a,b),其中 (a+b) % k = m,我们尝试构建最长的交替序列 a,b,a,b,… 或 b,a,b,a,…。我们可以使用哈希表记录每个模值出现的位置,然后对于每个可能的模值对,计算它们能形成的最长序列。

算法步骤
  1. 创建一个哈希表,记录每个模值出现的所有索引位置
  2. 初始化最大长度为 0
  3. 对于每个可能的模值对 (a,b),其中 (a+b) % k = m(m 从 0 到 k-1):
    a. 使用双指针分别遍历 a 和 b 的位置列表
    b. 交替选择 a 和 b 的位置,确保它们在原数组中的顺序
    c. 计算能形成的最长序列长度
    d. 更新最大长度
  4. 返回最大长度
代码实现
class Solution {public int maximumLength(int[] nums, int k) {// 创建哈希表记录每个模值出现的位置Map<Integer, List<Integer>> modPositions = new HashMap<>();for (int i = 0; i < nums.length; i++) {int mod = nums[i] % k;modPositions.computeIfAbsent(mod, key -> new ArrayList<>()).add(i);}int maxLen = 0;// 枚举所有可能的模值对(a,b),使得(a+b) % k = mfor (int m = 0; m < k; m++) {// 获取所有可能的模值Set<Integer> mods = modPositions.keySet();// 情况1:序列中只有一种元素for (int a : mods) {if ((2 * a) % k == m) {// 如果a+a的模为m,则可以使用所有a元素maxLen = Math.max(maxLen, modPositions.get(a).size());}}// 情况2:序列中交替出现两种元素a和bList<Integer> modList = new ArrayList<>(mods);for (int i = 0; i < modList.size(); i++) {int a = modList.get(i);int b = (m - a + k) % k;if (!modPositions.containsKey(b)) continue;// 使用双指针查找最长交替序列List<Integer> aPositions = modPositions.get(a);List<Integer> bPositions = modPositions.get(b);int len1 = getMaxAlternatingLength(aPositions, bPositions); // a开头int len2 = getMaxAlternatingLength(bPositions, aPositions); // b开头maxLen = Math.max(maxLen, Math.max(len1, len2));}}return maxLen;}// 计算以first开头,交替选择first和second的最长序列长度private int getMaxAlternatingLength(List<Integer> first, List<Integer> second) {int i = 0, j = 0;int len = 0;boolean chooseFirst = true;while (i < first.size() || j < second.size()) {if (chooseFirst) {if (i >= first.size()) break;len++;int lastIdx = first.get(i++);// 找到第二个列表中比lastIdx大的第一个元素while (j < second.size() && second.get(j) <= lastIdx) j++;chooseFirst = false;} else {if (j >= second.size()) break;len++;int lastIdx = second.get(j++);// 找到第一个列表中比lastIdx大的第一个元素while (i < first.size() && first.get(i) <= lastIdx) i++;chooseFirst = true;}}return len;}
}
复杂度分析
  • 时间复杂度:O(k² + n + k * n),其中 n 是数组长度,k 是给定的模数值
    • O(n):构建哈希表
    • O(k²):枚举所有可能的模值对
    • O(k * n):双指针遍历位置列表
  • 空间复杂度:O(n),存储每个模值的位置列表
方法比较

这种方法在 k 较小时表现良好,但当 k 较大时(接近 n),时间复杂度会接近 O(n²),不如前两种方法高效。不过它提供了一种不同的思路,通过预存储位置信息来构建最长序列。

🚀 算法复杂度分析

方法一复杂度
  • 时间复杂度:O(nk),其中 n 是数组长度,k 是给定的模数值
  • 空间复杂度:O(k²),需要维护一个 k×k 的二维数组
方法二复杂度
  • 时间复杂度:O(nk),需要枚举 k 种可能的模值 m,每种模值下遍历数组一次
  • 空间复杂度:O(k),只需维护一个长度为 k 的一维数组

📊 示例分析

示例 1:nums = [1,2,1,2,1,2], k = 3

  • 所有元素模 3 后为[1,2,1,2,1,2]
  • 方法一:通过 f[y][x] = f[x][y] + 1 不断更新
    • f[2][1] = 1 (处理第一个 1)
    • f[1][2] = 2 (处理第一个 2)
    • f[2][1] = 3 (处理第二个 1)
    • f[1][2] = 4 (处理第二个 2)
    • 最终得到最大长度 6
  • 方法二:枚举 m=0,1,2
    • 当 m=0 时,得到序列长度 2
    • 当 m=1 时,得到序列长度 6
    • 当 m=2 时,得到序列长度 2
    • 最终得到最大长度 6

示例 2:nums = [3,1,3,3,2], k = 5

  • 所有元素模 5 后为[3,1,3,3,2]
  • 最长有效子序列为[3,3,3],长度 3
  • 相邻元素之和模 5 均为 1 (3+3=6%5=1, 3+3=6%5=1)

💡 拓展思考

问题变体
  1. 如果要求子序列中相邻元素之和的余数等于某个特定值 m(而不只是所有余数相等),该如何解决?
  2. 如果允许子序列中有一个"错误"(即有一处相邻元素之和的余数与其他不同),最长有效子序列的长度会是多少?
实际应用

这个问题在时间序列分析和模式识别中有实际应用。例如,在分析周期性数据时,我们可能需要找到具有特定模式的最长子序列。

算法选择策略
  • 方法一空间复杂度较高(O(k²)),但实现简单直观
  • 方法二空间复杂度较低(O(k)),但需要枚举所有可能的模值
  • 当 k 较小时(如 k<1000),两种方法均可选择
  • 当 k 较大时(如 k>10000),方法二更适合

📝 总结

本题是 3201 题的扩展,需要处理更一般化的情况。通过数学分析,我们发现有效子序列的偶数项和奇数项分别关于模 k 同余,这一关键洞察帮助我们设计出高效的动态规划解法。

我们介绍了两种主要方法:

  1. 维护二维数组 f[y][x]记录最后两项模 k 分别为 y 和 x 的子序列长度
  2. 枚举所有可能的模值 m,维护一维数组 f[x]记录最后一项模 k 为 x 的子序列长度

两种方法均达到 O(nk)的时间复杂度,远优于暴力解法。通过这类问题,我们可以深入理解模运算的性质和动态规划在序列问题中的应用。

希望今天的讲解能帮助你更好地理解这个问题和动态规划的应用!如果你有任何疑问或想法,欢迎在评论区留言讨论。明天见!👋

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

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

相关文章

github不能访问怎么办

访问&#xff1a;“github.com”国内多个地点网站测速结果_网站测速 - 站长工具访问“github.global.ssl.fastly.net”国内多个地点网站测速结果_网站测速 - 站长工具复制红框中的ip 打开“C:\Windows\System32\drivers\etc\hosts”文件输入&#xff1a; 20.205.243.166 githu…

【深度学习新浪潮】AI在finTech领域有哪些值得关注的进展?

近年来,AI在金融科技(FinTech)领域的应用呈现爆发式增长,尤其在大模型技术突破和政策支持的双重驱动下,多个关键领域取得了显著进展。以下是值得关注的核心方向及具体案例: 一、大模型技术重塑金融服务范式 以DeepSeek为代表的国产大模型通过开源和低成本部署(本地化成…

【中等】题解力扣22:括号生成

题目详情 数字 n 代表生成括号的对数&#xff0c;设计一个函数生成所有可能的并且有效的括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”] 示例 2&#xff1a; 输入&#xff1a;n 1 输出&#…

【JEECG 组件扩展】JSwitch开关组件扩展单个多选框样式

功能说明&#xff1a;基于JeecgBoot开源框架&#xff0c;JSwitch开关组件扩展&#xff0c;支持单个多选样式。效果展示&#xff1a;使用示例&#xff1a;{field: JSwitch,component: JSwitch,label: JSwitch,},{field: JSwitchCheckBox,component: JSwitch,label: JSwitchCheck…

(转)Kubernetes基础介绍

Kubernetes是用于自动部署、扩展和管理容器化应用程序的开源系统。

vue 播放海康m3u8视频流笔记

1、安装hls.jsnpm i hls 2、使用<el-dialogtitle"监控"top"5vh":visible.sync"dialogVisible"width"30%"><video id"video" style"width:100%;height:300px" controls><sourcetype"applicati…

如何清除 npm 缓存

清除 npm 缓存&#xff1a;利弊分析与操作指南 在使用 Node.js 和 npm 进行项目开发时&#xff0c;我们经常会与 npm install 命令打交道。这个过程中&#xff0c;npm 会在本地建立一个缓存机制&#xff0c;用以存储已下载的包&#xff0c;从而显著提升后续安装的速度。然而&am…

Java学习-----消息队列

消息队列是分布式系统中重要的组件之一。使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。使用消息队列主要有三点好处&#xff1a;&#xff08;1&#xff09;通过异步处理提高系统性能&#xff08;减少响应所需时间&#xff09;&#xff1a;用户提交请…

玩转Docker | 使用Docker部署TeamMapper思维导图应用程序

玩转Docker | 使用Docker部署TeamMapper思维导图应用程序 前言 一、TeamMapper介绍 TeamMapper简介 TeamMapper功能 二、系统要求 环境要求 环境检查 Docker版本检查 检查操作系统版本 三、部署TeamMapper服务 下载TeamMapper镜像 编辑部署文件 创建容器 检查容器状态 检查服务…

深入解析Linux进程创建与fork机制

目录 一、fork函数初识 二、fork函数返回值 思考&#xff1a; 1. fork函数为何给子进程返回0&#xff0c;而给父进程返回子进程的PID&#xff1f; 2. 关于fork函数为何有两个返回值这个问题 三、写时复制机制 写时拷贝&#xff08;Copy-On-Write&#xff09;机制解析 1.…

【软件开发】主流 AI 编码插件

主流 AI 编码插件1. GitHub Copilot 支持平台&#xff1a;VS Code、Neovim、JetBrains 系列、Visual Studio 优点 深度语料库&#xff1a;基于 OpenAI 的大规模模型训练&#xff0c;能够生成高质量、上下文相关的代码补全。多语言支持&#xff1a;对 Python、JavaScript、TypeS…

实训十一——网络通信原理

补充如何解决IPv4地址不足的问题&#xff1f;使用专用的IPv4地址范围&#xff08;如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16&#xff09;并通过NAT转换与外部网络通信&#xff0c;能有效节约公网IPv4地址。根据RFC 1918的定义&#xff0c;以下是保留的私有IPv4地址范围&am…

Spring Cloud LoadBalancer 详解

在分布式系统快速发展的当下&#xff0c;服务间的调用日益频繁且复杂。如何合理分配请求流量&#xff0c;避免单个服务节点过载&#xff0c;保障系统的稳定性与高效性&#xff0c;成为关键问题。负载均衡技术便是解决这一问题的重要手段。Spring Cloud LoadBalancer 作为 Sprin…

Linux内核内存管理相关的配置参数

Linux内核内存管理相关的配置参数&#xff08;主要位于/proc/sys/vm/目录下&#xff09;&#xff0c;用于调整内存分配、缓存管理、交换机制、OOM&#xff08;内存溢出&#xff09;策略等核心内存行为。以下是对每个参数的详细解释&#xff1a; admin_reserve_kbytes block_dum…

Web开发 01

先放一下自己写的手敲的第一个网站代码&#xff01;~虽然很简单但还是有点成就感&#xff01;&#xff01;开心&#x1f60a;<!DOCTYPE html> <html><head><title>Title!</title><link rel "stylesheet"href "style.css"…

Redis 生产实战 7×24:容量规划、性能调优、故障演练与成本治理 40 条军规

&#xff08;一&#xff09;写在前面&#xff1a;为什么需要“军规” Redis 在测试环境跑得飞快&#xff0c;一到线上就“莫名其妙”抖动&#xff1b;大促前扩容 3 倍&#xff0c;成本却翻 5 倍&#xff1b;一次主从切换&#xff0c;缓存雪崩导致下游 DB 被打挂&#xff1b;开发…

【DOCKER】综合项目 MonitorHub (监控中心)

文章目录1、项目架构图1.1 架构组件2、实际实施2.1 安装docker2.2 编写dockerfile文件2.2.1 Prometheus2.2.2 node_exporter2.2.3 nginxvts模块2.2.4 nginx_exporeter 服务发现文件2.2.5 maridb dockerfile文件2.2.6 镜像总数2.3 具体操作2.3.1 Prometheus组件2.3.2 nginx组件2…

Java List 集合详解:从基础到实战,掌握 Java 列表操作全貌

作为一名 Java 开发工程师&#xff0c;你一定在项目中频繁使用过 List 集合。它是 Java 集合框架中最常用、最灵活的数据结构之一。无论是从数据库查询出的数据&#xff0c;还是前端传递的参数列表&#xff0c;List 都是处理这些数据的首选结构。本文将带你全面掌握&#xff1a…

SGMD辛几何模态分解 直接替换Excel运行包含频谱图相关系数图 Matlab语言!

SGMD辛几何模态分解 直接替换Excel运行包含频谱图相关系数图 Matlab语言算法近几年刚提出&#xff0c;知网还没几个人用&#xff0c;你先用&#xff0c;你就是创新&#xff01;算法新颖小众&#xff0c;用的人很少&#xff0c;包含分解图、频谱图、相关系数图&#xff0c;效果如…

Oracle数据泵详解——让数据迁移像“点外卖”一样简单​

​今天我想和大家聊一个数据库领域的“万能搬运工”——Oracle数据泵&#xff08;Data Pump&#xff09;​。相信很多人都有过这样的经历&#xff1a;业务要上线新系统&#xff0c;得把旧库的数据搬到新环境&#xff1b;或者领导突然要一份3年前的历史数据&#xff0c;可不能影…