2026-04-03:统计稳定子数组的数目。用go语言,给你一个整数数组 nums。
如果对某个连续子数组 nums[l..r] 来说,它内部不存在“逆序对”,也就是没有下标满足 i < j 且 nums[i] > nums[j],那么这个子数组称为“稳定子数组”。(单个元素的子数组也算稳定。)
另外给你一组查询 queries,每个查询是一个区间 [li, ri]。对每个查询,你要统计所有完全落在 nums[li..ri] 范围内的稳定子数组的数量。
请把每个查询的结果按顺序放入数组 ans 中并返回:ans[i] 表示区间 [li, ri] 内稳定子数组的个数。
1 <= nums.length <= 100000。
1 <= nums[i] <= 100000。
1 <= queries.length <= 100000。
queries[i] = [li, ri]。
0 <= li <= ri <= nums.length - 1。
输入:nums = [3,1,2], queries = [[0,1],[1,2],[0,2]]。
输出:[2,3,4]。
解释:
对于 queries[0] = [0, 1],子数组为 [nums[0], nums[1]] = [3, 1]。
稳定子数组包括 [3] 和 [1]。稳定子数组的总数为 2。
对于 queries[1] = [1, 2],子数组为 [nums[1], nums[2]] = [1, 2]。
稳定子数组包括 [1]、[2] 和 [1, 2]。稳定子数组的总数为 3。
对于 queries[2] = [0, 2],子数组为 [nums[0], nums[1], nums[2]] = [3, 1, 2]。
稳定子数组包括 [3]、[1]、[2] 和 [1, 2]。稳定子数组的总数为 4。
因此,ans = [2, 3, 4]。
题目来自力扣3748。
详细步骤解析
一、核心概念理解
- 稳定子数组:连续子数组,内部完全递增/非递减(无逆序对),单个元素天然是稳定子数组。
- 问题要求:对每个查询区间
[l, r],统计完全落在该区间内的所有稳定子数组的总数。 - 数据规模:数组长度和查询数量都达到 10 万级别,必须用O(n) 预处理 + O(1) 单次查询的算法,暴力枚举会超时。
二、算法整体流程(分4大步骤)
步骤1:预处理「递增段」与「前缀和数组」
稳定子数组的本质是最长非递减连续子段的子数组,我们先把原数组拆分成若干个连续的非递减递增段(这是计算的基础)。
-
遍历数组,标记以每个位置为右端点的稳定子数组数量
- 从左到右遍历数组,维护一个计数器
cnt:- 如果当前元素 ≥ 前一个元素,说明还在同一个递增段内,
cnt加 1; - 如果当前元素 < 前一个元素,说明递增段断开,
cnt重置为 1(新段的起点);
- 如果当前元素 ≥ 前一个元素,说明还在同一个递增段内,
cnt的含义:以当前下标为右端点的稳定子数组的个数。- 示例:
nums = [3,1,2]- 下标0:3,cnt=1(只有[3])
- 下标1:1 < 3,cnt=1(只有[1])
- 下标2:2 ≥ 1,cnt=2([2]、[1,2])
- 从左到右遍历数组,维护一个计数器
-
计算前缀和数组
sum- 前缀和数组的作用:快速计算任意区间内所有稳定子数组的总数。
sum[i]表示:数组前i个元素(下标0~i-1)中,所有稳定子数组的总数。- 递推规则:
sum[i+1] = sum[i] + cnt(累加每个位置的cnt值)。 - 示例:
sum = [0, 1, 2, 4]
步骤2:预处理「下一个递增段起点」数组 nxt
为了快速判断查询区间是否跨了多个递增段,预处理一个数组 nxt:
nxt[i]表示:下标i所在递增段的下一个递增段的第一个下标;如果是最后一段,值为数组长度n。- 计算方式:从右向左遍历数组
- 最后一个元素的
nxt直接设为数组长度n; - 如果当前元素 ≤ 下一个元素,说明和下一个元素同段,
nxt[i] = nxt[i+1]; - 如果当前元素 > 下一个元素,说明段在这里断开,
nxt[i] = i+1(下一个位置就是新段起点)。
- 最后一个元素的
- 示例:
nums = [3,1,2]- nxt[2] = 3(数组长度)
- 1 ≤ 2 → nxt[1] = nxt[2] = 3
- 3 > 1 → nxt[0] = 1
步骤3:处理每个查询,计算结果
对每个查询 [l, r],分两种情况计算,核心逻辑:查询区间内的稳定子数组,只能是各个递增段内部的子数组(跨段的子数组一定有逆序对,不是稳定子数组)。
情况1:查询区间在同一个递增段内
判断条件:nxt[l] > r(从l出发的下一个段起点,超过了查询右边界r)
- 计算方式:长度为
m = r-l+1的连续递增段,稳定子数组总数 =m*(m+1)/2(等差数列求和公式)。
情况2:查询区间跨了多个递增段
判断条件:nxt[l] ≤ r(查询区间被分成两段:[l, nxt[l]-1] 和 [nxt[l], r])
- 第一段:
[l, nxt[l]-1]是完整的递增段,用公式m*(m+1)/2计算(m为段长度); - 第二段:
[nxt[l], r]直接用前缀和数组快速计算总数(sum[r+1] - sum[nxt[l]]); - 总结果 = 第一段数量 + 第二段数量。
步骤4:示例验证(nums=[3,1,2],queries=[[0,1],[1,2],[0,2]])
-
查询[0,1]
- nxt[0]=1 ≤1,跨段
- 第一段[0,0]:1*2/2=1;第二段[1,1]:sum[2]-sum[1]=1
- 结果:1+1=2
-
查询[1,2]
- nxt[1]=3>2,同段
- 长度2:2*3/2=3,结果=3
-
查询[0,2]
- nxt[0]=1 ≤2,跨段
- 第一段[0,0]:1;第二段[1,2]:sum[3]-sum[1]=3
- 结果:1+3=4
最终结果:[2,3,4],与题目一致。
三、时间复杂度与额外空间复杂度
1. 总时间复杂度
- 预处理前缀和数组:O(n)(遍历一次数组);
- 预处理nxt数组:O(n)(反向遍历一次数组);
- 处理所有查询:O(q)(q为查询数量,每个查询O(1)计算);
- 总时间复杂度:O(n + q) 完美适配 10 万级别的数据规模,效率极高。
2. 总额外空间复杂度
- 前缀和数组
sum:长度 n+1,占用 O(n) 空间; - nxt数组:长度 n,占用 O(n) 空间;
- 答案数组:长度 q,占用 O(q) 空间;
- 总额外空间复杂度:O(n + q) 除了输入输出外,仅使用了线性空间,无额外冗余空间。
总结
- 算法核心:拆分递增段 + 前缀和预处理 + 快速查询,解决大数据量下的暴力超时问题;
- 计算逻辑:严格区分查询区间是否跨递增段,分别用公式/前缀和计算;
- 效率:时间 O(n+q)、空间 O(n+q),完全满足题目 10 万级数据的要求。
Go完整代码如下:
package main
import (
"fmt"
)
func countStableSubarrays(nums []int, queries [][]int) []int64 {
n := len(nums)
// 计算递增子数组个数的前缀和
sum := make([]int64, n+1)
cnt := 0
for i, x := range nums {
if i > 0 && x < nums[i-1] {
cnt = 0
}
cnt++
// 现在 cnt 表示以 i 为右端点的递增子数组个数
sum[i+1] = sum[i] + int64(cnt)
}
// nxt[i] 表示 i 右边下一个递增段的左端点,若不存在则为 n
nxt := make([]int, n)
nxt[n-1] = n
for i := n - 2; i >= 0; i-- {
if nums[i] <= nums[i+1] {
nxt[i] = nxt[i+1]
} else {
nxt[i] = i + 1
}
}
ans := make([]int64, len(queries))
for k, q := range queries {
l, r := q[0], q[1]
l2 := nxt[l]
if l2 > r { // l 和 r 在同一个区间
m := int64(r - l + 1)
ans[k] = m * (m + 1) / 2
} else { // l 和 r 在不同区间
// 分成 [l, l2) + [l2, r]
// 由于 [l2, r] 中的每个右端点所在递增段的左端点都在 [l2, r] 内,所以可以用前缀和计算
m := int64(l2 - l)
ans[k] = m*(m+1)/2 + sum[r+1] - sum[l2]
}
}
return ans
}
func main() {
nums := []int{3, 1, 2}
queries := [][]int{{0, 1}, {1, 2}, {0, 2}}
result := countStableSubarrays(nums, queries)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
def count_stable_subarrays(nums, queries):
n = len(nums)
# 计算递增子数组个数的前缀和
sum_prefix = [0] * (n + 1)
cnt = 0
for i, x in enumerate(nums):
if i > 0 and x < nums[i - 1]:
cnt = 0
cnt += 1
# 现在 cnt 表示以 i 为右端点的递增子数组个数
sum_prefix[i + 1] = sum_prefix[i] + cnt
# nxt[i] 表示 i 右边下一个递增段的左端点,若不存在则为 n
nxt = [0] * n
nxt[n - 1] = n
for i in range(n - 2, -1, -1):
if nums[i] <= nums[i + 1]:
nxt[i] = nxt[i + 1]
else:
nxt[i] = i + 1
ans = []
for l, r in queries:
l2 = nxt[l]
if l2 > r: # l 和 r 在同一个区间
m = r - l + 1
ans.append(m * (m + 1) // 2)
else: # l 和 r 在不同区间
# 分成 [l, l2) + [l2, r]
# 由于 [l2, r] 中的每个右端点所在递增段的左端点都在 [l2, r] 内,所以可以用前缀和计算
m = l2 - l
ans.append(m * (m + 1) // 2 + sum_prefix[r + 1] - sum_prefix[l2])
return ans
if __name__ == "__main__":
nums = [3, 1, 2]
queries = [[0, 1], [1, 2], [0, 2]]
result = count_stable_subarrays(nums, queries)
print(result)
C++完整代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<long long> countStableSubarrays(vector<int>& nums, vector<vector<int>>& queries) {
int n = nums.size();
// 计算递增子数组个数的前缀和
vector<long long> sum(n + 1, 0);
int cnt = 0;
for (int i = 0; i < n; i++) {
if (i > 0 && nums[i] < nums[i - 1]) {
cnt = 0;
}
cnt++;
// 现在 cnt 表示以 i 为右端点的递增子数组个数
sum[i + 1] = sum[i] + cnt;
}
// nxt[i] 表示 i 右边下一个递增段的左端点,若不存在则为 n
vector<int> nxt(n, 0);
nxt[n - 1] = n;
for (int i = n - 2; i >= 0; i--) {
if (nums[i] <= nums[i + 1]) {
nxt[i] = nxt[i + 1];
} else {
nxt[i] = i + 1;
}
}
vector<long long> ans(queries.size(), 0);
for (int k = 0; k < queries.size(); k++) {
int l = queries[k][0], r = queries[k][1];
int l2 = nxt[l];
if (l2 > r) { // l 和 r 在同一个区间
long long m = r - l + 1;
ans[k] = m * (m + 1) / 2;
} else { // l 和 r 在不同区间
// 分成 [l, l2) + [l2, r]
// 由于 [l2, r] 中的每个右端点所在递增段的左端点都在 [l2, r] 内,所以可以用前缀和计算
long long m = l2 - l;
ans[k] = m * (m + 1) / 2 + sum[r + 1] - sum[l2];
}
}
return ans;
}
int main() {
vector<int> nums = {3, 1, 2};
vector<vector<int>> queries = {{0, 1}, {1, 2}, {0, 2}};
vector<long long> result = countStableSubarrays(nums, queries);
cout << "[";
for (int i = 0; i < result.size(); i++) {
if (i > 0) cout << " ";
cout << result[i];
}
cout << "]" << endl;
return 0;
}