区间子数组个数 图解题解
这道题到底在问什么
- 输入
- 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
最优解:一步一步想明白
- 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++ 跟着练。
面试官可能追问
为什么偏小(< left)的元素 dp 不清零,而是延续上一个值?+
因为它不超过 right,不会让任何子数组的最大值越界;它只是自己当不了最大值。把它接到之前那些合法子数组后面,最大值仍由前面的界内元素决定,所以合法子数组的数量原封不动地延续下来。
参考代码的 f(right) − f(left−1) 和动画的一遍三情况法是一回事吗?+
是等价的。f(x) 数「所有元素都 ≤ x 的子数组数」。f(right) 包含了最大值 ≤ right 的全部子数组,其中最大值 < left 的那部分恰好是 f(left−1),相减就只剩最大值在 [left,right] 的。动画把这两遍合并成一遍,按元素落点直接累加,本质相同。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 区间子数组个数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。