从混乱中寻找节拍:我的三种解法演进之路(3201. 找出有效子序列的最大长度 I)


从混乱中寻找节拍:我的三种解法演进之路😎

嘿,各位热爱代码的朋友们!我是你们的老朋友,一名总在寻找代码优雅之道的开发者。今天,我想和大家聊一个发生在我真实项目里的故事,关于如何从一堆看似混乱无序的数据中,抽丝剥茧,找到其中最长、最和谐的"节拍"。

我遇到了什么问题?

最近,我在为一个物联网(IoT)项目开发固件,任务是处理一个低功耗传感器传来的一连串数据包。为了极度省电,这个传感器的协议设计得有点"奇葩":它会不间断地发来两种类型的数据包,我们权且称之为"A包"(我们用奇数代表)和"B包"(我们用偶数代表)。

这些数据包混杂在一起,形成一个长长的数据流。然而,并不是所有的数据包序列都是有效的。根据协议规定,一个有效的传输序列 必须满足一个"和谐"的条件:序列中任意相邻两个包的类型组合,其"和谐度"必须始终如一。这里的"和谐度"被定义为两个包类型值之和的奇偶性。

说白了,就是要么序列中任意相邻两个包的类型值加起来都是偶数,要么加起来都是奇数。

由于信号干扰,我们收到的数据流 nums 是"脏"的,里面混杂了很多噪声。我的任务就是从这个混乱的 nums 数组中,提取出最长的一段有效传输子序列

