[ 力扣 1124 ] 解锁最长良好时段问题:前缀和+哈希表的优雅解法

解锁最长良好时段问题:前缀和+哈希表的优雅解法l

Bilibili 同步视频

力扣 1124 解锁最长良好时段问题:前缀和+哈希表的优雅解法

在算法刷题的路上,我们总会遇到一些看似复杂、实则暗藏巧思的数组问题,最长良好时段问题就是其中的经典代表。这类问题核心考察对序列的转化能力和前缀和技巧的灵活运用,初看让人无从下手,但若掌握了"状态转化+前缀和+哈希表"的组合拳,便能迎刃而解。今天,我们就从问题本质出发,一步步拆解解题思路,用C++实现高效解法,带你吃透这一经典题型✨。

一、问题溯源:读懂最长良好时段的核心要求

首先,我们要明确问题的定义:当一个时间段内,表现良好的次数大于表现不良好的次数时,该时间段为表现良好时段,要求找到最长的这一时间段

为了方便理解,我们用具体的数值序列举例,比如题目中提到的9960669(可理解为每日表现评分,大于8为良好,小于等于8为不良),我们的目标就是从这个序列中,找到最长的连续子区间满足"良好次数>不良次数"。

问题初转化:把次数比较变成数值运算

直接统计"良好/不良次数"进行比较,会涉及大量的区间遍历和计数,时间复杂度极高。这里有一个关键巧思 :将表现良好记为+1,表现不良好记为-1

这样的转化让问题发生了本质变化:

原问题"良好次数>不良次数" → 转化后"连续子区间的和>0"

比如序列9960669转化为+1/-1序列为:[1,1,-1,-1,-1,-1,1],此时我们只需找到这个序列中和大于0的最长连续子区间,就是原问题的答案。这一步转化是解题的基石,将计数比较问题转化为了经典的数组区间和问题💡。

二、核心原理:前缀和------快速计算区间和的利器

当问题转化为"找和大于0的最长连续子区间"后,我们需要一个能快速计算任意区间和 的工具,前缀和 就是为此而生的。它能将区间和的计算时间复杂度从 O ( n ) O(n) O(n) 降至 O ( 1 ) O(1) O(1) ,是处理数组区间和问题的必备算法。

1. 前缀和的定义

设转化后的+1/-1序列为 A 0 , A 1 , ... , A n − 1 A0,A1,\dots,An-1 A0,A1,...,An−1 ,定义前缀和数组 S S S ,其中 S 0 = 0 S0=0 S0=0 , S i Si Si 表示序列 A A A 的前 i i i 项和,即:

S 0 = 0 S0 = 0 S0=0

S 1 = A 0 S1 = A0 S1=A0

S 2 = A 0 + A 1 S2 = A0+A1 S2=A0+A1

... \dots ...

S i = A 0 + A 1 + ⋯ + A i − 1 Si = A0+A1+\dots+Ai-1 Si=A0+A1+⋯+Ai−1

简单来说,前缀和数组的第 i i i 项,就是原数组前 i − 1 i-1 i−1 项的累加和,S0=0是人为初始化的哨兵值,用于简化边界计算

2. 前缀和求区间和的公式

对于原数组 A A A 的任意连续子区间 l , r l, r l,r (下标从0开始),其区间和可以通过前缀和数组快速计算:

s u m ( A l . . r ) = S r + 1 − S l sum(Al..r) = Sr+1 - Sl sum(Al..r)=Sr+1−Sl

原理图解

Plain 复制代码
原数组A:[A0, A1, A2, A3, A4]
前缀和S:[0, S1, S2, S3, S4, S5]
求A[1..3]的和:A1+A2+A3 = (A0+A1+A2+A3) - A0 = S4 - S1

从图中能清晰看到,区间 l , r l,r l,r 的和,等于前缀和数组中"后点减前点",这一特性让我们摆脱了对原数组的重复遍历。

3. 问题二次转化:前缀和数组上的新问题

结合前缀和的区间和公式,原问题"找 A A A 中sum>0的最长连续子区间 l , r l,r l,r ",可以转化为:

S r + 1 − S l > 0    ⟹    S r + 1 > S l Sr+1 - Sl > 0 \implies Sr+1 > Sl Sr+1−Sl>0⟹Sr+1>Sl

同时,子区间 l , r l,r l,r 的长度为 r − l + 1 = ( r + 1 ) − l r-l+1 = (r+1) - l r−l+1=(r+1)−l 。

