区间子数组个数 图解题解
这道题到底在问什么
- 输入
- nums=[2,1,4,3], left=2, right=3
- 输出
- 3 ([2]、[2,1]、[3])
- 输入
- nums=[2,9,2,5,6], left=2, right=8
- 输出
- 7
最优解:为什么这么做
一句话答案:LeetCode 795 区间子数组个数用计数差分:定义 f(x)=最大值 ≤x 的子数组个数,答案=f(right)−f(left−1),把『卡两头』拆成两次『只卡一头』。时间 O(n)、空间 O(1)。
所谓「最大值落在界内」的子数组,怎么数出来
给数组 nums 和两个界 left、right,要数出有多少个连续非空子数组,它的最大元素既 ≥left 又 ≤right,即最大值落在闭区间 [left, right](含两端)里。题面 nums=[2,1,4,3]、left=2、right=3 时答案是 3:[2] 最大值 2、[2,1] 最大值 2、[3] 最大值 3 都卡在界内;含 4 的子数组最大值超界,单独一个 1 的又不够,都不算。
把每个子数组都框出来求最大值,慢在哪
顺着定义枚举每个子数组:固定左端,右端一路往后扩,扩时记住当前最大值,看它落不落在 [left, right]。子数组共 O(n²) 个,逐个查是 O(n²) 时间。n 上万时平方级就吃力,得想个一遍扫完的办法。
数「恰在界内」难,数「不超某条线」却容易
直接数「最大值恰好在 [left, right]」两头都卡着,不好下手。换个角度:定义 f(x)= 最大值 ≤x 的子数组个数,只卡上界,而「所有元素都 ≤x」的子数组好数得多。最大值 ≤right 的子数组共 f(right) 个,其中最大值还 <left(即 ≤left−1)的那批共 f(left−1) 个,是前者的子集。拿 f(right) 减掉 f(left−1),剩的就是最大值 ≤right 又 ≥left 的,落在 [left, right] 的那些。答案 = f(right) − f(left−1)。
只卡一个上界,f(x) 一遍扫描就数得出
f(x) 能一遍线性扫出来。扫数组时维护一个 t,表示「以当前元素结尾、且这一段没有元素超过 x 的子数组个数」。碰到 v ≤x,它接在合法段后、也能自己单独成段,t 加一——以它结尾,每往右延一个合法元素就多一个左端点,加一而非翻倍;碰到 v >x,它像一堵墙,以它结尾的合法段一个没有,t 归零。每步把 t 累进 cnt,扫完就是 f(x)。一次遍历、几个计数器。
[2,1,4,3] 上,f(3) 与 f(1) 分别怎么算
right=3,先算 f(3)。v=2 ≤3,t=1,cnt=1;v=1 ≤3,t=2,cnt=3;v=4 >3,t 归零,cnt 仍是 3;v=3 ≤3,t=1,cnt=4。得 f(3)=4。再算 f(left−1)=f(1)。v=2 >1,t=0,cnt=0;v=1 ≤1,t=1,cnt=1;v=4 >1,t=0,cnt=1;v=3 >1,t=0,cnt=1。得 f(1)=1。答案 f(3) − f(1) = 4 − 1 = 3。
差分下界写成 f(left),等于 left 的那些就被误删
两次 f(x) 各扫一遍,合起来仍是 O(n) 时间、O(1) 空间。两处容易写坏:一是差分下界得是 f(left−1) 不是 f(left),写成 f(left) 会把最大值恰好等于 left 的合法子数组也一并扣掉,答案偏小;二是墙必须清零,v >x 时忘了把 t 归零、让它继续加,跨过墙、含超界元素的子数组就被错数进来。边界上,全部元素超界或全部 <left 时,f(right) 与 f(left−1) 相减都是 0,只有出现界内元素才数得出来。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这套「在界内算距离、偏小延续、超界清零」,下面每一帧都在套它。
- 4start 记最近一个超过 right 的下标,开局当作 −1(还没出现过墙)。dp 记「以当前元素结尾的合法子数组数」,ans 是累计答案。从第 0 个开始扫。
- 5第 0 个是 2,2 ≤ 2 ≤ 3,正好落在界内。它自己就能当一段子数组的最大值。
- 6从 start 右边(下标 0)一直到 0,每个起点配上 0 都是一个合法子数组,共 1 个。绿色高亮的就是这 1 个子数组的起点范围。
- 7把 dp = 1 累进答案,ans 现在是 1。
- 8第 1 个是 1,1 < left=2,比下界还小。它当不了最大值,但也不会把最大值顶出界。
- 91 偏小,把它接到之前那些合法子数组后面,最大值还在原来那个界内元素手里,数量不变,dp 仍是 1。
- 10把 dp = 1 累进答案,ans 现在是 2。
- 11第 2 个是 4,4 > right=3,太大了。任何包含它的子数组,最大值都会超过 right。
- 124 超界,像一堵墙把窗口斩断。以它结尾的合法子数组一个都没有,dp 清零;start 记到 2,之后的窗口只能从墙右边算起。
- 13这一步给答案加 0,ans 仍是 2。继续往后看。
- 14第 3 个是 3,2 ≤ 3 ≤ 3,正好落在界内。它自己就能当一段子数组的最大值。
- 15从 start 右边(下标 3)一直到 3,每个起点配上 3 都是一个合法子数组,共 1 个。绿色高亮的就是这 1 个子数组的起点范围。
- 16把 dp = 1 累进答案,ans 现在是 3。
- 17第 4 个是 2,2 ≤ 2 ≤ 3,正好落在界内。它自己就能当一段子数组的最大值。
- 18从 start 右边(下标 3)一直到 4,每个起点配上 4 都是一个合法子数组,共 2 个。绿色高亮的就是这 2 个子数组的起点范围。
- 19把 dp = 2 累进答案,ans 现在是 5。
- 20第 5 个是 5,5 > right=3,太大了。任何包含它的子数组,最大值都会超过 right。
- 215 超界,像一堵墙把窗口斩断。以它结尾的合法子数组一个都没有,dp 清零;start 记到 5,之后的窗口只能从墙右边算起。
- 22这一步给答案加 0,ans 仍是 5。继续往后看。
- 23第 6 个是 3,2 ≤ 3 ≤ 3,正好落在界内。它自己就能当一段子数组的最大值。
- 24从 start 右边(下标 6)一直到 6,每个起点配上 6 都是一个合法子数组,共 1 个。绿色高亮的就是这 1 个子数组的起点范围。
- 25把 dp = 1 累进答案,ans 现在是 6。
- 26全程扫完,每一步以当前元素结尾的合法子数组数依次是 1、1、0、1、2、0、1,相加得 6。这就是最终答案 6,整趟只扫了一遍。
⚠️ 容易写错的地方
✗ 错:把偏小的数当成「断开」清零
✓ 对:v < left 时 dp 要延续,不清零
它不超 right,含界内元素的子数组接上它依然合法
✗ 错:超界时忘了重置 start
✓ 对:v > right 时 dp 归 0 且 start 移到 i
不移 start,后面 i − start 会把墙左边的非法起点也数进来
✗ 错:只统计「恰好等于 left 或 right」的子数组
✓ 对:是最大值落在闭区间 [left,right] 内
题目要的是 max 在区间内,不是元素等于边界
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
def f(x):
cnt = t = 0
for v in nums:
t = 0 if v > x else t + 1
cnt += t
return cnt
return f(right) - f(left - 1)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int numSubarrayBoundedMax(vector<int>& nums, int left, int right) {
auto f = [&](int x) {
int cnt = 0, t = 0;
for (int& v : nums) {
t = v > x ? 0 : t + 1;
cnt += t;
}
return cnt;
};
return f(right) - f(left - 1);
}
};Java
import java.util.*;
class Solution {
public int numSubarrayBoundedMax(int[] nums, int left, int right) {
return f(nums, right) - f(nums, left - 1);
}
private int f(int[] nums, int x) {
int cnt = 0, t = 0;
for (int v : nums) {
t = v > x ? 0 : t + 1;
cnt += t;
}
return cnt;
}
}复杂度
时间
O(n)
参考代码用 f(right)−f(left−1),对数组做两次线性扫描,总时间仍是 O(n)
空间
O(1)
只用几个变量计数,常数空间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 区间子数组个数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么减的是 f(left−1),不是 f(left)?+
f(x) 数的是最大值 ≤x 的子数组。要留下「最大值 ≥left」的,就得把「最大值 <left」的排除掉,而对整数来说「最大值 <left」和「最大值 ≤left−1」是一回事,所以扣的是 f(left−1)。若写成 f(left),扣掉的是「最大值 ≤left」,连最大值正好等于 left 的合法子数组也一并删了,答案会少一截。差分的这个 −1 就是为了让下界「够到 left 本身」。
参考代码的 f(right)−f(left−1) 和一遍直接分三种情况数,是同一回事吗?+
是等价的。还有一种更直观的一遍法:扫数组时,元素落在 [left, right] 就新增「从上一堵墙右边到当前位置」这么多合法子数组,元素偏小(<left)就沿用上一格的个数、不清零,元素超界(>right)就把计数清零并把墙记到当前位置。它一遍就把答案直接算出来;f(right)−f(left−1) 则用两遍、各只卡一头再相减。两种走法数的是同一批子数组,结果必然一致,选顺手的写即可。
什么样的题能套这个「两次只卡一头再相减」的差分?+
只要答案是「某个量恰好落在闭区间 [L, R] 内的对象个数」,且「≤某上界的对象个数」比「恰好落在区间内」好数,就能拆成 f(R)−f(L−1)。本题的量是子数组的最大值,f(x)=最大值 ≤x 的子数组数好数,因为它等价于「全体元素都 ≤x」。像数「恰好含 k 个奇数的子数组」「元素和 ≤某值的子数组」这类题,也常把一个「恰好」转成两个「不超过」之差来算。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 区间子数组个数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。