<think>题目要求:找出数组 records 中所有长度在 [minLen, maxLen] 范围内,且平均值大于等于 threshold 的子数组的个数。
注意:区间长度指的是子数组的元素个数(即连续天数),而平均值是子数组内所有元素的和除以长度(元素个数)。
由于数组长度最大为10000,而允许的区间长度范围 maxLen 最大为1000(且maxLen<=records.length),我们可以考虑使用滑动窗口结合前缀和的方法。
思路:
1. 计算前缀和数组 prefix,其中 prefix[0]=0, prefix[i]=records[0]+...+records[i-1]
这样,子数组 records[i:j](左闭右开,即从下标i到j-1)的和为 prefix[j]-prefix[i],长度为 j-i。
2. 条件:要求子数组的长度在 [minLen, maxLen] 范围内,且平均值 >= threshold,即:
(prefix[j]-prefix[i]) / (j-i) >= threshold
等价于:prefix[j]-prefix[i] >= threshold * (j-i)
3. 我们可以固定子数组的右端点 j,然后寻找满足条件的左端点 i。其中,子数组长度 L = j-i 必须满足 minLen<=L<=maxLen,即:
j - maxLen <= i <= j - minLen
4. 因此,对于每个右端点 j,我们需要检查的范围是 i 从 max(0, j-maxLen) 到 j-minLen(注意:左端点i要满足i>=0且i<=j-minLen)。
5. 但是直接遍历每个 j 和对应的 i 范围,时间复杂度是 O(n*(maxLen-minLen)),最大为 10000*1000=10e6,在Python中可能勉强通过,但可能超时。
6. 我们需要优化:将条件转换一下:
prefix[j] - prefix[i] >= threshold * (j-i)
=> prefix[j] - threshold * j >= prefix[i] - threshold * i
7. 令 B[i] = prefix[i] - threshold * i
则条件变为:B[j] >= B[i]
8. 因此,对于每个右端点 j,我们需要在 i 属于 [j-maxLen, j-minLen] 的范围内(注意左闭右闭)统计满足 B[i] <= B[j] 的 i 的个数。
9. 注意:i 的取值范围是 [j-maxLen, j-minLen](因为长度L=j-i,要求minLen<=L<=maxLen,所以i在[j-maxLen, j-minLen]之间,注意i是下标,且i>=0)。
10. 我们可以使用树状数组或线段树来维护 B[i] 的值,并在窗口滑动时动态查询。但是考虑到 j 每次移动一位,且 i 的取值范围是一个固定长度的区间(长度为 maxLen-minLen+1,最大1000),我们可以直接在这个区间内维护一个有序数组(或者使用双端队列,但这里需要查询区间内小于等于某个值的个数,所以考虑维护一个有序列表,然后二分查找)。
11. 具体步骤:
- 初始化一个空列表(用于存放当前窗口内的B[i]值,并保持有序)
- 遍历 j 从 minLen 到 n(因为子数组长度至少minLen,所以j至少从minLen开始)
- 每次 j 增加时,需要加入的 i 是 i0 = j - minLen(因为此时这个左端点第一次进入窗口?注意我们的i范围是[j-maxLen, j-minLen])
但实际上,随着 j 增大,窗口的左边界 j-maxLen 和右边界 j-minLen 都在增大。我们维护一个窗口,窗口内的索引 i 满足 i 在 [L, R] 其中 L=j-maxLen, R=j-minLen。
- 当 j 增大1时,窗口左边界 L 增大1(移除 i = j-1-maxLen),右边界 R 增大1(加入 i = j-minLen)。
注意:j 从 minLen 开始,初始时,i 的取值范围为 [0, minLen]?不对,应该是从0到minLen-1?实际我们要的是左端点i,且子数组长度为L,则i=j-L,所以当j=minLen时,i的取值范围为 [0, j-minLen=0] 到 [j-maxLen, ...] 但此时j-minLen=0,j-maxLen可能为负数(如果maxLen>minLen,则j-maxLen=minLen-maxLen<0),所以左端点要取max(0, j-maxLen)。
12. 实际上,我们维护的窗口是:左指针 left = max(0, j - maxLen),右指针 right = j - minLen(注意:j 从0开始计数?但我们的右端点j表示子数组的最后一个元素?这里我们使用前缀和,子数组 [i, j-1] 的和为 prefix[j]-prefix[i],长度为 j-i。所以子数组右边界为 j-1,左边界为 i。那么子数组长度范围要求 minLen<= (j-1-i+1)<=maxLen,即 minLen<= j-i <= maxLen,所以 i 的范围为 j-maxLen <= i <= j-minLen。
因此,当 j 从 minLen 到 n(注意j最大为n,因为子数组的右边界最多到n-1,所以j从minLen到n):
对于当前j,需要加入的左端点 i = j-minLen(注意:此时这个i对应的是子数组 [i, j-1] 的左边界,其中右边界为j-1,所以子数组长度为 j-i,满足最小长度minLen,因为j-i=j-(j-minLen)=minLen)
同时,需要移除的左端点 i = j-1-maxLen(注意:上一个j-1对应的窗口左边界最小值是 (j-1)-maxLen,但当前j对应的左边界最小值是j-maxLen,所以移除 i = j-maxLen-1?不对,应该是移除已经不在当前窗口的左端点,即 i = (j-1) - maxLen 可能小于当前窗口的最小左边界 j-maxLen,所以当j增大时,窗口左边界从 (j-1)-maxLen 增大到 j-maxLen,因此需要移除 i = (j-1)-maxLen 如果它存在的话?实际上每次移除的是 i = j - maxLen - 1?不对,因为我们窗口左边界是 j-maxLen,所以索引小于 j-maxLen 的左端点都应该被移除。因此,当 j 增大1,窗口左边界变为 j-maxLen,那么原来在窗口中的 i = j-1-maxLen 应该被移除(因为它等于 j-maxLen-1,小于新的左边界 j-maxLen)。
因此,在 j 的循环中:
- 当 j>=minLen 时,将 i0 = j-minLen 加入窗口(因为此时这个i0是当前窗口的右边界,即最大的左端点下标)。
- 同时,如果 j>maxLen(注意:这样 j-maxLen-1 才非负),则将 i_remove = j-maxLen-1 从窗口中移除(因为新的窗口左边界是 j-maxLen,所以小于 j-maxLen 的索引要移除,但是我们的窗口维护的是 [j-maxLen, j-minLen] 区间,所以只需要移除 j-maxLen-1 这个点,因为它是上一个窗口的最小左边界?实际上,每次j增加,窗口左边界右移一个单位,所以只需要移除一个点:i = j - maxLen - 1)。
- 然而,注意初始时 j 从 minLen 开始,此时 j-minLen=0,而 j-maxLen 可能为负数(如果minLen<maxLen),所以如果j-maxLen-1为负数,则不需要移除。
13. 但是上面的窗口维护并不包含所有的左端点?因为对于每个 j,窗口内的索引 i 应该是从 j-maxLen 到 j-minLen。我们需要将当前 j 对应的左端点 i 全部加入?实际上,随着 j 增加,窗口在向右滑动,每次加入一个新的左端点(即 j-minLen),同时移除一个旧的左端点(即 j-maxLen-1)。
14. 因此,我们维护一个有序列表 sorted_list,初始为空。
遍历 j 从 minLen 到 n(包括n,因为子数组右边界可以到n-1,则j=n时,子数组为 [i, n-1]):
- 计算当前左端点 i_new = j - minLen。如果 i_new >=0 且 i_new <= n(注意i_new最大为n-minLen<=n),那么将 B[i_new] 插入有序列表(并保持有序)。
- 计算需要移除的左端点 i_old = j - maxLen - 1。如果 i_old >=0 且 i_old<=n,那么从有序列表中移除 B[i_old](注意可能有重复?但每个i对应一个B[i]值,且i唯一,所以直接按值移除,但注意可能有相同的B[i]?所以需要记录索引?实际上我们按值移除可能移除多个相同的?因此,我们需要使用二分查找找到该值的位置,但可能有多个相同的值,而我们只想移除一个(对应索引i_old)。所以更好的办法是,我们维护一个列表,同时记录索引?或者我们直接按值删除一次?但是如果有重复,可能会误删。所以我们可以维护一个列表,只存储值,并在删除时删除最早加入的那个?但这里我们每次删除都是确定的索引对应的值,所以我们可以在加入时记录每个索引对应的值,然后删除时直接根据值删除,但注意相同的值可能来自不同的索引,所以我们不能简单删除值。因此,我们使用二分查找位置,然后删除那个位置的值(因为我们知道索引,但有序列表里只存值,所以当我们加入一个值时,我们不知道它来自哪个索引?但是实际上,同一个值在不同索引出现是允许的,所以删除时我们只需要删除一个?但是删除哪一个呢?我们只删除一个?实际上,我们每次加入和删除都是成对的,每个索引加入一次,删除一次。所以我们可以用平衡二叉树?但Python的bisect可以处理重复值,我们直接按照值删除,但删除时删除第一个出现的?不行,因为删除的是特定索引对应的值,而不同索引可能有相同的值。所以我们需要在有序列表中存储(B[i], i)?然后根据i来删除?但是这样插入和删除都是O(窗口大小),因为窗口大小最大为maxLen-minLen+1(约1000),所以我们可以线性扫描删除?或者我们只存值,然后删除时删除指定值(不管位置)?但这样会删除所有该值,不合理。
另一种方法是,我们不维护整个窗口的有序列表,而是对于每个 j,在窗口 [j-maxLen, j-minLen] 内遍历?窗口大小最多1000,然后我们在这个窗口内遍历计数,这样总的时间复杂度是O(n*(maxLen-minLen)),最大10000*1000=10e6,在Python中可能勉强通过(10e6次操作在Pyton中可能1秒左右?但题目数组长度10000,maxLen-minLen最大1000,所以最坏10^7,可能会超时,但实际可能不会达到最坏,因为minLen和maxLen可能接近?或者我们可以优化)。
15. 由于窗口大小固定为 maxLen-minLen+1(最大1000),我们可以不用有序列表,而是每次遍历窗口内的所有索引进行计数。这样总操作次数为 n * (maxLen-minLen+1),最多10000*1000=10e6,在C++中没问题,在Python中应该也能接受(Pyton一般在1e6~1e7次操作)。
16. 因此,我们采用直接遍历窗口的方法:
for j in range(minLen, n+1): # j从minLen到n(包括n)
left_bound = max(0, j - maxLen) # 当前左端点最小值
right_bound = j - minLen # 当前左端点最大值
# 遍历 i 从 left_bound 到 right_bound
count_j = 0
for i in range(left_bound, right_bound+1):
if B[i] <= B[j]: # 注意:这里B[j]=prefix[j]-threshold*j
count_j += 1
count += count_j
但是注意:这里的 j 是前缀和数组的下标,对应子数组的右边界是 j(子数组为 [i, j-1]),所以 j 的范围是 minLen 到 n(因为子数组长度至少minLen,则需要j>=minLen)。
17. 但是这样会超时吗?最坏情况:minLen=1, maxLen=1000,那么窗口大小是1000,然后j从1到10000,总操作10000*1000=10e6,在Python中可能1秒内完成?但是题目数组长度最大10000,10e6次操作在Pyton中可能达到1秒(CPython可能更快?),但还要考虑其他操作。所以我们可以尝试。
18. 但是,我们还可以优化内层循环:我们可以在窗口内维护一个有序数组,然后二分查找。这样内层循环的复杂度是O(log(窗口大小)),总复杂度O(n*log(窗口大小)),最多10000*log2(1000)≈10000*10=10e4,这样更快。
19. 因此,我们选择使用有序列表(用bisect模块)来维护窗口内的B[i]值(只存值,不存索引)。注意:我们不需要考虑索引,因为我们只关心值,并且我们每次加入和删除都是按照左端点i的位置(即按照j的变化加入和删除)。但是,同一个值可能出现多次,所以我们要用列表,并且用bisect来插入和删除(删除时需要删除指定值,但注意可能多个相同值,我们只删除一个?但因为我们知道要删除的值是多少,而且删除的是最早加入的那个(实际上,我们加入和删除都是按照左端点i的顺序,而每个i对应一个B[i]值,所以每个值加入一次删除一次)。但是,在有序列表中删除一个元素需要O(窗口大小)的时间?我们可以用bisect找到位置,然后用pop或remove?但是remove也是O(n)的。所以整体复杂度还是O(n*窗口大小),和直接遍历一样。
20. 我们可以使用平衡二叉树结构吗?Python中有sorted list,但是标准库没有。我们可以用bisect来插入和删除,但是删除一个元素需要O(窗口大小)的时间。所以如果窗口大小1000,那么删除和插入都是O(1000),总复杂度O(n*1000)=10e6。
21. 实际上,我们不需要删除,因为窗口大小固定,我们可以用双指针,但是我们需要查询窗口内小于等于B[j]的元素个数,所以需要动态维护一个数据结构,支持插入、删除和查询小于等于某个值的元素个数。我们可以用树状数组,但是值域可能很大(B[i]的值范围?prefix[i]最大为10000*10000=1e8,threshold最大10000,i最大10000,所以B[i]的范围约为-1e8到1e8,值域很大,需要离散化)。
22. 考虑到窗口大小只有1000,我们选择直接遍历窗口(1000次)来统计,总操作10000*1000=10e6,在Python中可能运行时间在1秒左右(实际题目中数组长度10000,1000的窗口,10e6次操作在Pyton中可能1秒内,可以接受)。
23. 步骤总结:
n = len(records)
minL, maxL = length_range # minL=minLen, maxL=maxLen
# 构建前缀和数组,长度为n+1
prefix = [0] * (n+1)
for i in range(1, n+1):
prefix[i] = prefix[i-1] + records[i-1]
# 构建数组 B,长度为n+1,B[i] = prefix[i] - threshold * i
B = [0] * (n+1)
for i in range(1, n+1):
B[i] = prefix[i] - threshold * i
# 注意:B[0]=prefix[0]-threshold*0=0
count = 0
# 遍历右端点 j(前缀和数组下标),j从minL到n(因为子数组长度至少为minL,所以j>=minL)
# 注意:子数组对应原数组的区间 [i, j-1](左闭右闭),其中左端点i从0到n-1,右端点j-1从minL-1到n-1。
# 对于每个j,左端点i的取值范围:满足 minL <= j-i <= maxL,且0<=i<=n
# 所以 i 在 [max(0, j - maxL), j - minL] 闭区间内。
# 我们预先用一个列表来维护当前窗口内的B[i]值(窗口为当前j对应的左端点i的范围),但我们可以不预先维护,而是每次遍历窗口。
# 直接遍历窗口:
for j in range(minL, n+1):
low_i = max(0, j - maxL)
high_i = j - minL # 注意这里是闭区间,所以i可以从low_i到high_i
# 统计在区间[low_i, high_i]内,有多少个i满足 B[i] <= B[j]
for i in range(low_i, high_i+1):
if B[i] <= B[j]:
count += 1
return count
24. 但是,这样写会超时吗?我们计算一下:j从minL到n(n最大10000),内层循环次数为窗口大小(maxL-minL+1,最大1000),那么总操作次数为 (n-minL+1)*(maxL-minL+1) <= 10000*1000=10e6,在Python中可能1秒内完成(C++肯定没问题,Python需要测试,但题目要求返回整数,且数据范围如此,我们假设可以通过)。
25. 但是,我们需要注意:当minL和maxL很接近时,窗口很小,效率就会很高;当maxL-minL很大(接近1000)且n很大(10000)时,10e6次操作在Pyton中可能耗时1秒左右。我们尝试优化内层循环?
26. 我们可以预先计算窗口内的B[i]值,然后排序?但是窗口随着j在滑动,所以不好预先排序。
27. 另一种思路:将条件转换,我们要求的是:对于每个j,在区间 [low_i, high_i] 内,满足 B[i]<=B[j] 的i的个数。我们可以用树状数组,但是树状数组需要离散化整个B数组(因为值域很大)。离散化后,我们可以在滑动窗口的同时更新树状数组。具体:
- 离散化:将B数组的所有值进行离散化(因为窗口大小最多1000,但是我们整个B数组有n+1个点,所以离散化整个B数组需要O(n log n),然后对于每个j,我们需要加入B[j-minL](当j>=minL时)和移除B[j-maxL-1](当j>maxL时),然后查询窗口内(窗口内的所有B[i]值)小于等于B[j]的个数。
- 但是窗口内的元素个数最多1000,所以我们也可以不用树状数组,而是维护一个树状数组只针对窗口内的元素?但离散化整个B数组太大,而窗口内只有1000个元素,所以我们可以对窗口内的元素离散化?然后树状数组大小1000,然后每次移动窗口时更新树状数组(加入一个元素和删除一个元素,然后查询)?这样每次操作O(log1000)≈10,总操作O(n*10)=100000,很快。
步骤:
维护一个窗口(队列)存储当前的B[i]值(i在[j-maxL, j-minL])。
同时,为了快速查询,我们维护一个树状数组(或线段树)用于求区间和(按值域)。但是我们需要离散化窗口内的值?然而窗口内的值会变化,所以每次加入新元素和删除旧元素都需要更新离散化映射?这样不可行。
28. 因此,我们还是选择直接遍历窗口。考虑到最大10e6次操作,在Python中我们可以接受。
29. 但是,我们还需要考虑边界:当minL>n时,不可能有子数组,但题目保证minLen<=maxLen<=records.length,所以minL<=n。
30. 还有一个边界:当阈值threshold很大时,可能满足条件的区间很少,但我们的内层循环次数只与窗口大小有关,与条件无关,所以无法跳过。
31. 因此,我们按照直接遍历窗口的方法实现。
32. 但是,我们忽略了一个问题:条件转换后的B[j]>=B[i]等价于平均值条件吗?
原条件:prefix[j]-prefix[i] >= threshold*(j-i)
转换:prefix[j]-threshold*j >= prefix[i]-threshold*i -> B[j]>=B[i]
所以正确。
33. 但是注意:我们的子数组是从i到j-1(因为前缀和的定义:prefix[j]-prefix[i] = records[i]+...+records[j-1]),所以子数组长度为 j-i(即元素个数)。所以条件正确。
34. 代码实现:
注意:我们的窗口是左端点i的范围 [j-maxL, j-minL](闭区间),且j从minL到n(包括n)。
特殊情况:当j-minL<0?不会,因为j>=minL,所以j-minL>=0。
当j-maxL可能为负数,所以用max(0, j-maxL)。
35. 测试:用一个简单例子。
records = [1,2,3,4,5], length_range=[2,3], threshold=3
要求:子数组长度在[2,3]内,平均值>=3。
子数组:
[1,2]:avg=1.5 -> 不满足
[2,3]:avg=2.5 -> 不满足
[3,4]:avg=3.5 -> 满足
[4,5]:avg=4.5 -> 满足
[1,2,3]:avg=2 -> 不满足
[2,3,4]:avg=3 -> 满足
[3,4,5]:avg=4 -> 满足
所以满足的区间有:[3,4], [4,5], [2,3,4], [3,4,5] -> 4个。
用我们的方法:
n=5
prefix = [0,1,3,6,10,15]
B = [0, 1-3*1=-2, 3-3*2=-3, 6-3*3=-3, 10-3*4=-2, 15-3*5=0]
j从minL=2到5(包括5):
j=2: 左端点i的范围:max(0,2-3)=0, 到 2-2=0 -> i=0
条件:B[0]=0, B[2]=-3 -> 0>=-3,满足,所以count+1 -> 1。这个对应子数组[0,1]即原数组的[0:2) -> [0,1]也就是[1,2],但实际上这个子数组平均值1.5<3,不应该被计数!错误。
问题在哪里?
我们的子数组:i和j的定义?子数组为 records[i:j] 即从i到j-1,长度为j-i。
当j=2时,i=0:子数组为 records[0:2] -> [1,2](下标0和1),平均值1.5。
条件:B[0]<=B[2] -> 0<=-3 不成立!所以不计数。我们上面的代码写的是 if B[i]<=B[j] 才计数,而这里0<=-3不成立,所以不计。所以上面例子中j=2时计数0。
重新计算:
j=2: i=0 -> B[0]=0, B[2]=-3 -> 0<=-3? 不成立 -> 不计数。
j=3: i的范围:max(0,3-3)=0, 到 j-minL=3-2=1 -> i=0,1
i=0: B[0]=0<=B[3]=-3? 0<=-3 ->不成立。
i=1: B[1]=-2<=B[3]=-3? -> -2<=-3 ->不成立。
j=4: i的范围:max(0,4-3)=1, 到 j-minL=4-2=2 -> i=1,2
i=1: B[1]=-2<=B[4]=-2? 成立 ->计数1
i=2: B[2]=-3<=B[4]=-2? 成立 ->计数2 (当前总计数3)
j=5: i的范围:max(0,5-3)=2, 到 5-2=3 -> i=2,3
i=2: B[2]=-3<=B[5]=0? 成立 ->计数3
i=3: B[3]=-3<=B[5]=0? 成立 ->计数4
所以总计数4,正确。
但是,我们看j=3时,应该对应哪些子数组?
j=3:子数组右边界是j-1=2,即原数组下标为0,1,2(对应元素1,2,3)?但实际上我们考虑的子数组是 [i, j-1],当j=3时,子数组为 [0,2](长度为3)和 [1,2](长度为2)。我们要求长度在[2,3]内,所以两个都考虑。
[0,2]:对应i=0,长度为3,平均值(1+2+3)/3=2<3 ->不应该计入。
[1,2]:对应i=1,长度为2,平均值(2+3)/2=2.5<3 ->不应该计入。
所以j=3时计数0正确。
再看j=4:对应子数组:
i=1:子数组[1,3](长度为3)即元素[2,3,4],平均值3,满足。
i=2:子数组[2,3](长度为2)即元素[3,4],平均值3.5,满足。
j=5:对应子数组:
i=2:子数组[2,4](长度为3)即[3,4,5],平均值4,满足。
i=3:子数组[3,4](长度为2)即[4,5],平均值4.5,满足。
所以计数4。
36. 但是,再检查一下j=2: 对应的子数组只有i=0,即[0,1](元素1,2),平均值1.5,不满足。所以不计。
37. 因此,代码正确。
38. 但是,我们还要注意:我们的B[0]对应的是prefix[0](即没有元素时),我们允许i=0,因为prefix[j]-prefix[0]是前j个元素的和(即原数组[0,j-1])。所以合理。
39. 但是,我们上面的例子中,当j=5的时候,i=2和3,对应的子数组是[2,4]和[3,4](即原数组下标2,3,4和3,4),正确。
40. 因此,代码实现如下:
注意:由于内层循环可能达到10e6,我们尽量优化内层循环(比如用局部变量替代全局变量)。
41. 优化:将B[j]和B[i]的访问用局部变量存储。
42. 完整代码:
from typing import List, Tuple
class Solution:
def count_intervals(self, records: List[int], length_range: Tuple[int, int], threshold: int) -> int:
n = len(records)
minL, maxL = length_range
# 特殊情况:如果没有记录,返回0
if n == 0:
return 0
# 构建前缀和
prefix = [0] * (n+1)
for i in range(1, n+1):
prefix[i] = prefix[i-1] + records[i-1]
# 构建数组B
B = [0] * (n+1)
# B[0]=0
for i in range(1, n+1):
B[i] = prefix[i] - threshold * i
count = 0
# 遍历j从minL到n
# 注意:当j<minL时,没有满足长度>=minL的子数组,所以跳过。
for j in range(minL, n+1):
low_i = j - maxL
if low_i < 0:
low_i = 0
high_i = j - minL # 注意:high_i必须>=low_i,否则跳过
if high_i < low_i:
continue
# 遍历i从low_i到high_i
# 优化:将B[j]存入局部变量
b_j = B[j]
for i in range(low_i, high_i+1):
if B[i] <= b_j:
count += 1
return count
43. 但是,我们可以进一步优化:如果内层循环能使用二分查找,则可以降低复杂度。我们可以维护窗口内B[i]的有序数组,然后二分查找窗口内有多少个数<=B[j]。但是窗口是滑动的,我们需要动态维护一个有序列表,并支持添加和删除元素。
由于窗口大小最多为maxL-minL+1(最大1000),我们可以用二分查找来定位插入位置,但是删除的时候需要线性时间(因为列表的删除是O(n)的)。这样总复杂度O(n*window_size)和遍历一样。
所以如果直接遍历窗口内元素的时间可以接受,就不需要优化。
44. 但是,我们尝试另一种优化:将窗口内B[i]排序,然后二分查找B[j]的位置,这样内层循环复杂度O(log(window_size)),但是由于每次添加和删除元素需要O(window_size)的时间,所以总时间复杂度仍然是O(n*window_size)。所以没有改善。
45. 因此,我们保留直接遍历窗口内元素的方法。
46. 然而,最坏情况10e6次操作,在Python中可能会超时(10000*1000=10e6,在Pyton中大约1秒?但实际运行环境可能不同,我们最好优化)。
47. 我们可以考虑用归并排序的思想?但是窗口滑动,很难。
48. 或者,我们可以预先计算所有B,然后对于每个j,我们只考虑左端点i的范围,然后利用之前计算的结果?但似乎不行。
49. 我们尝试另一种方法:将左端点的B[i]按照i从小到大存储,同时窗口是连续的一段下标(i是连续的),所以我们可以用一个队列维护窗口内的B[i],然后维护这个队列的升序排列(类似单调队列?),但是这里我们每次需要查询队列内小于等于B[j]的元素个数。我们可以用平衡二叉搜索树,但Python没有。
50. 因此,我们回到直接遍历。提交后如果超时再考虑优化。
51. 实际上,题目中n最大10000,maxL-minL最大1000,那么总操作数10000*1000=10e6,在Pyton中应该能通过(在Pyton3中,10e6次简单操作大约0.5~1秒)。
52. 我们编写代码并进行测试。
53. 注意:有可能出现浮点数问题?但B[i]=prefix[i]-threshold*i,两者都是整数,所以整数比较不会有浮点问题。
54. 最终代码:
from typing import List, Tuple
class Solution:
def count_intervals(self, records: List[int], length_range: Tuple[int, int], threshold: int) -> int:
n = len(records)
minL, maxL = length_range
if n == 0:
return 0
# 前缀和
prefix = [0] * (n+1)
for idx in range(n):
prefix[idx+1] = prefix[idx] + records[idx]
# 构建B数组
B = [0] * (n+1) # B[0]=0
for i in range(1, n+1):
B[i] = prefix[i] - threshold * i
cnt = 0
# 枚举j,从minL到n(包括n)
# 注意:j表示子数组的右边界+1,即子数组为[i, j-1]
for j in range(minL, n+1):
low_i = j - maxL
high_i = j - minL
if low_i < 0:
low_i = 0
if high_i < low_i:
continue
bj = B[j]
# 遍历左端点i
for i in range(low_i, high_i+1):
if B[i] <= bj:
cnt += 1
return cnt
55. 测试:用之前的小例子 [1,2,3,4,5], [2,3], 3 -> 4,通过。
56. 再测试一个边界:records=[0], length_range=[1,1], threshold=0
n=1, minL=1, maxL=1
j从minL=1到1
prefix=[0,0]
B[0]=0, B[1]=0-0*1=0
j=1: low_i=1-1=0, high_i=1-1=0 -> i=0
B[0]=0<=B[1]=0 ->计数1
子数组:长度为1,即[0],平均值0>=0,满足。所以返回1,正确。
57. 另一个边界:records=[1,0], length_range=[1,2], threshold=1
n=2
prefix=[0,1,1]
B[0]=0, B[1]=1-1*1=0, B[2]=1-1*2=-1
j=1: low_i=1-2=-1->0, high_i=1-1=0 -> i=0: 0<=B[1]=0 ->计数1(子数组[0]:平均1>=1)
j=2: low_i=2-2=0, high_i=2-1=1 -> i=0:0<=B[2]=-1? 0<=-1不成立;i=1:0<=-1不成立 -> 计数0
另外,子数组[0,1]:平均值(1+0)/2=0.5<1,所以不满足。
所以总计数1,正确。
58. 因此,我们提交这个代码。
但是,我们担心最坏情况会超时。如果超时,我们可以使用树状数组+离散化(对整个B数组离散化)来优化内层循环为O(log n),同时用滑动窗口维护当前窗口内的B[i]值(用树状数组维护窗口内每个离散化值出现的次数)。
优化版本(如果上面版本超时,则用这个):
步骤:
n = len(records)
minL, maxL = length_range
# 计算前缀和和B数组(长度n+1)
prefix = [0]*(n+1)
for i in range(1, n+1):
prefix[i] = prefix[i-1] + records[i-1]
B = [prefix[i] - threshold * i for i in range(n+1)]
# 离散化B数组:注意,我们只需要离散化B数组,然后树状数组的下标是离散化后的值。
# 但是窗口大小最多maxL-minL+1,所以实际上我们只需要窗口内的元素?不对,我们要查询整个B数组(离散化整个B数组)?因为树状数组需要整个值域。
# 离散化整个B数组(有n+1个数据)
sorted_B = sorted(set(B))
dic = {}
for idx, val in enumerate(sorted_B):
dic[val] = idx+1 # 离散化后的值(1-indexed)
size = len(sorted_B)
# 初始化树状数组(大小为size)
tree = [0]*(size+1) # 1-indexed
# 树状数组函数
def lowbit(x):
return x & (-x)
def update(pos, delta):
while pos <= size:
tree[pos] += delta
pos += lowbit(pos)
def query(pos): # 查询[1,pos]的区间和
s = 0
while pos:
s += tree[pos]
pos -= lowbit(pos)
return s
# 滑动窗口:左端点i的范围,随着j变化
cnt = 0
# 首先,将左端点i在[minL, maxL]?实际上,对于j=minL,我们需要加入的左端点i的范围是 [0, minL-minL=0] 即i=0,但是我们的树状数组需要提前加入这些点吗?
# 我们需要在j从minL到n的过程中,动态加入左端点 i=j-minL,并移除左端点 i=j-maxL-1(当j>maxL时)
# 初始化:在j=minL之前,没有任何左端点加入。然后j从minL开始,每次加入一个左端点(即i=j-minL),当j>maxL时,移除一个左端点(即i=j-maxL-1)。
# 注意:左端点i的取值范围:对于当前的j,左端点i在区间 [j-maxL, j-minL] 内。
# 因此,在j=minL时,我们需要加入的左端点是 i=j-minL=0(因为minL-minL=0)?但是j=minL时,左端点i的范围是 [minL-maxL, 0](因为j-minL=0,j-maxL=minL-maxL<=0,所以左端点范围是[max(0,minL-maxL)=0,0])。
# 所以,我们这样:
# for j in range(minL, n+1):
# 新的左端点:i_new = j - minL -> 加入树状数组:update(dic[B[i_new]], 1)
# 如果j>maxL:那么需要移除左端点 i_old = j - maxL - 1,更新:update(dic[B[i_old]], -1)
# 然后,查询当前树状数组中有多少个数小于等于B[j](即query(dic[B[j]]),注意离散化后的值),然后累加这个数量
# 但是注意:当前树状数组中存储的是当前窗口内(即左端点i在[j-maxL, j-minL])的B[i]值。我们要求满足B[i]<=B[j]的个数,就是query(dic[B[j]])。
for j in range(minL, n+1):
i_new = j - minL # 加入这个左端点
pos_new = dic[B[i_new]]
update(pos_new, 1)
if j > maxL:
i_old = j - maxL - 1
pos_old = dic[B[i_old]]
update(pos_old, -1)
# 查询当前树状数组中小于等于B[j]的元素个数
# 注意:当前树状数组中存储的是左端点i在 [j-maxL, j-minL] 区间的B[i]值
# 而我们要查询的是这些B[i]值中有多少个<=B[j]
# 由于树状数组维护的是离散化后的值,我们直接查询query(dic[B[j]])?不对,因为离散化后,相同值会被压缩,但是树状数组的query(dic[B[j]])返回的就是<=B[j]的个数(因为离散化映射后,比B[j]小的数对应的离散值小于等于dic[B[j]])。
cnt += query(dic[B[j]])
return cnt
然而,这个版本需要验证正确性。而且,注意树状数组在查询时,我们查询的是离散化后的值,因为离散化映射后,我们保证了从小到大的顺序,所以树状数组的query(dic[B[j]])就是窗口内所有小于等于B[j]的个数。
但是,我们注意:离散化时,相同的B[i]值会被映射到同一个离散值,所以树状数组计数正确。
但是,这个版本的复杂度为O(n log(window_size)),但离散化整个B数组需要O(n log n),然后树状数组每次操作O(log(size))(size最大为n+1,即10000),所以总复杂度O(n log n),比直接遍历的O(n*(maxL-minL))(10e6)可能更快。
考虑到题目中n最大10000,log n≈14,所以O(n log n)≈140000,很快。
因此,我们用树状数组的方法来实现。
但是,注意:树状数组查询的是整个值域中小于等于B[j]的个数,但是我们只加入了当前窗口内的左端点。所以正确。
但是,我们的离散化是针对整个B数组的,所以树状数组的大小是离散化后的值域大小(最大10001)。
实现步骤:
1. 计算前缀和和B数组。
2. 离散化整个B数组(包括0~n)。
3. 初始化树状数组(全0)。
4. 遍历j从minL到n:
将左端点 i = j-minL 加入树状数组(即更新树状数组在位置dic[B[i]]的值+1)。
如果j>maxL,则移除左端点 i = j-maxL-1(树状数组更新-1)。
查询树状数组中小于等于B[j]的个数(即query(dic[B[j]])),并累加到答案。
5. 返回答案。
注意:树状数组的query(pos)返回的是小于等于当前离散化值pos(对应的原值为B[j])的个数。
测试例子:records=[1,2,3,4,5], length_range=[2,3], threshold=3
B = [0, -2, -3, -3, -2, 0]
离散化:sorted_B = sorted(set([0,-2,-3,-3,-2,0])) = [-3,-2,0]
dic: -3->1, -2->2, 0->3
树状数组初始化:tree = [0]*(4) -> 索引0不用,1~3。
j=minL=2:
i_new=2-2=0 -> B[0]=0 -> 离散化值3 -> update(3,1): tree[3]+=1, 然后tree[3]=1(同时更新父节点?树状数组更新)
然后查询:B[2]=-3 -> 离散化值1 -> query(1) = tree[1](但是树状数组还没有下标1的值?)
树状数组:tree[1]=0, tree[2]=0, tree[3]=1 -> 查询1:位置1: tree[1]=0,然后lowbit(1)=1,所以结束,返回0?不对,query(1)应该返回tree[1]的值(因为1的二进制001,所以只加tree[1])->0。
所以cnt=0。
j=3:
i_new=3-2=1 -> B[1]=-2 -> update(2,1)
j>maxL? 3>3? 否,不删除。
查询:B[3]=-3 -> 离散化值1 -> query(1)=0 -> cnt=0。
j=4:
i_new=4-2=2 -> B[2]=-3 -> update(1,1) -> tree[1]=1, tree[2]=1? 树状数组:
更新:位置1:tree[1]+=1,然后更新父节点:位置2(1+lowbit(1)=2)-> tree[2]+=1;位置4(2+2=4,超出范围,结束)-> tree[4]不存在,所以停止。
然后,j>maxL? 4>3 -> 移除 i_old=4-3-1=0 -> B[0]=0 -> 离散化值3 -> update(3,-1): tree[3]=0(原来tree[3]=1,现在0),并更新父节点:位置4(3+1=4,跳出)-> 所以tree[3]=0, tree[2]不用更新?不对,树状数组更新:更新位置3:tree[3]-=1,然后位置4(3+1=4>size=3,结束)。
然后,查询:B[4]=-2 -> 离散化值2 -> query(2)=tree[2](tree[2]在更新位置1的时候已经更新为1了?)-> 树状数组的query(2):
s=0
pos=2: s+=tree[2]=1, 然后pos=2-lowbit(2)=0 -> 结束。所以返回1。
然后cnt=0+0+1=1。
j=5:
i_new=5-2=3 -> B[3]=-3 -> update(1,1) -> 此时树状数组位置1:tree[1]原来是1,现在变为2?不对,tree[1]原本为1,更新后为2;然后更新父节点:位置2(1+1=2)-> tree[2]=1+1=2;位置4(2+2=4>3,结束)。
j>maxL: 5>3 -> 移除 i_old=5-3-1=1 -> B[1]=-2 -> update(2,-1): tree[2]从2->1,并更新父节点:位置4(2+2=4>3,结束)。
查询:B[5]=0 -> 离散化值3 -> query(3)= tree[3]+tree[2](因为3的二进制011,先加tree[3]=0,然后3-lowbit(3)=011-001=010=2,加tree[2]=1) -> 一共1。
cnt=1+1=2。
最终计数2,但正确答案是4。所以错误。
错误分析:
在j=4时,窗口内的左端点i:j-minL=4-2=2, j-maxL=4-3=1,所以左端点i在[1,2](注意:闭区间,包括1和2)?不对,我们的窗口左端点i的范围是:[j-maxL, j-minL] -> [4-3=1, 4-2=2]。
但是,在树状数组中,我们加入了哪些点?
j=2: 加入i=0 -> 此时窗口内[0](但此时对应的左端点i范围应该是[0,0])
j=3: 加入i=1 -> 窗口内[0,1](但此时j=3,左端点范围应该是[0,1]?不对,j=3时,左端点范围:[3-3=0, 3-2=1] -> [0,1],所以加入i=1后,窗口内应该有i=0(在j=2加入)和i=1(在j=3加入))。
j=4:
加入i=2 -> 窗口内应该变成[0,1,2]?但是j=4时,左端点范围应该是[4-3=1,4-2=2] -> [1,2]。所以当j>maxL时(j>=4>3),我们会在j=4时移除i_old=4-3-1=0(即移除i=0),所以窗口内为[1,2]。
j=5:
加入i=3 -> 窗口内[1,2,3],然后移除i_old=5-3-1=1 -> 移除i=1,所以窗口内[2,3]。
因此,树状数组在j=4时,窗口内是i=1和2,而我们加入树状数组的点:i=1(B[1]=-2)和i=2(B[2]=-3) -> 在树状数组中,离散化后,位置2(-2)有1,位置1(-3)有1。
查询B[4]=-2,离散化值2,query(2)返回的是树状数组中值小于等于-2的个数:包括-2(1个)和-3(1个) -> 2个。所以j=4时应计数2。
在j=5时,窗口内是i=2和3:B[2]=-3, B[3]=-3 -> 树状数组:位置1(-3)有2个(注意之前加入i=2时在位置1加1,加入i=3时也在位置1加1,然后移除i=1(位置2减1)后,位置1还是2?不对,我们加入i=2(j=4)时在位置1加1,加入i=3(j=5)时在位置1加1,然后移除i=1(j=5)时在位置2减1,所以树状数组位置1有2,位置2有0?不对,位置2在移除i=1(B[1]=-2)时减1,所以位置2从1变为0。所以树状数组只有位置1有2。
查询B[5]=0,离散化值3,query(3)返回树状数组所有元素个数(因为0是离散化最大值),即2。所以j=5计数2。
总计数:j=2:0, j=3:0, j=4:2, j=5:2 -> 4,正确。
因此,代码修改:在树状数组版本中,我们加入和移除的索引是:
j: 从minL到n
加入:i = j - minL (必须确保j>=minL,所以i>=0)
移除:i = j - maxL - 1 (当j>maxL时,即j>=maxL+1时,移除 i = j-maxL-1。注意j-maxL-1要>=0,所以当j>maxL时,j-maxL-1>=0?不一定,如果j-maxL-1<0则不移除)
所以,在移除时,需要判断 i_old = j-maxL-1 >=0 才移除。
修正后的树状数组版本代码:
from typing import List, Tuple
class Solution:
def count_intervals(self, records: List[int], length_range: Tuple[int, int], threshold: int) -> int:
n = len(records)
minL, maxL = length_range
if n == 0:
return 0
# 前缀和
prefix = [0] * (n+1)
for i in range(1, n+1):
prefix[i] = prefix[i-1] + records[i-1]
# B数组
B = [0] * (n+1)
for i in range(1, n+1):
B[i] = prefix[i] - threshold * i
# 离散化
sorted_B = sorted(set(B))
size_b = len(sorted_B)
dic = {val: idx+1 for idx, val in enumerate(sorted_B)} # 离散化映射到1..size_b
# 树状数组
tree = [0] * (size_b+1) # 1-indexed
def lowbit(x):
return x & -x
def update(pos, delta):
while pos <= size_b:
tree[pos] += delta
pos += lowbit(pos)
def query(pos):
s = 0
while pos:
s += tree[pos]
pos -= lowbit(pos)
return s
total_count = 0
# 遍历j from minL to n
for j in range(minL, n+1):
# 加入新的左端点 i = j - minL
i_new = j - minL
# 离散化位置
pos_new = dic[B[i_new]]
update(pos_new, 1)
# 当 j > maxL 时,移除 i_old = j - maxL - 1 (如果>=0)
if j > maxL:
i_old = j - maxL - 1
if i_old >= 0: # 确保不越界
pos_old = dic[B[i_old]]
update(pos_old, -1)
# 查询
# 注意:B[j]可能不在离散化字典中?但B[j] in set(B) -> 所以dic[B[j]]存在。
pos_j = dic[B[j]]
total_count += query(pos_j)
return total_count
59. 测试:records=[1,2,3,4,5], [2,3], 3 -> 4
records=[0], [1,1], 0 -> 1
records=[1,0], [1,2], 1 -> 1
60. 这个版本的时间复杂度为O(n log(size_b)),size_b<=n+1,所以O(n log n),效率更高。
61. 因此,我们选择树状数组版本。
注意:树状数组下标从1开始。
由于题目要求,我们提交树状数组版本(即使直接暴力也能过,但树状数组更优)。
62. 但是,离散化时,我们用了set(B)然后排序,注意B[0]也在内。
63. 编写代码并测试。
64. 边界:n=0 -> return 0。
65. 提交。
</think>```python
from typing import List, Tuple
class Solution:
def count_intervals(self, records: List[int], length_range: Tuple[int, int], threshold: int) -> int:
n = len(records)
minL, maxL = length_range
if n == 0:
return 0
# 计算前缀和
prefix = [0] * (n + 1)
for i in range(1, n + 1):
prefix[i] = prefix[i - 1] + records[i - 1]
# 构建B数组:B[i] = prefix[i] - threshold * i
B = [0] * (n + 1)
for i in range(1, n + 1):
B[i] = prefix[i] - threshold * i
# 离散化B数组
unique_b = sorted(set(B))
val_to_idx = {val: idx + 1 for idx, val in enumerate(unique_b)}
size = len(unique_b)
# 树状数组实现
tree = [0] * (size + 1)
def lowbit(x):
return x & -x
def update(pos, delta):
while pos <= size:
tree[pos] += delta
pos += lowbit(pos)
def query(pos):
s = 0
while pos > 0:
s += tree[pos]
pos -= lowbit(pos)
return s
count = 0
# 遍历右端点j从minL到n
for j in range(minL, n + 1):
# 加入新左端点:i = j - minL
new_i = j - minL
new_pos = val_to_idx[B[new_i]]
update(new_pos, 1)
# 移除过期左端点:当j>maxL时,移除i = j - maxL - 1
if j > maxL:
old_i = j - maxL - 1
# 确保索引有效
if old_i >= 0:
old_pos = val_to_idx[B[old_i]]
update(old_pos, -1)
# 查询当前窗口内满足B[i] <= B[j]的个数
j_pos = val_to_idx[B[j]]
count += query(j_pos)
return count
```
### 算法说明:
1. **前缀和与转换**:计算数组前缀和,并构建转换数组 `B`,其中 `B[i] = prefix[i] - threshold * i`。这步将平均值条件转化为 `B[j] >= B[i]`。
2. **离散化**:对 `B` 数组离散化处理,将大范围值映射到紧凑区间,便于树状数组操作。
3. **树状数组**:用于高效维护滑动窗口内的 `B[i]` 值,支持动态添加/删除及范围查询。
4. **滑动窗口**:遍历右端点 `j`(子数组结束位置+1),动态维护左端点 `i` 的范围 `[j-maxL, j-minL]`:
- 加入新左端点 `j-minL`
- 当 `j > maxL` 时移除过期左端点 `j-maxL-1`
- 查询窗口内满足 `B[i] <= B[j]` 的数量
5. **复杂度**:
- 时间复杂度:$O(n \log n)$(离散化 + 树状数组操作)
- 空间复杂度:$O(n)$(存储前缀和、离散化映射等)