📚 力扣每日一题--2025.7.16
📚 3201. 找出有效子序列的最大长度 I(中等)
今天我们要解决的是力扣上的第 3201 题——找出有效子序列的最大长度 I。这道题虽然标记为中等难度,但只要掌握了正确的思路,就能轻松解决!
📝 题目描述
🤔 思路分析
核心需求推导过程 📝
题目要求子序列中所有相邻元素之和的奇偶性必须相同,即:
(sub[0] + sub[1]) % 2 == (sub[1] + sub[2]) % 2 == ... == (sub[x-2] + sub[x-1]) % 2
这个条件可以从两个角度理解:
- 数学本质:所有相邻元素对的和具有相同的奇偶性
- 序列特性:子序列中的元素必须形成一种特定的奇偶性模式
通过进一步推理,我们可以得出两种基本的有效序列模式:
模式一:全同奇偶性序列
- 当要求相邻元素之和为偶数时,所有元素必须具有相同的奇偶性
- 证明:若 a+b 为偶数且 b+c 为偶数,则 a 和 b 奇偶性相同,b 和 c 奇偶性相同,因此 a 和 c 奇偶性相同,以此类推
模式二:交替奇偶性序列
- 当要求相邻元素之和为奇数时,元素必须交替出现奇偶性
- 证明:若 a+b 为奇数且 b+c 为奇数,则 a 和 b 奇偶性不同,b 和 c 奇偶性不同,因此 a 和 c 奇偶性相同,形成交替模式
让我们通过具体示例验证这些模式:
- 全奇序列 [1,3,5,7]:所有相邻和都为偶数 (4,8,12),符合模式一
- 奇偶交替序列 [1,2,3,4]:所有相邻和都为奇数 (3,5,7),符合模式二
- 混合序列 [1,2,2,3]:相邻和为 3(奇)、4(偶)、5(奇),不符合任何模式
所以,有效子序列有两种基本类型:
- 所有相邻元素之和都为偶数
- 所有相邻元素之和都为奇数
我们需要分别找出这两种类型的最长子序列,然后取较大值作为答案。
📑 解法探讨
方法一:暴力枚举(理解问题本质)
最直观的思路是枚举所有可能的子序列,检查它们是否有效,并记录最长有效子序列的长度。
这个官方的测试用例可以通过,但是提交的时候会超时,我这边只是给大家提供一个思路方法。
/**
* 方法一:暴力枚举法
* 思路:枚举所有可能的子序列起点和两种类型的有效子序列,
* 构建并检查每个子序列的有效性,记录最长有效子序列长度
* 注意:此方法仅用于理解问题本质,实际提交可能会超时
*/
public class Solution {
/**
* 找出最长有效子序列的长度
* @param nums 输入的整数数组
* @return 最长有效子序列的长度
*/
public int maximumLength(int[] nums) {
int n = nums.length;
// 边界情况处理:空数组或单元素数组,直接返回数组长度
if (n <= 1) return n;
// 最小有效子序列长度为1(单个元素本身就是有效子序列)
int maxLen = 1;
// 枚举所有可能的子序列起点
for (int i = 0; i < n; i++) {
// 尝试两种类型的有效子序列
// type=0: 相邻元素之和为偶数
// type=1: 相邻元素之和为奇数
for (int type = 0; type <= 1; type++) {
int len = 1; // 当前子序列长度,至少包含起点元素
int last = nums[i]; // 记录子序列最后一个元素
// 从起点后一个元素开始构建子序列
for (int j = i + 1; j < n; j++) {
// 计算当前元素与子序列最后一个元素之和的奇偶性
int sumMod = (last + nums[j]) % 2;
// 如果符合当前类型要求,则加入子序列
if (sumMod == type) {
len++;
last = nums[j]; // 更新子序列最后一个元素
}
}
// 更新最长有效子序列长度
maxLen = Math.max(maxLen, len);
}
}
return maxLen;
}
}
复杂度分析:
- 时间复杂度:O(n²),其中 n 是数组长度
- 空间复杂度:O(1),只使用了常数额外空间
方法二:贪心算法(最优解法)
通过观察,我们可以发现两种类型的有效子序列具有明显的模式:
-
类型 0(相邻元素之和为偶数):所有元素必须具有相同的奇偶性
- 全是偶数,或全是奇数
- 最长长度 = max(偶数元素个数, 奇数元素个数)
-
类型 1(相邻元素之和为奇数):元素必须交替出现奇偶性
- 奇偶奇偶...或偶奇偶奇...
- 最长长度取决于两种模式中较长的一个
基于这些观察,我们可以设计出 O(n)时间复杂度的贪心算法:
/**
* 方法二:贪心算法(最优解法)
* 思路:通过分析有效子序列的两种模式,分别计算其最大长度
* 类型0(相邻和为偶数):所有元素奇偶性相同,取较多的那种奇偶性元素个数
* 类型1(相邻和为奇数):元素奇偶性交替出现,计算两种起始模式的最大长度
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public class Solution {
/**
* 找出最长有效子序列的长度
* @param nums 输入的整数数组
* @return 最长有效子序列的长度
*/
public int maximumLength(int[] nums) {
int n = nums.length;
// 边界情况处理:空数组或单元素数组,直接返回数组长度
if (n <= 1) return n;
// 统计数组中奇数和偶数的个数
int oddCount = 0, evenCount = 0;
for (int num : nums) {
if (num % 2 == 0) evenCount++;
else oddCount++;
}
// 类型0的最大长度:所有元素奇偶性相同,取较多的那种
int type0Max = Math.max(oddCount, evenCount);
// 如果只有一种奇偶性,无法形成类型1的子序列(需要交替出现)
if (oddCount == 0 || evenCount == 0) {
return type0Max;
}
// 计算类型1的两种模式的最大长度
// 模式1:以奇数开始的交替序列(奇-偶-奇-偶...)
int type1Max1 = calculateAlternatingLength(nums, true);
// 模式2:以偶数开始的交替序列(偶-奇-偶-奇...)
int type1Max2 = calculateAlternatingLength(nums, false);
// 取两种模式中的较大值
int type1Max = Math.max(type1Max1, type1Max2);
// 返回两种类型中的最大值
return Math.max(type0Max, type1Max);
}
/**
* 计算交替奇偶性序列的长度
* @param nums 输入的整数数组
* @param startWithOdd 是否以奇数开始
* @return 交替序列的长度
*/
private int calculateAlternatingLength(int[] nums, boolean startWithOdd) {
int length = 0; // 序列长度
boolean expectedOdd = startWithOdd; // 期望的下一个元素的奇偶性
// 遍历数组,构建交替序列
for (int num : nums) {
boolean isOdd = num % 2 == 1; // 当前元素是否为奇数
// 如果当前元素符合期望的奇偶性,则加入序列
if (isOdd == expectedOdd) {
length++;
expectedOdd = !expectedOdd; // 切换期望的奇偶性
}
}
return length;
}
}
复杂度分析:
- 时间复杂度:O(n),只需遍历数组几次
- 空间复杂度:O(1),只使用了常数额外空间
方法三:动态规划(更通用的解决方案)
这个也超时了,但是方法思路应该是这样的,之后我再想办法去优化一下。
我们也可以使用动态规划来解决这个问题,定义两个状态:
- dp0[i]:以第 i 个元素结尾,且相邻元素之和为偶数的最长有效子序列长度
- dp1[i]:以第 i 个元素结尾,且相邻元素之和为奇数的最长有效子序列长度
/**
* 方法三:动态规划(更通用的解决方案)
* 思路:定义两个状态数组,分别记录以每个位置结尾的两种类型子序列的最大长度
* dp[i][0]:以第i个元素结尾,相邻和为偶数的最长子序列长度
* dp[i][1]:以第i个元素结尾,相邻和为奇数的最长子序列长度
* 时间复杂度:O(n²),空间复杂度:O(n)
*/
public class Solution {
/**
* 找出最长有效子序列的长度
* @param nums 输入的整数数组
* @return 最长有效子序列的长度
*/
public int maximumLength(int[] nums) {
int n = nums.length;
// 边界情况处理:空数组或单元素数组,直接返回数组长度
if (n <= 1) return n;
// 动态规划数组定义:
// dp[i][0]:以第i个元素结尾,且相邻元素之和为偶数的最长有效子序列长度
// dp[i][1]:以第i个元素结尾,且相邻元素之和为奇数的最长有效子序列长度
int[][] dp = new int[n][2];
// 初始化:第一个元素本身就是长度为1的有效子序列
dp[0][0] = 1; // 以第一个元素结尾的类型0子序列
dp[0][1] = 1; // 以第一个元素结尾的类型1子序列
int maxLen = 1; // 记录最长有效子序列长度
// 从第二个元素开始计算
for (int i = 1; i < n; i++) {
// 默认情况下,子序列只包含当前元素,长度为1
dp[i][0] = 1;
dp[i][1] = 1;
// 检查前面所有元素,看是否可以形成更长的有效子序列
for (int j = 0; j < i; j++) {
// 计算nums[j]和nums[i]之和的奇偶性
int sumMod = (nums[j] + nums[i]) % 2;
// 如果前面元素j和当前元素i的和的奇偶性为sumMod
// 则可以将i添加到以j结尾的sumMod类型子序列后面
if (dp[j][sumMod] + 1 > dp[i][sumMod]) {
dp[i][sumMod] = dp[j][sumMod] + 1;
}
}
// 更新最长有效子序列长度
maxLen = Math.max(maxLen, Math.max(dp[i][0], dp[i][1]));
}
return maxLen;
}
}
复杂度分析:
- 时间复杂度:O(n²),其中 n 是数组长度
- 空间复杂度:O(n),需要存储 dp 数组
🚀 优化后的贪心算法
我们可以进一步优化贪心算法,只需要一次遍历就能计算出两种类型 1 子序列的最大长度:
/**
* 方法四:优化后的贪心算法
* 思路:在基础贪心算法的基础上,通过一次遍历同时计算两种类型1子序列的长度
* 减少了遍历次数,进一步优化了性能
* 时间复杂度:O(n),空间复杂度:O(1)
*/
public class Solution {
/**
* 找出最长有效子序列的长度
* @param nums 输入的整数数组
* @return 最长有效子序列的长度
*/
public int maximumLength(int[] nums) {
int n = nums.length;
// 边界情况处理:空数组或单元素数组,直接返回数组长度
if (n <= 1) return n;
// 统计数组中奇数和偶数的个数
int oddCount = 0, evenCount = 0;
for (int num : nums) {
if (num % 2 == 0) evenCount++;
else oddCount++;
}
// 类型0的最大长度:所有元素奇偶性相同,取较多的那种
int type0Max = Math.max(oddCount, evenCount);
// 如果只有一种奇偶性,无法形成类型1的子序列(需要交替出现)
if (oddCount == 0 || evenCount == 0) {
return type0Max;
}
// 一次遍历同时计算两种类型1子序列的最大长度
int type1OddStart = 0; // 以奇数开始的交替序列长度(奇-偶-奇-偶...)
int type1EvenStart = 0; // 以偶数开始的交替序列长度(偶-奇-偶-奇...)
boolean expectOddForOddStart = true; // 奇开始模式下期望的下一个奇偶性
boolean expectOddForEvenStart = false; // 偶开始模式下期望的下一个奇偶性
// 遍历数组,同时构建两种交替模式的子序列
for (int num : nums) {
boolean isOdd = num % 2 == 1; // 当前元素是否为奇数
// 处理奇-偶-奇-偶...模式
if (isOdd == expectOddForOddStart) {
type1OddStart++;
expectOddForOddStart = !expectOddForOddStart; // 切换期望的奇偶性
}
// 处理偶-奇-偶-奇...模式
if (isOdd == expectOddForEvenStart) {
type1EvenStart++;
expectOddForEvenStart = !expectOddForEvenStart; // 切换期望的奇偶性
}
}
// 类型1的最大长度为两种模式中的较大值
int type1Max = Math.max(type1OddStart, type1EvenStart);
// 返回两种类型中的最大值
return Math.max(type0Max, type1Max);
}
}
复杂度分析:
- 时间复杂度:O(n),只需一次遍历
- 空间复杂度:O(1),只使用了常数额外空间
🔍 算法执行流程可视化
flowchart TD
A[开始] --> B[统计奇数和偶数个数]
B --> C[计算类型0最大长度]
C --> D{是否同时有奇数和偶数?}
D -- 否 --> E[返回类型0最大长度]
D -- 是 --> F[计算两种类型1子序列长度]
F --> G[比较类型0和类型1的最大值]
G --> H[返回最终结果]
📊 示例分析
让我们通过几个示例来理解算法的工作原理:
示例 1:nums = [1,2,3,4,5]
- 奇数: 1,3,5 (count=3)
- 偶数: 2,4 (count=2)
- 类型 0 最大长度: max(3,2)=3
- 类型 1(奇-偶-奇-偶...): 1,2,3,4,5 → 长度 5
- 类型 1(偶-奇-偶-奇...): 2,3,4,5 → 长度 4
- 最终结果: max(3,5)=5
示例 2:nums = [1,3,5,7]
- 奇数: 1,3,5,7 (count=4)
- 偶数: 0 (count=0)
- 无法形成类型 1 子序列
- 最终结果: 4
示例 3:nums = [1,2,2,2,2]
- 奇数: 1 (count=1)
- 偶数: 2,2,2,2 (count=4)
- 类型 0 最大长度: max(1,4)=4
- 类型 1(奇-偶-奇-偶...): 1,2 → 长度 2
- 类型 1(偶-奇-偶-奇...): 2 → 长度 1
- 最终结果: max(4,2)=4
💡 拓展思考
问题变体
- 如果要求子序列中相邻元素之和的余数等于某个特定值 k(而不只是所有余数相等),该如何解决?(这个是紧挨着的 力扣3202 题)
- 如果允许子序列中有一个"错误"(即有一处相邻元素之和的余数与其他不同),最长有效子序列的长度会是多少?
实际应用
这个问题在信号处理和模式识别中有实际应用。例如,在分析时间序列数据时,我们可能需要找到具有特定模式的最长子序列。
算法选择策略
- 对于小规模数据,任何算法都能胜任
- 对于大规模数据,贪心算法是最佳选择,因为它具有 O(n)时间复杂度
- 动态规划方法虽然复杂度较高,但具有更好的通用性,可以解决更复杂的变体问题
📝 总结
本题考察了对子序列概念的理解以及贪心算法的应用。通过分析相邻元素之和的奇偶性规律,我们发现有效子序列有两种基本类型,并分别计算了它们的最大长度。
贪心算法是解决本题的最优选择,它基于以下关键洞察:
- 类型 0 子序列要求所有元素具有相同的奇偶性
- 类型 1 子序列要求元素的奇偶性交替出现
通过计算这两种类型的最大长度并取较大值,我们得到了问题的解。
希望今天的讲解能帮助你更好地理解这个问题和贪心算法的应用!如果你有任何疑问或想法,欢迎在评论区留言讨论。明天见!👋