从暴力到优化:统计最大元素至少出现K次的子数组数目

问题描述

给定一个整数数组 nums 和一个正整数 k,我们需要统计满足以下条件的子数组数目:子数组中的最大元素至少出现 k 次。子数组是指数组中一个连续的元素序列。

初步思考:暴力解法

首先,我想到最直观的解法是枚举所有可能的子数组,然后检查每个子数组是否满足条件。

暴力解法思路

  1. 遍历所有可能的子数组起始位置 i

  2. 对于每个起始位置 i,扩展子数组的结束位置 j

  3. 在扩展过程中维护当前子数组的最大值及其出现次数

  4. 当最大值出现次数达到 k 时,统计剩余可能的子数组数目

暴力解法代码

java

public long countSubarrays(int[] nums, int k) {
    long count = 0;
    int n = nums.length;
    
    for (int i = 0; i < n; i++) {
        int max = nums[i];
        int maxCount = 0;
        for (int j = i; j < n; j++) {
            if (nums[j] > max) {
                max = nums[j];
                maxCount = 1;
            } else if (nums[j] == max) {
                maxCount++;
            }
            
            if (maxCount >= k) {
                count += n - j;
                break;
            }
        }
    }
    
    return count;
}

暴力解法分析

  • 时间复杂度:O(n²),因为需要双重循环遍历所有子数组

  • 空间复杂度:O(1)

  • 问题:当数组长度较大时(如n=10⁵),这种解法会超时

优化思路:滑动窗口

考虑到暴力解法的时间复杂度过高,我开始思考如何优化。观察到题目要求的是"最大元素至少出现k次",这提示我们可以利用滑动窗口技术来优化。

滑动窗口解法思路

  1. 首先找到数组中的最大值 mx

  2. 记录所有 mx 出现的位置

  3. 使用滑动窗口统计满足条件的子数组数目

滑动窗口解法代码

java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Solution {
    public long countSubarrays(int[] nums, int k) {
        int n = nums.length;
        int mx = Arrays.stream(nums).max().getAsInt();
        List pos = new ArrayList<>();
        pos.add(-1); // 哨兵节点
        
        for (int i = 0; i < n; i++) {
            if (nums[i] == mx) {
                pos.add(i);
            }
        }
        
        long ans = 0;
        int left = 1, right = k;
        while (right < pos.size()) {
            ans += (long)(pos.get(left) - pos.get(left - 1)) * (n - pos.get(right));
            left++;
            right++;
        }
        
        return ans;
    }
}

滑动窗口解法分析

  • 时间复杂度:O(n),只需要两次遍历数组

  • 空间复杂度:O(n),需要存储最大值的位置

  • 优点:高效处理大规模数据

进一步优化:单次遍历滑动窗口

为了进一步优化空间复杂度,我想到可以在一次遍历中完成统计,而不需要存储所有最大值的位置。

优化滑动窗口解法思路

  1. 使用双指针维护滑动窗口

  2. 统计窗口内最大值的出现次数

  3. 当次数达到k时,计算满足条件的子数组数目

优化滑动窗口解法代码

java

public long countSubarrays(int[] nums, int k) {
    int mx = Arrays.stream(nums).max().getAsInt();
    long ans = 0;
    int cnt = 0, left = 0;
    
    for (int x : nums) {
        if (x == mx) {
            cnt++;
        }
        while (cnt == k) {
            if (nums[left] == mx) {
                cnt--;
            }
            left++;
        }
        ans += left;
    }
    
    return ans;
}

优化滑动窗口解法分析

  • 时间复杂度:O(n),只需一次遍历

  • 空间复杂度:O(1),不需要额外空间

  • 优点:空间效率最高

测试用例验证

为了确保解法的正确性,我设计了几组测试用例:

  1. 简单测试用例:

    • 输入:nums = [1,3,2,3,3], k = 2

    • 输出:6

    • 解释:满足条件的子数组有[1,3,2,3], [3,2,3], [2,3,3], [3,3], [3,2,3,3], [1,3,2,3,3]

  2. 边界测试用例:

    • 输入:nums = [1,1,1,1], k = 1

    • 输出:10

    • 解释:所有子数组都满足条件

  3. 复杂测试用例:

    • 输入:nums = [28,5,58,91,24,91,53,9,48,85,16,70,91,91,47,91,61,4,54,61,49], k = 4

    • 输出:25

总结

通过这个问题的解决过程,我学到了:

  1. 从暴力解法入手可以帮助理解问题本质

  2. 滑动窗口技术能有效优化子数组统计问题

  3. 在优化过程中,要权衡时间复杂度和空间复杂度

  4. 设计全面的测试用例对验证算法正确性至关重要

这个问题展示了算法优化的一般过程:从暴力解法开始,逐步分析问题特性,找到优化方向,最终得到高效解法。这种思考过程对解决其他算法问题也很有帮助。

最终推荐解法

综合考虑时间复杂度和空间复杂度,我推荐使用最后一种优化滑动窗口解法:

java

public long countSubarrays(int[] nums, int k) {
    int mx = Arrays.stream(nums).max().getAsInt();
    long ans = 0;
    int cnt = 0, left = 0;
    
    for (int x : nums) {
        if (x == mx) {
            cnt++;
        }
        while (cnt == k) {
            if (nums[left] == mx) {
                cnt--;
            }
            left++;
        }
        ans += left;
    }
    
    return ans;
}

这个解法在时间和空间上都达到了最优,能够高效处理大规模数据输入。

你可能感兴趣的:(算法,leetcode,数据结构)