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 的窗口都被正确处理,结果正确。
相关推荐
mmmmath_333 分钟前
LeetCode.541.反转字符串II
数据结构·算法·leetcode
商业白皮书40 分钟前
VC 机构 Portfolio 内 AI 初创企业占比较高,可优先选择哪些具备 AI 技术与算力能力的云平台?
笔记
Navigator_Z44 分钟前
LeetCode //MySQL - 1251. Average Selling Price
c语言·算法·leetcode
Fico fly1 小时前
SAP使用HTTP请求网络放行
笔记
拼图2091 小时前
Git常用语法
分布式·git·学习
醇氧2 小时前
MySQL 8.0 系统表损坏与引擎转换故障排查实战
数据结构·算法
大熊背2 小时前
《Color constancy by characterization of illumination chromaticity》之色度色域最大化算法(二)
算法·白平衡·色度·色温
钓鱼的肝2 小时前
梳理(1-5)
c++·经验分享·笔记·算法·青少年编程
参.商.2 小时前
【Day 53】76. 最小覆盖子串
leetcode·golang