Leetcode 66 几乎唯一子数组的最大和

1 题目

2841. 几乎唯一子数组的最大和

给你一个整数数组 nums 和两个正整数 mk

请你返回 nums 中长度为 k几乎唯一 子数组的 最大和 ,如果不存在几乎唯一子数组,请你返回 0

如果 nums 的一个子数组有至少 m 个互不相同的元素,我们称它是 几乎唯一 子数组。

子数组指的是一个数组中一段连续 非空 的元素序列。

示例 1:

复制代码
输入:nums = [2,6,7,3,1,7], m = 3, k = 4
输出:18
解释:总共有 3 个长度为 k = 4 的几乎唯一子数组。分别为 [2, 6, 7, 3] ,[6, 7, 3, 1] 和 [7, 3, 1, 7] 。这些子数组中,和最大的是 [2, 6, 7, 3] ,和为 18 。

示例 2:

复制代码
输入:nums = [5,9,9,2,4,5,4], m = 1, k = 3
输出:23
解释:总共有 5 个长度为 k = 3 的几乎唯一子数组。分别为 [5, 9, 9] ,[9, 9, 2] ,[9, 2, 4] ,[2, 4, 5] 和 [4, 5, 4] 。这些子数组中,和最大的是 [5, 9, 9] ,和为 23 。

示例 3:

复制代码
输入:nums = [1,2,1,2,1,2,1], m = 3, k = 3
输出:0
解释:输入数组中不存在长度为 k = 3 的子数组含有至少  m = 3 个互不相同元素的子数组。所以不存在几乎唯一子数组,最大和为 0 。

提示:

  • 1 <= nums.length <= 2 * 104
  • 1 <= m <= k <= nums.length
  • 1 <= nums[i] <= 109

2 代码实现

cpp 复制代码
class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        int n = nums.size();
        unordered_map<int ,int > count ;
        long long sum = 0 ;
        long long max_sum = 0 ;
        int unique = 0 ;
        int left =0;
        int right = 0;

        while (right < n ){
            int c = nums[right];

            if(count[c] == 0 ){
                unique++;
            }
            count[c]++;
            sum+= c ;
            right++ ;

            if (right - left == k ){
                if (unique >= m ){
                    if (sum > max_sum){
                        max_sum = sum ;
                    }
                }
                
            int d = nums[left];
            count[d] --;
            sum -= d ;
            if (count[d] == 0){
                unique--;
            }
            left++;
            }

        }
        return max_sum ;
    }
};

题解

框架