因此,原问题最终转化为 :在前缀和数组 S S S 中,找到两个下标 i i i 和 j j j ( j > i j>i j>i ),满足 S j > S i Sj>Si Sj>Si ,且 j − i j-i j−i 的差值最大------这个最大差值就是原问题的答案✅。

到这里,解题的核心思路已经清晰,我们把一个看似复杂的计数问题,一步步转化为了前缀和数组上的"找最长逆序对"问题,这就是算法转化的魅力。

三、进阶优化:利用前缀和特性+哈希表降维

现在问题聚焦到了前缀和数组 S S S 上,如何高效找到满足 S j > S i Sj>Si Sj>Si 且 j − i j-i j−i 最大的 i , j i,j i,j ?

首先,我们观察到前缀和数组的特殊性质 :由于原数组 A A A 只有+1和-1,因此前缀和数组 S S S 的数值是连续变化 的------即 S i Si Si 的取值只能是 S i − 1 + 1 Si-1+1 Si−1+1 或 S i − 1 − 1 Si-1-1 Si−1−1 。比如序列[1,1,-1,-1,-1,-1,1]的前缀和数组为 S = 0 , 1 , 2 , 1 , 0 , − 1 , − 2 , − 1 S=0,1,2,1,0,-1,-2,-1 S=0,1,2,1,0,−1,−2,−1 ,数值始终在相邻整数间变化。

基于这一特性,我们可以得出一个重要结论:对于前缀和数组中的某个值 S j = n Sj=n Sj=n ,要找到最小的 i < j i<j i<j 满足 S i < n Si<n Si<n ,只需从 n − 1 n-1 n−1 开始向前找,找到第一个出现的小于 n n n 的值即可

而要快速找到"每个值第一次出现的下标",哈希表(unordered_map) 是最优选择------我们用哈希表记录前缀和数组中每个值第一次出现的下标 ,因为要让 j − i j-i j−i 最大,必须保证 i i i 尽可能小(即第一次出现的位置)。

核心思路总结

  1. 将原序列转化为+1/-1序列,把次数比较变为区间和判断;

  2. 构建前缀和数组,将区间和问题转化为前缀和的差值比较;

  3. 用哈希表记录前缀和每个值第一次出现的下标,保证后续找到的区间长度最大;

  4. 遍历前缀和数组,对每个值 S j Sj Sj ,向前找小于它的第一个值,计算 j − i j-i j−i 的最大值,即为答案。

四、代码实现:C++版高效解法

结合上述思路,我们编写C++代码,核心要点:

  1. 无需显式构建+1/-1序列和前缀和数组,用一个变量count实时计算前缀和,节省空间;

  2. 哈希表first_pos记录每个前缀和值第一次出现的下标 ,初始化first_pos[0] = -1(对应前缀和S0=0的下标);

  3. 遍历过程中,实时更新最长良好时段的长度ans

  4. 利用前缀和的连续性,仅需判断count-1是否存在,即可快速找到满足条件的前驱值。

完整C++代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;

// 求解最长良好时段
int longestWPI(vector<int>& hours) {
    int n = hours.size();
    unordered_map<int, int> first_pos; // 记录前缀和第一次出现的下标
    int count = 0; // 实时计算前缀和,替代显式的前缀和数组
    int ans = 0;   // 记录最长良好时段长度
    first_pos[0] = -1; // 初始化:前缀和0出现在下标-1(哨兵值)

    for (int i = 0; i < n; ++i) {
        // 转化为+1/-1,更新实时前缀和
        count += (hours[i] > 8) ? 1 : -1;

        // 记录当前前缀和第一次出现的下标(只记录第一次,保证i尽可能小)
        if (first_pos.find(count) == first_pos.end()) {
            first_pos[count] = i;
        }

        // 寻找count-1(利用前缀和连续性,小于count的第一个值)
        if (first_pos.find(count - 1) != first_pos.end()) {
            ans = max(ans, i - first_pos[count - 1]);
        }
    }
    return ans;
}

// 测试案例
int main() {
    vector<int> hours = {9,9,6,0,6,6,9}; // 对应题目中的9960669
    cout << "最长良好时段长度:" << longestWPI(hours) << endl; // 输出3
    return 0;
}

