力扣每日一题--2025.7.16

106 阅读12分钟

📚 力扣每日一题--2025.7.16

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

今天我们要解决的是力扣上的第 3201 题——找出有效子序列的最大长度 I。这道题虽然标记为中等难度,但只要掌握了正确的思路,就能轻松解决!

📝 题目描述

image.png

image.png

🤔 思路分析

核心需求推导过程 📝

题目要求子序列中所有相邻元素之和的奇偶性必须相同,即:

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

这个条件可以从两个角度理解:

  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(奇),不符合任何模式

所以,有效子序列有两种基本类型:

  1. 所有相邻元素之和都为偶数
  2. 所有相邻元素之和都为奇数

我们需要分别找出这两种类型的最长子序列,然后取较大值作为答案。

📑 解法探讨

方法一:暴力枚举(理解问题本质)

最直观的思路是枚举所有可能的子序列,检查它们是否有效,并记录最长有效子序列的长度。

这个官方的测试用例可以通过,但是提交的时候会超时,我这边只是给大家提供一个思路方法。

/**
 * 方法一:暴力枚举法
 * 思路:枚举所有可能的子序列起点和两种类型的有效子序列,
 *       构建并检查每个子序列的有效性,记录最长有效子序列长度
 * 注意:此方法仅用于理解问题本质,实际提交可能会超时
 */
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),只使用了常数额外空间
方法二:贪心算法(最优解法)

通过观察,我们可以发现两种类型的有效子序列具有明显的模式:

  1. 类型 0(相邻元素之和为偶数):所有元素必须具有相同的奇偶性

    • 全是偶数,或全是奇数
    • 最长长度 = max(偶数元素个数, 奇数元素个数)
  2. 类型 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

💡 拓展思考

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

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

算法选择策略
  • 对于小规模数据,任何算法都能胜任
  • 对于大规模数据,贪心算法是最佳选择,因为它具有 O(n)时间复杂度
  • 动态规划方法虽然复杂度较高,但具有更好的通用性,可以解决更复杂的变体问题

📝 总结

本题考察了对子序列概念的理解以及贪心算法的应用。通过分析相邻元素之和的奇偶性规律,我们发现有效子序列有两种基本类型,并分别计算了它们的最大长度。

贪心算法是解决本题的最优选择,它基于以下关键洞察:

  1. 类型 0 子序列要求所有元素具有相同的奇偶性
  2. 类型 1 子序列要求元素的奇偶性交替出现

通过计算这两种类型的最大长度并取较大值,我们得到了问题的解。

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