cpp 复制代码
// 滑动窗口算法伪码框架
void slidingWindow(string s) {
    // 用合适的数据结构记录窗口中的数据,根据具体场景变通
    // 比如说,我想记录窗口中元素出现的次数,就用 map
    // 如果我想记录窗口中的元素和,就可以只用一个 int
    auto window = ...
 
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        window.add(c);
        // 增大窗口
        right++;
 
        // 进行窗口内数据的一系列更新
        ...
 
        // *** debug 输出的位置 ***
        printf("window: [%d, %d)\n", left, right);
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
 
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            window.remove(d);
            // 缩小窗口
            left++;
 
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

先明确框架与题目的对应关系

框架是通用的,我们需要先把题目中的 "窗口规则" 映射到框架的各个部分:

框架组件 题目中的具体含义
窗口数据结构 window 需记录 2 件事: ① 窗口内各元素的出现次数(判断唯一元素个数); ② 窗口内元素和(计算最大和)。用哈希表 count 记录次数,用变量 sum 记录和。
窗口边界 left, right 窗口固定长度为 kright 负责扩展窗口,left 负责在窗口长度超 k 时收缩(保持窗口长度为 k)。
扩展窗口(right++ nums[right] 加入窗口,更新 count(次数 + 1)和 sum(和 + 1)。
收缩窗口条件 (needs shrink 窗口长度 right - left == k 时,无需继续收缩(因为窗口固定长度,每次扩展后只收缩 1 次),所以这里是 if 逻辑(而非 while)。
收缩窗口(left++ nums[left] 移出窗口,更新 count(次数 - 1,若次数为 0 则减少唯一元素个数)和 sum(和 - 1)。
窗口内数据更新 每次窗口稳定(长度为 k)后,判断唯一元素个数是否 ≥ m,若满足则更新最大和。

按照框架拆解解题步骤

我们将代码和框架一一对应,分步骤讲解:

1. 初始化变量(对应框架开头)
cpp 复制代码
int n = nums.size();
unordered_map<int, int> count;  // 窗口数据结构:记录元素出现次数
long long sum = 0;              // 窗口数据结构:记录窗口元素和
long long max_sum = 0;          // 结果:最大几乎唯一子数组和
int unique = 0;                 // 窗口内互不相同元素的个数(派生自 count)
int left = 0, right = 0;        // 窗口边界
2. 扩展窗口(right 右移,对应框架 while (right < s.size())

循环条件:right < n(遍历整个数组),每次将 nums[right] 加入窗口:

cpp 复制代码
while (right < n) {
    // c 是将移入窗口的元素(对应框架 `char c = s[right]`)
    int c = nums[right];
    // 窗口添加元素:更新 count 和 sum(对应框架 `window.add(c)`)
    if (count[c] == 0) {
        unique++;  // 新元素,唯一元素个数+1
    }
    count[c]++;    // 元素出现次数+1
    sum += c;      // 窗口和+1
    // 增大窗口(对应框架 `right++`)
    right++;

    // 3. 窗口内数据更新(扩展后暂未收缩,窗口长度可能不足 k,暂不判断结果)
    // (此时窗口长度为 `right - left`,可能小于 k,无需处理)

    // 4. 判断是否需要收缩窗口(对应框架 `while (window needs shrink)`)
    // 窗口长度 == k 时,需要收缩 left 保持窗口长度固定(固定长度窗口,收缩 1 次即可,用 if)
    if (right - left == k) {
        // 窗口稳定(长度为 k),判断是否满足"几乎唯一"条件,更新结果
        if (unique >= m) {
            if (sum > max_sum) {
                max_sum = sum;
            }
        }

        // 收缩窗口:移除 left 元素(对应框架 `char d = s[left]` 和 `window.remove(d)`)
        int d = nums[left];
        count[d]--;  // 元素出现次数-1
        sum -= d;    // 窗口和-1
        if (count[d] == 0) {
            unique--;  // 元素次数为 0,唯一元素个数-1
        }
        // 缩小窗口(对应框架 `left++`)
        left++;
    }
}
3. 最终返回结果
cpp 复制代码
return max_sum;

框架适配关键点说明

  1. 固定长度窗口的收缩逻辑 :框架中的收缩是 while 循环(应对可变长度窗口),但本题窗口长度固定为 k,所以收缩条件是 right - left == k(窗口刚达到长度 k),收缩时只移动 left 一次(if 而非 while),确保窗口始终保持长度 k

  2. 窗口数据结构的设计 :框架中的 window 是抽象的,本题根据需求拆分为 count(记录次数)和 sum(记录和),再派生 unique 变量(记录唯一元素个数),都是为了快速获取窗口状态(无需遍历窗口计算)。

  3. 结果更新时机 :只有当窗口稳定(长度为 k)时,才判断是否满足 "至少 m 个唯一元素",此时计算的和才是长度为 k 的子数组和,符合题目要求。

完整代码(贴合框架版)

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

class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        int n = nums.size();
        // 窗口数据结构:count 记录元素出现次数,sum 记录窗口和
        unordered_map<int, int> count;
        long long sum = 0;
        long long max_sum = 0;
        int unique = 0;  // 窗口内互不相同元素个数(派生自 count)
        
        int left = 0, right = 0;
        while (right < n) {
            // 1. 移入右侧元素,扩展窗口
            int c = nums[right];
            if (count[c] == 0) {
                unique++;
            }
            count[c]++;
            sum += c;
            right++;  // 增大窗口

            // 2. 判断是否需要收缩窗口(固定长度 k,达到长度则收缩)
            if (right - left == k) {
                // 3. 窗口稳定,更新结果(判断是否为几乎唯一子数组)
                if (unique >= m) {
                    max_sum = max(max_sum, sum);
                }

                // 4. 移出左侧元素,收缩窗口
                int d = nums[left];
                count[d]--;
                sum -= d;
                if (count[d] == 0) {
                    unique--;
                }
                left++;  // 缩小窗口
            }
        }
        return max_sum;
    }
};

核心逻辑总结(对照框架)

框架步骤 本题具体执行
初始化窗口数据和边界 初始化 countsumleft=0right=0
扩展窗口(right++ 加入 nums[right],更新 countsumunique
判断收缩条件 窗口长度 == k(固定长度,必须收缩以保持窗口大小)
窗口内数据更新(结果) 窗口稳定后,判断 unique >= m,更新最大和
收缩窗口(left++ 移除 nums[left],更新 countsumunique
循环直至数组遍历结束 right < n 循环结束,返回最大和

这种适配方式完全遵循了滑动窗口的通用框架,只是根据 "固定窗口长度" 的题目特点,将收缩逻辑从 while 改为 if,同时按需设计了窗口数据结构(count+sum),让代码逻辑更清晰、更贴合框架思想。

错误百出的代码

cpp 复制代码
class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        int n = nums.size();
        unordered_map<int ,int > count ;
        long long sum = 0 ;
        long long max_sum = 0 ;
        int unique = 0 ;
        int left , right = 0;

        while (right < n ){
            int c = nums[right];

            if(count[c] == 0 ){
                unique++;
            }
            count[c]++;
            sum+= c ;
            right++ ;

            if (right - left == k ){
                if (unique >= m ){
                    if (sum > max_sum){
                        max_sum = sum ;
                    }
                }
            }

            int d = nums[left];
            count[d] --;
            sum -= d ;
            if (count[d] == 0){
                unique--;
            }
            left++;
        }
        return max_sum ;
    }
};

要解决这个问题,我们需要找到数组中长度为 k 且包含至少 m 个不同元素的子数组的最大和。你的代码思路是正确的(滑动窗口 + 哈希表统计),但存在窗口维护逻辑错误,导致结果不正确。下面先分析错误,再详细解释核心逻辑和正确实现。

一、你的代码哪里错了?

核心错误:窗口还没形成长度 k 就提前移动左指针,导致窗口始终不满足长度要求

你的代码逻辑:

cpp 复制代码
while (right < n ){
    // 右指针移动,加入元素(正确)
    ... 
    right++ ;

    // 检查窗口长度是否为k(正确)
    if (right - left == k ){ ... }

    // 无论窗口长度是否为k,都移动左指针(错误!)
    count[d] --;
    sum -= d ;
    ...
    left++;
}

比如当 k=3 时:

  • right=0:加入元素,right=1,窗口长度 1-0=1 <3,但你直接移动左指针 left=1,窗口长度变为 0
  • 后续循环中,窗口永远无法达到长度 k,自然无法正确计算符合条件的子数组和。

修正原则 :只有当窗口长度 超过 k 时,才移动左指针缩小窗口(保证窗口始终是长度为 k 的固定窗口)。

二、核心逻辑详解(滑动窗口 + 唯一元素统计)

1. 问题本质
  • 固定窗口长度 k:我们只关注所有长度为 k 的子数组(滑动窗口的 "固定窗口" 模式);
  • 约束条件:子数组中不同元素的个数 ≥ m
  • 目标:找到满足约束的子数组中,元素和最大的那个。
2. 关键问题:如何高效统计窗口内的 "唯一元素个数"?

哈希表(unordered_map) 统计窗口内每个元素的出现次数:

  • 哈希表的 key:窗口中的元素值;
  • 哈希表的 value:该元素在窗口中出现的次数;
  • 辅助变量 unique:记录窗口内不同元素的个数(避免每次遍历哈希表统计,提升效率)。
3. 唯一元素统计的核心逻辑
  • 元素加入窗口(右指针移动) :当把元素 nums[right] 加入窗口时:

    • 如果哈希表中该元素的计数为 0(说明是新加入的唯一元素),则 unique++
    • 然后将该元素的计数 +1,并累加元素和 sum
  • 元素移出窗口(左指针移动) :当窗口长度超过 k(需要移除左边界元素 nums[left])时:

    • 将该元素的计数 -1
    • 如果计数变为 0(说明该元素已完全移出窗口,不再是窗口内的元素),则 unique--
    • 最后减去该元素的值,更新 sum,并移动左指针。
4. 为什么这样统计 "唯一元素"?
  • unique 直接记录 "当前窗口中出现过的不同元素的数量",比每次遍历哈希表(count.size())更高效(O (1) 操作);
  • 只有 "元素首次加入窗口" 时,unique 才会增加;只有 "元素完全移出窗口" 时,unique 才会减少,确保统计准确。

第二次依旧写错

cpp 复制代码
class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        int n = nums.size();
        unordered_map<int ,int > count ;
        long long sum = 0 ;
        long long max_sum = 0 ;
        int unique = 0 ;
        int left , right = 0;

        while (right < n ){
            int c = nums[right];

            if(count[c] == 0 ){
                unique++;
            }
            count[c]++;
            sum+= c ;
            right++ ;

            if (right - left == k ){
                if (unique >= m ){
                    if (sum > max_sum){
                        max_sum = sum ;
                    }
                }
                
            int d = nums[left];
            count[d] --;
            sum -= d ;
            if (count[d] == 0){
                unique--;
            }
            left++;
            }

        }
        return max_sum ;
    }
};

你的思路其实已经很接近了!只需要 把 "移动左指针、移除左元素" 的逻辑,放进 "窗口长度等于 k" 的判断里 即可 ------ 这样就不会出现 "窗口没形成就提前缩窗" 的问题。

下面是微调后的正确代码(只改了 3 行,核心是调整代码块的缩进):

cpp 复制代码
class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        int n = nums.size();
        unordered_map<int, int> count;
        long long sum = 0;
        long long max_sum = 0;
        int unique = 0;
        int left = 0, right = 0; // 修正:left初始化0(原代码只给right赋值,left是随机值,会出错!)

        while (right < n) {
            int c = nums[right];

            if (count[c] == 0) {
                unique++;
            }
            count[c]++;
            sum += c;
            right++;

            // 关键:只有窗口长度等于k时,才判断是否更新max_sum,然后缩窗
            if (right - left == k) {
                if (unique >= m) {
                    if (sum > max_sum) {
                        max_sum = sum;
                    }
                }
                // 缩窗逻辑:移除左元素(必须放在if里面!)
                int d = nums[left];
                count[d]--;
                sum -= d;
                if (count[d] == 0) {
                    unique--;
                }
                left++;
            }
        }
        return max_sum;
    }
};