代码逐行讲解

  1. 变量初始化

    • first_pos:哈希表,键为前缀和值,值为该值第一次出现的下标;

    • count:实时前缀和,替代显式构建前缀和数组,空间复杂度从 O ( n ) O(n) O(n) 降至 O ( 1 ) O(1) O(1) ;

    • first_pos[0] = -1:初始化哨兵值,对应前缀和 S 0 = 0 S0=0 S0=0 ,处理边界情况(如从序列第一个元素开始的良好时段)。

  2. 遍历原数组

    • 对每个元素hours[i],大于8则count+1(良好),否则count-1(不良),完成+1/-1转化和前缀和实时计算;

    • 若当前count未在first_pos中出现过,记录其下标i------只记录第一次,保证后续找到的前驱下标尽可能小,区间长度尽可能大

  3. 寻找满足条件的前驱值

    • 利用前缀和的连续性,只需判断count-1是否存在(小于count的第一个值);

    • 若存在,计算当前下标icount-1第一次出现的下标之差,更新ans为最大值。

  4. 测试案例

    • 输入{9,9,6,0,6,6,9},对应转化后的+1/-1序列[1,1,-1,-1,-1,-1,1]

    • 最终输出3,即最长良好时段为前3个元素[9,9,6](良好2次,不良1次,2>1)。

算法性能分析

  • 时间复杂度 : O ( n ) O(n) O(n) ,仅对原数组进行一次遍历,哈希表的查找和插入操作均为 O ( 1 ) O(1) O(1) (平均情况);

  • 空间复杂度 : O ( n ) O(n) O(n) ,最坏情况下,前缀和的每个值都不重复,哈希表需要存储 n + 1 n+1 n+1 个键值对;

  • 相比暴力枚举所有区间的 O ( n 2 ) O(n^2) O(n2) 时间复杂度,该解法实现了质的提升,能高效处理大规模数组。

五、拓展思考:算法的灵活变通

本文的解法基于"前缀和+哈希表",并利用了"原序列只有+1/-1,前缀和连续"的特性做了优化,让代码更简洁。但这一思路可以推广到更通用的场景:

如果原问题的"良好/不良"转化后不是+1/-1,而是任意整数,那么只需去掉对 count-1 的判断,改为遍历哈希表中所有小于当前 count 的值,即可找到满足条件的前驱下标。当然,此时时间复杂度会略有上升,但核心思路(前缀和+哈希表记录第一次出现位置)依然适用。

此外,该问题与LeetCode 1124. 表现良好的最长时间段完全一致,本文的解法可以直接解决该题,大家可以动手测试更多案例🌐。

六、总结

最长良好时段问题的解题过程,是一次典型的算法问题转化训练:从"次数比较"到"+1/-1序列",再到"前缀和数组的差值比较",每一步转化都让问题向经典算法靠拢。而前缀和+哈希表的组合,更是处理数组区间和问题的黄金搭档,掌握这一组合,能解决一大类类似的算法题。

解题的关键从来不是死记硬背代码,而是理解为什么要转化为什么用这个数据结构

  • 转化是为了将未知问题变为已知问题;

  • 前缀和是为了快速计算区间和;

  • 哈希表是为了快速找到满足条件的前驱下标,保证区间长度最大。

希望这篇文章能让你对前缀和技巧有更深入的理解,在算法刷题的路上更上一层楼🚀!

相关推荐
程序大视界1 分钟前
【C++ 从基础到项目实战】C++(九):友元与设计模式初探——打破封装的艺术
开发语言·c++·cpp
東隅已逝,桑榆非晚3 分钟前
C语言预处理详解:从宏到条件编译
c语言·笔记·算法
hhb_6183 分钟前
Bash变量不加引号:空格文件名致命陷阱
开发语言·chrome·bash
宸津-代码粉碎机4 分钟前
Spring AI企业级RAG进阶|文档智能分片调优、ES深度整合、接口限流熔断监控生产实战
java·开发语言·人工智能·后端·spring·elasticsearch·oracle
唐青枫5 分钟前
Java MyBatis-Flex 实战指南:从 BaseMapper 到 QueryWrapper 的轻量 ORM 用法
java·mybatis
两年半的个人练习生^_^7 分钟前
JVM进阶系列:彻底理解 Java 内存模型(JMM)
java·开发语言
cpp_25017 分钟前
P10377 [GESP202403 六级] 好斗的牛
数据结构·c++·算法·题解·洛谷·gesp六级
邪修king7 分钟前
C++ 红黑树自平衡核心:旋转变色、规则详解与 STL 选型逻辑
数据结构·c++·b树·算法
一个博客1 小时前
pdf-viewer 实现预览pdf文件
开发语言·javascript·pdf
一位代码1 小时前
微软开源项目MarkitDown:一款将pdf/word/ppt等各类文件转换为Markdown格式的python工具
python