举个例子,如果收到 [1, 2, 1, 1, 2, 1, 2](A, B, A, A, B, A, B),我需要找出其中最长的有效序列。(3201. 找出有效子序列的最大长度 I

这个问题一度让我头大 😭。我该如何高效地找到这个最长的"完美节拍"呢?

我是如何解决的:解法的演进

一开始,我差点就掉进了动态规划的复杂深渊,想着 dp[i][j] 表示什么...但很快我就意识到了问题的提示:数据流的长度 nums.length 最高可达20万!O(N^2) 的解法绝对会超时。我必须找到线性的解决方案。

我决定静下心来,仔细分析那个核心的"和谐"条件:(sub[i] + sub[i+1]) % 2 == 常数

💡 恍然大悟的瞬间来了! 这个看似复杂的条件,其实只描述了两种底层模式:

  1. 和为偶数 :要求相邻两个数的奇偶性相同 。这意味着,整个有效子序列必须是"纯色"的,要么全是奇数 ([A, A, A, ...]),要么全是偶数 ([B, B, B, ...])。
  2. 和为奇数 :要求相邻两个数的奇偶性不同 。这意味着,整个有效子序列必须是"交替色"的,比如 [A, B, A, B, ...] 或者 [B, A, B, A, ...]

所以,我的任务被清晰地分解成了寻找这四种特定模式的最长子序列。我的解法也经历了三次迭代升级。

解法一:直观分治法,清晰易懂

最直观的方法,就是把这四种情况分开计算,用贪心法找到每种模式的最长长度。

  • 对于"全奇/全偶"模式:这太简单了,不就是数数嘛!遍历一遍数组,统计奇数和偶数的总数即可。
  • 对于"交替"模式:我们可以用一个"期望值"来贪心地遍历数组。比如,我想找"奇偶交替",我首先期望找到一个奇数,找到后,我的期望就变为偶数,然后再继续向后找...

这种"分而治之"的思路让代码的可读性变得非常好,是解决问题的第一步。

java 复制代码
/*
 * 思路:分治与贪心。将问题分解为"同奇偶性"和"交替奇偶性"两种模式。
 * 1. 同奇偶性模式:直接计算数组中奇数和偶数的总数,取最大值。
 * 2. 交替奇偶性模式:使用贪心策略,分别计算以奇数开头和以偶数开头的最长交替序列,取最大值。
 * 3. 最终答案是所有这些可能性中的最大值。
 * 时间复杂度:O(N),我们需要对数组进行数次单遍扫描。
 * 空间复杂度:O(1),只使用常数个额外变量。
 */
class Solution {
    public int maximumLength(int[] nums) {
        // --- Part 1: 计算同奇偶性模式的最长长度 ---
        int oddCount = 0;
        int evenCount = 0;
        for (int num : nums) {
            if (num % 2 == 1) {
                oddCount++;
            } else {
                evenCount++;
            }
        }
        int maxSameParityLength = Math.max(oddCount, evenCount);

        // --- Part 2: 计算交替奇偶性模式的最长长度 ---
        
        // 2.1 寻找以奇数开头的最长交替序列 (奇, 偶, 奇, ...)
        int alternatingLength1 = 0;
        // expectedParity: 0 for even, 1 for odd
        int expectedParity1 = 1; 
        for (int num : nums) {
            if (num % 2 == expectedParity1) {
                alternatingLength1++;
                // 找到后,期望的奇偶性反转。用 1 - x 来实现 0和1的切换是一种常见的技巧。
                expectedParity1 = 1 - expectedParity1;
            }
        }
        
        // 2.2 寻找以偶数开头的最长交替序列 (偶, 奇, 偶, ...)
        int alternatingLength2 = 0;
        int expectedParity2 = 0;
        for (int num : nums) {
            if (num % 2 == expectedParity2) {
                alternatingLength2++;
                expectedParity2 = 1 - expectedParity2;
            }
        }
        int maxAlternatingLength = Math.max(alternatingLength1, alternatingLength2);

        // --- Part 3: 最终结果 ---
        return Math.max(maxSameParityLength, maxAlternatingLength);
    }
}

解法二:动态规划思想,单次遍历的奇迹

虽然解法一可行,但需要多次遍历数组。我心想:能不能在一次遍历中搞定所有事?答案是肯定的!这就要用到动态规划的思想,但我们可以用几个变量来压缩状态,避免开一个大数组。

我定义了四个变量,分别代表四种状态的当前最长长度:

  • sameOddLen:全奇模式的长度。
  • sameEvenLen:全偶模式的长度。
  • altOddLen:以奇数结尾的交替模式的长度。
  • altEvenLen:以偶数结尾的交替模式的长度。

遍历数组时,根据当前数字的奇偶性,同步更新这四个状态。比如,当我遇到一个奇数 num

  • 它显然可以让 sameOddLen 加一。
  • 同时,它还可以接在任何一个"以偶数结尾的交替序列"后面,形成一个更长的"以奇数结尾的交替序列"。所以,新的 altOddLen 就是旧的 altEvenLen + 1

就这样,一次遍历就能追踪所有可能性的变化,效率极高!

java 复制代码
/*
 * 思路:动态规划状态压缩。使用四个变量模拟DP状态,分别记录同奇偶性和交替奇偶性两种模式下,
 * 以奇数或偶数结尾的最长子序列长度。通过一次遍历更新这四个状态,最终取最大值。
 */
public int maximumLengthWithDP(int[] nums) {
    // sameOddLen: 模式一(全奇)的最长长度
    int sameOddLen = 0;
    // sameEvenLen: 模式一(全偶)的最长长度
    int sameEvenLen = 0;
  
    // altOddLen: 模式二中,以奇数结尾的最长长度
    int altOddLen = 0;
    // altEvenLen: 模式二中,以偶数结尾的最长长度
    int altEvenLen = 0;

    for (int num : nums) {
        // 使用 num % 2 是判断奇偶性的标准且最高效的方式。
        if (num % 2 == 1) { // 当前数字是奇数
            // 全奇模式,长度直接加1
            sameOddLen += 1;
            // 新的以奇数结尾的交替序列,可以由任何以偶数结尾的交替序列加上当前数得到
            altOddLen = altEvenLen + 1;
        } else { // 当前数字是偶数
            // 全偶模式,长度直接加1
            sameEvenLen += 1;
            // 新的以偶数结尾的交替序列,可以由任何以奇数结尾的交替序列加上当前数得到
            altEvenLen = altOddLen + 1;
        }
    }
  
    // 最终结果是所有可能性中的最大值。
    // Math.max嵌套使用可以找出多个数中的最大值。
    return Math.max(Math.max(sameOddLen, sameEvenLen), Math.max(altOddLen, altEvenLen));
}

解法三:模式驱动的艺术,极致优雅

解法二已经很棒了,但代码里还是有 if-else 分支。我追求的是极致的优雅,能不能把分支也去掉?答案是可以的,通过数据驱动设计

我发现,所有四种模式都可以被一个统一的"模板"来描述。我们可以用一个二维数组来定义这些模式:

java 复制代码
/*
 * 思路:模式化贪心算法。此解法将所有四种可能的有效子序列模式抽象成一个二维数组 `patterns`。
 * 然后通过一个统一的循环结构,对每种模式进行贪心匹配,最终返回所有模式结果中的最大值。
 * 这种方法将多种情况的逻辑高度统一,代码极为精简。
 */
public int maximumLength(int[] nums) {
    int res = 0;
    // 'patterns' 是本算法的核心。它定义了所有需要寻找的目标模式。
    // {0,0}代表全偶, {1,1}代表全奇, {1,0}代表奇偶交替, {0,1}代表偶奇交替
    int[][] patterns = {{0, 0}, {0, 1}, {1, 0}, {1, 1}};
  
    // 外层循环:遍历每一种预设的模式。
    for (int[] pattern : patterns) {
        int cnt = 0;
        // 内层循环:对整个输入数组进行一次贪心扫描,寻找匹配当前模式的元素。
        for (int num : nums) {
            // `pattern[cnt % 2]`:根据已找到的子序列长度(cnt),决定我们下一个期望的数字是奇是偶。
            if (num % 2 == pattern[cnt % 2]) {
                cnt++; // 如果匹配,子序列长度加一
            }
        }
        res = Math.max(res, cnt); // 更新全局最大长度
    }
    return res;
}

这段代码是不是感觉B格瞬间高了?它把所有重复的逻辑都抽象出来,用数据(patterns数组)来驱动算法。pattern[cnt % 2] 这行代码简洁地表达了"根据当前已匹配的长度,决定下一个期望的奇偶性",简直是神来之笔!

解法大比拼

特性 解法一 (直观分治) 解法二 (动态规划思想) 解法三 (模式驱动)
核心思想 分情况讨论,独立求解 单次遍历,同步更新所有状态 数据驱动,用统一算法处理所有模式
代码可读性 ⭐⭐⭐⭐⭐ (极高) ⭐⭐⭐⭐ (较高) ⭐⭐⭐ (需要理解模式)
代码优雅度 ⭐⭐ (一般) ⭐⭐⭐⭐ (优雅) ⭐⭐⭐⭐⭐ (极致优雅)
性能 O(N)时间, O(1)空间 (多次遍历) O(N)时间, O(1)空间 (单次遍历) O(N)时间, O(1)空间 (多次遍历)
抽象级别 低,逻辑具体 中,状态转移 高,逻辑与数据分离

总结: 三种方法都是正确且高效的。

  • 解法一 最适合初学者或需要快速实现原型时,逻辑清晰第一。
  • 解法二 是性能上的最优选(理论上常数时间最小),展现了良好的优化思路。
  • 解法三 是代码工程艺术的体现,它告诉我们优秀的设计可以用数据来简化逻辑,是资深开发者的追求。

举一反三,触类旁通

这种将具体逻辑抽象成"模式",再用统一算法处理的思想,在开发中非常有用:

  1. UI主题切换:一个App有多套主题(日间、夜间、商务蓝...),每套主题就是一种"模式",包含颜色、字体、图标等一系列配置。切换主题时,我们只需传入不同的"模式"配置,UI渲染引擎这套"统一算法"就能应用整套样式。
  2. 网络协议解析:解析不同版本的通信协议。每个版本都可以看作一种"模式",有其特定的包头、数据段和校验规则。我们可以写一个通用的解析器,由传入的"版本模式"来指导它如何解析数据流。

更多练手机会

如果你对这类子序列和贪心问题感兴趣,强烈推荐下面几道LeetCode题目:

希望我这次从真实问题出发的分享,能帮助你更好地理解算法在实践中的应用和魅力。下次遇到看似复杂的问题时,不妨也试试退一步,看看是否能从中提炼出简单的"模式"!😉

相关推荐
ZLRRLZ24 分钟前
【数据结构】二叉树进阶算法题
数据结构·c++·算法
arin8762 小时前
【图论】倍增与lca
算法·图论
CoovallyAIHub2 小时前
别卷单模态了!YOLO+多模态 才是未来场景实战的“天选方案”
深度学习·算法·计算机视觉
guozhetao2 小时前
【图论,拓扑排序】P1347 排序
数据结构·c++·python·算法·leetcode·图论·1024程序员节
慕雪_mx2 小时前
最短路算法
算法
AI_Keymaker2 小时前
对话DeepMind创始人Hassabis:AGI、宇宙模拟与人类文明的下一个十年
算法
yi.Ist2 小时前
关于二进制的规律
算法·二进制·bitset
88号技师3 小时前
2025年7月一区SCI-投影迭代优化算法Projection Iterative Methods-附Matlab免费代码
开发语言·人工智能·算法·机器学习·matlab·优化算法
草香农3 小时前
SHA-3算法详解
算法
花海如潮淹3 小时前
量子算法可视化工具:撕裂量子黑箱的破壁者
经验分享·笔记·算法·量子计算