只改了两处关键微调(原代码的核心问题):

  1. left 初始化原代码 int left , right = 0; 中,left 没有初始化(是随机垃圾值),会导致 right - left 计算错误,必须改成 int left = 0, right = 0;
  2. 缩窗逻辑缩进 :原代码中 "移除左元素、移动 left" 的逻辑在 if (right - left == k) 外面,导致窗口没形成就缩窗;现在把这部分代码放进 if 里面,只有窗口长度等于 k 时才缩窗,保证窗口始终是 "长度为 k" 的固定窗口。

为什么这样微调就对了?

  • 窗口形成条件:right - left == k(因为 right 已经先移动了,窗口长度是 right - left)。
  • 只有窗口形成后,才做两件事:
    1. 检查是否满足 "唯一元素≥m",如果满足就更新最大和;
    2. 缩窗(移除左边界元素,移动 left),为下一个窗口做准备。
  • 这样遍历下来,每个 "长度为 k" 的子数组都会被检查,且不会漏判或误判。

再用小例子验证(nums=[2,6,7,3,1,7], m=3, k=4):

  • right 从 0 到 3 时,right - left = 4(窗口形成):
    • 窗口元素 [2,6,7,3],unique=4≥3,sum=18,max_sum=18;
    • 然后移除左元素 2,left=1,sum=16,unique=3。
  • right=4 时,right - left = 4(窗口 [6,7,3,1]):
    • unique=4≥3,sum=17,max_sum 保持 18;
    • 移除左元素 6,left=2,sum=11,unique=3。
  • right=5 时,right - left =4(窗口 [7,3,1,7]):
    • unique=3≥3,sum=18,max_sum 保持 18?不对,原数组最后一个窗口是 [3,1,7,7]?哦,重新算:其实 right=5 时,窗口是 [7(index2),3(3),1(4),7(5)],sum=7+3+1+7=18,确实和之前一样。
  • 整个过程每个长度为 k 的窗口都被正确处理,结果正确。
相关推荐
敲代码的嘎仔43 分钟前
LeetCode面试HOT100—— 206. 反转链表
java·数据结构·学习·算法·leetcode·链表·面试
丝斯20111 小时前
AI学习笔记整理(19)—— AI核心技术(深度学习3)
人工智能·笔记·学习
自然语1 小时前
深度学习时代结束了,2025年开始只剩下轮廓
数据结构·人工智能·深度学习·学习·算法
海天一色y1 小时前
Leetcode07-整数反转
算法
Aspect of twilight1 小时前
华为华为AI岗实习面试算法题
算法·华为·面试
客梦1 小时前
数据结构-图结构
java·数据结构·笔记
让学习成为一种生活方式1 小时前
基因组结构注释实战案例1--随笔14
人工智能·算法·机器学习
岳来1 小时前
lscpu 命令学习
学习·lscpu
TomCode先生1 小时前
2个月精通SaaS MES系统业务知识学习计划(每天2小时)
学习