2026-11-06:数组中的峰值Ⅱ。用go语言,给定一个整数数组 nums,长度为 n;再给定若干查询,存放在二维数组 queries 中。需要按顺序处理
🤖 AI总结
主题
力扣4017题:数组中的峰值Ⅱ的线段树解法。
摘要
用线段树维护区间峰值信息,合并时扣除不合法跨区间子段,实现O(log n)查询与修改。
关键信息
- 1 将合法子段转化为包含内部峰值的子段。
- 2 线段树节点维护len、hasPeak、pre、suf、cnt五个信息。
- 3 单点修改最多影响三个位置的峰值状态,总复杂度O(n+q log n)。
2026-11-06:数组中的峰值Ⅱ。用go语言,给定一个整数数组 nums,长度为 n;再给定若干查询,存放在二维数组 queries 中。需要按顺序处理这些查询,并维护数组 nums 的当前值。
一个连续子段如果同时满足下面两个条件,就称为“符合条件的子段”:
第一,子段长度不少于 3;
第二,子段内部存在某个位置 k,使得该位置的数严格大于它左边相邻的数,并且严格大于它右边相邻的数。也就是说,在子段内部存在一个严格峰值。
查询分为两类:
第一类查询格式为 [1, li, ri]。它的任务是统计:所有完全位于 nums[li] 到 nums[ri] 这个区间内的连续子段中,有多少个是上述“符合条件的子段”。这里要求子段的起点和终点都不能超出 li 和 ri。
第二类查询格式为 [2, indexi, vali]。它的任务是把 nums[indexi] 的值修改为 vali。修改会立即生效,并影响之后的所有查询。
最终需要返回一个结果数组 answer。answer 中依次保存每个第一类查询的答案,顺序与这些查询在 queries 中出现的顺序一致。第二类查询不产生输出。
3 <= n == nums.length <= 100000。
0 <= nums[i] <= 100000。
1 <= queries.length <= 100000。
queries[i] = [1, li, ri] 或 queries[i] = [2, indexi, vali]。
0 <= li < ri <= n – 1。
0 <= indexi <= n – 1。
0 <= vali <= 100000。
输入: nums = [1,3,2,4], queries = [[1,0,3],[2,1,1],[1,0,3]]。
输出: [2,0]。
解释:
查询 [1, 0, 3]:
[1, 3, 2]:选择 k = 1。则 nums[k] = 3,nums[k – 1] = 1,且 nums[k + 1] = 2。因为 3 > 1 且 3 > 2,这是一个峰值子数组。
[1, 3, 2, 4]:选择 k = 1。则 nums[k] = 3,nums[k – 1] = 1,且 nums[k + 1] = 2。因为 3 > 1 且 3 > 2,这是一个峰值子数组。
查询 [2, 1, 1]:将 nums[1] 更新为 1。数组变为 [1, 1, 2, 4]。
查询 [1, 0, 3]:现在没有峰值子数组。
因此,answer = [2, 0]。
题目来自力扣4017。
大体步骤如下: 1. 峰值和“符合条件的子段”的转化
数组某个位置p是峰值,当且仅当:
•p不是数组左端点;
•p不是数组右端点;
•nums[p-1] < nums[p];
•nums[p] > nums[p+1]。
题目要求统计的子段必须满足:
• 长度至少为 3;
• 子段内部存在一个峰值,即该峰值不能是子段端点。
因此,一个子段“符合条件”,等价于:这个子段内部至少包含一个峰值位置。
因为峰值本身需要左右相邻元素,所以只要峰值在子段内部,子段长度自然至少为 3。
代码中的线段树维护的正是:某个区间内有多少个连续子段包含至少一个“内部峰值”。
2. 线段树节点保存的信息
每个线段树节点对应原数组的一个连续区间,保存五个信息:
1.len:这个区间的长度。
2.hasPeak:这个区间内是否存在至少一个峰值位置。
3.pre:从区间左端点开始,到区间内第一个峰值位置为止的长度,包含该峰值位置。
如果整个区间没有峰值,那么pre就等于区间长度。
4.suf:从区间右端点开始,到区间内最后一个峰值位置为止的长度,包含该峰值位置。
如果整个区间没有峰值,那么suf就等于区间长度。
5.cnt:这个区间内,所有包含至少一个内部峰值的连续子段数量。
对于第一类查询,最终答案就是查询区间对应节点的cnt。
对于单个元素构成的叶子节点:
•len = 1;
•pre = 1;
•suf = 1;
•cnt = 0,因为单个元素不可能形成符合条件的子段;
•hasPeak根据原数组中该位置是否为峰值来确定。
叶子节点的hasPeak只表示“这个位置本身是不是峰值”。
3. 两个区间的合并过程
线段树的核心是把左右两个子区间的信息合并成父区间的信息。
假设左区间为a,右区间为b。
3.1 合并后的长度
合并后长度就是:
a.len + b.len。
3.2 合并后的hasPeak
只要左区间或右区间任意一个有峰值,合并后就有峰值:
a.hasPeak || b.hasPeak。
3.3 合并后的pre
pre本来应该从合并区间的左端点开始,找到第一个峰值位置。
• 先继承左区间的pre;
• 如果左区间完全没有峰值,那么左区间的整个前缀都不会遇到峰值,于是还要继续延伸到右区间,加上右区间的pre;
• 如果左区间有峰值,那么第一个峰值已经在左区间内出现,不能继续延伸到右区间。
所以合并后的pre由左区间的pre决定,必要时加上右区间的pre。
3.4 合并后的suf
suf是对称的:
• 先继承右区间的suf;
• 如果右区间完全没有峰值,那么还要继续向左延伸到左区间,加上左区间的suf;
• 如果右区间有峰值,那么最后一个峰值已经在右区间内出现,不能继续延伸到左区间。
3.5 合并后的cnt
左右区间内部已经统计过的合法子段数量分别是:
•a.cnt;
•b.cnt。
还需要统计“跨越左右两个区间的子段”。
跨越左右区间的子段总数量是:
a.len * b.len。
因为跨区间子段的起点可以在左区间任意位置,终点可以在右区间任意位置。
但是这些跨区间子段中,并不是全部都包含“内部峰值”。有些跨区间子段虽然跨过左右区间,但峰值可能落在子段端点上,或者根本没有峰值。
需要扣除的数量是:
a.suf * b.pre。
原因是:
•a.suf表示左区间从右端开始到最后一个峰值位置的长度;
•b.pre表示右区间从左端开始到第一个峰值位置的长度;
• 如果一个跨区间子段的起点落在左区间的这段后缀中,终点落在右区间的这段前缀中,那么这个子段不会把任何峰值包在内部,峰值最多只能落在子段端点上,因此不合法。
所以跨区间新增的合法子段数量为:
a.len * b.len - a.suf * b.pre。
最终:
cnt = a.cnt + b.cnt + a.len * b.len - a.suf * b.pre。
4. 建树过程
建树时递归处理整个数组区间。
• 如果当前区间只有一个位置,就建立叶子节点:
• 长度、pre、suf都是 1;
•cnt是 0;
• 根据左右邻居判断该位置是否是峰值,得到hasPeak。
• 如果当前区间长度大于 1:
• 先递归建立左子树;
• 再递归建立右子树;
• 然后使用上面的合并规则,把左右子节点合并成当前节点。
建树完成后,根节点保存整个数组区间的统计信息。
5. 第一类查询:统计区间答案
第一类查询是[1, li, ri],需要统计所有完全位于[li, ri]内的合法子段数量。
线段树的查询过程如下:
1. 如果当前节点区间完全被查询区间[li, ri]覆盖,就直接返回当前节点的信息。
2. 如果查询区间只在左子树中,就递归查询左子树。
3. 如果查询区间只在右子树中,就递归查询右子树。
4. 如果查询区间同时覆盖左右子树,就分别查询左右两边,然后把两个查询结果按照合并规则合并。
最终返回的结果中,cnt就是区间[li, ri]内所有符合条件的子段数量。
把这个cnt加入答案数组即可。
注意:查询返回的pre、suf是相对于查询区间的,而不是相对于整棵线段树原始区间的,这样不会把查询区间外的峰值错误算进来。
6. 第二类查询:单点修改
第二类查询是[2, index, val],表示把nums[index]改成val。
一个位置是否是峰值,只取决于它自己和左右相邻元素。因此修改nums[index]后,只有最多三个位置的峰值状态可能改变:
1. 位置index - 1:它的右邻居是index,可能受影响;
2. 位置index:它自身被修改,可能受影响;
3. 位置index + 1:它的左邻居是index,可能受影响。
代码中分别检查这三个位置。
对于每个可能受影响的位置:
1. 用修改前的数组值计算它原来是不是峰值,记为旧状态;
2. 用新的值val临时替代nums[index],计算它修改后是不是峰值,记为新状态;
3. 如果新旧状态不同,说明这个位置的hasPeak需要翻转,于是调用线段树的点更新,把这个叶子节点的hasPeak取反;
4. 点更新会沿着路径向上重新合并节点,更新祖先的pre、suf、cnt、hasPeak。
三个可能位置检查完后,再把nums[index]真正改成val。
注意边界条件:
• 位置 0 和位置 n-1 不可能是峰值,因此相关检查会跳过;
• 如果index太靠左或太靠右,某些相邻位置不存在,也不会检查。
由于每次修改最多只影响三个位置的峰值状态,所以最多只做三次线段树点更新。
7. 主流程总结
整体处理流程是:
1. 根据初始数组nums建立线段树;
2. 按顺序遍历所有查询;
3. 如果当前查询是第一类查询:
• 查询线段树区间[li, ri];
• 取出结果的cnt;
• 加入答案数组;
4. 如果当前查询是第二类查询:
• 检查index-1、index、index+1三个位置的峰值状态是否变化;
• 对发生变化的位置在线段树中翻转对应叶子的hasPeak;
• 最后把nums[index]更新为val;
5. 所有查询处理完后返回答案数组。
对于示例:
• 初始nums = [1,3,2,4],位置 1 是峰值;
• 查询[1,0,3]时,符合条件的子段是[1,3,2]和[1,3,2,4],答案为 2;
• 更新nums[1] = 1后,数组变为[1,1,2,4],峰值消失;
• 再次查询[1,0,3],答案为 0。
所以最终答案是[2,0]。
8. 时间复杂度
设数组长度为n,查询数量为q。
• 建树需要遍历所有叶子并合并,时间复杂度为O(n);
• 每次第一类查询是线段树区间查询,时间复杂度为O(log n);
• 每次第二类修改最多检查三个位置,最多进行三次线段树点更新,每次点更新为O(log n),因此修改总代价仍为O(log n)。
所以总时间复杂度为:
O(n + q log n)。
9. 额外空间复杂度
• 线段树需要存储约O(n)个节点;
• 递归建树、查询、更新的递归深度为O(log n);
• 答案数组需要保存所有第一类查询的结果,最多O(q)。
如果只计算辅助数据结构,不计返回答案数组,则额外空间复杂度为:
O(n)。
如果把返回答案数组也计入额外空间,则为:
O(n + q)。
Go完整代码如下:
package main
import (
"fmt"
"math/bits"
)
type data struct {
cnt, pre, suf, lenint
hasPeak bool
}
type seg []datafunc newSegmentTree(a []int) seg {
n := len(a)
t := make(seg, 2< uint (n -1 )))
t.build(a, 1 , 0 , n -1 )
return t
}
func (seg) mergeData(a, b data) data {
cnt := a.cnt + b.cnt + a. len *b. len - a.suf*b.pre
pre := a.pre
if !a.hasPeak {
pre += b.pre
}
suf := b.suf
if !b.hasPeak {
suf += a.suf
}
return data{cnt, pre, suf, a. len + b. len , a.hasPeak || b.hasPeak}
}
func (t seg) maintain(node int ) {
t[node] = t.mergeData(t[node* 2 ], t[node* 2 + 1 ])
}
func (t seg) build(a [] int , node, l, r int ) {
if l == r { // 叶子
hasPeak := 0 < l && l < len (a) -1 && a[l -1 ] < a[l] && a[l] > a[l+ 1 ]
t[node] = data{ 0 , 1 , 1 , 1 , hasPeak} // 初始化叶节点的值
return
}
m := (l + r) >> 1
t.build(a, node* 2 , l, m) // 初始化左子树
t.build(a, node* 2 + 1 , m+ 1 , r) // 初始化右子树
t.maintain(node)
}
func (t seg) update(node, l, r, i int ) {
if l == r { // 叶子(到达目标)
t[node].hasPeak = !t[node].hasPeak
return
}
m := (l + r) >> 1
if i <= m { // i 在左子树
t.update(node* 2 , l, m, i)
} else { // i 在右子树
t.update(node* 2 + 1 , m+ 1 , r, i)
}
t.maintain(node)
}
func (t seg) query(node, l, r, ql, qr int ) data {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
return t[node]
}
m := (l + r) >> 1
if qr <= m { // [ql, qr] 与右子树无交集,仅需递归左子树
return t.query(node* 2 , l, m, ql, qr)
}
if ql > m { // [ql, qr] 与左子树无交集,仅需递归右子树
return t.query(node* 2 + 1 , m+ 1 , r, ql, qr)
}
// [ql, qr] 与左右子树均有交集,分别递归,然后合并结果
return t.mergeData(t.query(node* 2 , l, m, ql, qr), t.query(node* 2 + 1 , m+ 1 , r, ql, qr))
}
func countOfPeaks(nums []int, queries [][]int) (ans [] int64 ) {
n := len (nums)
t := newSegmentTree(nums)
for _, q := range queries {
if q[ 0 ] == 1 {
ans = append (ans, int64 (t.query( 1 , 0 , n -1 , q[ 1 ], q[ 2 ]).cnt))
continue
}
i, v := q[ 1 ], q[ 2 ]
if i > 1 {
oldHas := nums[i -2 ] < nums[i -1 ] && nums[i -1 ] > nums[i]
newHas := nums[i -2 ] < nums[i -1 ] && nums[i -1 ] > v
if newHas != oldHas {
t.update( 1 , 0 , n -1 , i -1 )
}
}
if 0 < i && i < n -1 {
oldHas := nums[i -1 ] < nums[i] && nums[i] > nums[i+ 1 ]
newHas := nums[i -1 ] < v && v > nums[i+ 1 ]
if newHas != oldHas {
t.update( 1 , 0 , n -1 , i)
}
}
if i < n -2 {
oldHas := nums[i] < nums[i+ 1 ] && nums[i+ 1 ] > nums[i+ 2 ]
newHas := v < nums[i+ 1 ] && nums[i+ 1 ] > nums[i+ 2 ]
if newHas != oldHas {
t.update( 1 , 0 , n -1 , i+ 1 )
}
}
nums[i] = v
}
return
}
func main() {
nums := [] int { 1 , 3 , 2 , 4 }
queries := [][] int {{ 1 , 0 , 3 }, { 2 , 1 , 1 }, { 1 , 0 , 3 }}
result := countOfPeaks(nums, queries)
fmt.Println(result)
}
![]()
Python完整代码如下:
# -*-coding:utf-8-*-
from typing import List
class Data:
__slots__ = ("cnt", "pre", "suf", "length", "hasPeak")
def __init__(self, cnt=0, pre=0, suf=0, length=0, hasPeak=False):
self.cnt = cnt
self.pre = pre
self.suf = suf
self.length = length
self.hasPeak = hasPeak
class SegmentTree:
def __init__(self, a: List[int]):
self.n = len(a)
size = 2 << ((self.n - 1).bit_length()) if self.n > 0else2
self.t = [Data() for _ in range(size)]
if self.n > 0:
self.build(a, 1, 0, self.n - 1)
def merge_data(self, a: Data, b: Data) -> Data:
cnt = a.cnt + b.cnt + a.length * b.length - a.suf * b.pre
pre = a.pre
if not a.hasPeak:
pre += b.pre
suf = b.suf
if not b.hasPeak:
suf += a.suf
return Data(
cnt,
pre,
suf,
a.length + b.length,
a.hasPeak or b.hasPeak,
)
def maintain(self, node: int) -> None:
self.t[node] = self.merge_data(self.t[node * 2], self.t[node * 2 + 1])
def build(self, a: List[int], node: int, l: int, r: int) -> None:
if l == r:
has_peak = (
0 < l < len(a) - 1
and a[l - 1] < a[l]
and a[l] > a[l + 1]
)
self.t[node] = Data(0, 1, 1, 1, has_peak)
return
m = (l + r) // 2
self.build(a, node * 2, l, m)
self.build(a, node * 2 + 1, m + 1, r)
self.maintain(node)
def update(self, node: int, l: int, r: int, i: int) -> None:
if l == r:
self.t[node].hasPeak = not self.t[node].hasPeak
return
m = (l + r) // 2
if i <= m:
self.update(node * 2, l, m, i)
else:
self.update(node * 2 + 1, m + 1, r, i)
self.maintain(node)
def query(self, node: int, l: int, r: int, ql: int, qr: int) -> Data:
if ql <= l and r <= qr:
return self.t[node]
m = (l + r) // 2
if qr <= m:
return self.query(node * 2, l, m, ql, qr)
if ql > m:
return self.query(node * 2 + 1, m + 1, r, ql, qr)
return self.merge_data(
self.query(node * 2, l, m, ql, qr),
self.query(node * 2 + 1, m + 1, r, ql, qr),
)
def count_of_peaks(nums: List[int], queries: List[List[int]]) -> List[int]:
n = len(nums)
tree = SegmentTree(nums)
ans = []
for q in queries:
if q[0] == 1:
ans.append(tree.query(1, 0, n - 1, q[1], q[2]).cnt)
continue
i, v = q[1], q[2]
if i > 1:
old_has = nums[i - 2] < nums[i - 1] and nums[i - 1] > nums[i]
new_has = nums[i - 2] < nums[i - 1] and nums[i - 1] > v
if new_has != old_has:
tree.update(1, 0, n - 1, i - 1)
if0 < i < n - 1:
old_has = nums[i - 1] < nums[i] and nums[i] > nums[i + 1]
new_has = nums[i - 1] < v and v > nums[i + 1]
if new_has != old_has:
tree.update(1, 0, n - 1, i)
if i < n - 2:
old_has = nums[i] < nums[i + 1] and nums[i + 1] > nums[i + 2]
new_has = v < nums[i + 1] and nums[i + 1] > nums[i + 2]
if new_has != old_has:
tree.update(1, 0, n - 1, i + 1)
nums[i] = v
return ans
def main() -> None:
nums = [1, 3, 2, 4]
queries = [[1, 0, 3], [2, 1, 1], [1, 0, 3]]
result = count_of_peaks(nums, queries)
print(result)if __name__ == "__main__":
main()
![]()
C++完整代码如下:
using namespace std;
struct Data {
long long cnt;
int pre, suf, len;
bool hasPeak;
Data() : cnt(0), pre(0), suf(0), len(0), hasPeak(false) {}
Data(long long c, int p, int s, int l, bool h)
: cnt(c), pre(p), suf(s), len(l), hasPeak(h) {}
};
class SegmentTree {
private:
vector t;
int n;
Data mergeData(const Data& a, const Data& b) {
long long cnt = a.cnt + b.cnt
+ 1LL * a.len * b.len
- 1LL * a.suf * b.pre;
int pre = a.pre;
if (!a.hasPeak) {
pre += b.pre;
}
int suf = b.suf;
if (!b.hasPeak) {
suf += a.suf;
}
return Data(cnt, pre, suf, a.len + b.len, a.hasPeak || b.hasPeak);
}
void maintain(int node) {
t[node] = mergeData(t[node * 2], t[node * 2 + 1]);
}
void build(const vector& a, int node, int l, int r) {
if (l == r) {
int n = (int)a.size();
bool hasPeak = (0 < l && l < n - 1 && a[l - 1] < a[l] && a[l] > a[l + 1]);
t[node] = Data(0, 1, 1, 1, hasPeak);
return;
}
int m = (l + r) >> 1;
build(a, node * 2, l, m);
build(a, node * 2 + 1, m + 1, r);
maintain(node);
}
public:
SegmentTree(const vector& a) {
n = (int)a.size();
t.assign(4 * n + 5, Data());
if (n > 0) {
build(a, 1, 0, n - 1);
}
}
void update(int node, int l, int r, int i) {
if (l == r) {
t[node].hasPeak = !t[node].hasPeak;
return;
}
int m = (l + r) >> 1;
if (i <= m) {
update(node * 2, l, m, i);
} else {
update(node * 2 + 1, m + 1, r, i);
}
maintain(node);
}
Data query(int node, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
return t[node];
}
int m = (l + r) >> 1;
if (qr <= m) {
return query(node * 2, l, m, ql, qr);
}
if (ql > m) {
return query(node * 2 + 1, m + 1, r, ql, qr);
}
return mergeData(
query(node * 2, l, m, ql, qr),
query(node * 2 + 1, m + 1, r, ql, qr)
);
}
};vector countOfPeaks(vector< int>& nums, vector int >>& queries) {
int n = ( int )nums.size();
SegmentTree tree(nums);
vector ans;
for (auto& q : queries) {
if (q[ 0 ] == 1 ) {
ans.push_back(tree.query( 1 , 0 , n - 1 , q[ 1 ], q[ 2 ]).cnt);
continue ;
}
int i = q[ 1 ], v = q[ 2 ];
if (i > 1 ) {
bool oldHas = nums[i - 2 ] < nums[i - 1 ] && nums[i - 1 ] > nums[i];
bool newHas = nums[i - 2 ] < nums[i - 1 ] && nums[i - 1 ] > v;
if (newHas != oldHas) {
tree.update( 1 , 0 , n - 1 , i - 1 );
}
}
if ( 0 < i && i < n - 1 ) {
bool oldHas = nums[i - 1 ] < nums[i] && nums[i] > nums[i + 1 ];
bool newHas = nums[i - 1 ] < v && v > nums[i + 1 ];
if (newHas != oldHas) {
tree.update( 1 , 0 , n - 1 , i);
}
}
if (i < n - 2 ) {
bool oldHas = nums[i] < nums[i + 1 ] && nums[i + 1 ] > nums[i + 2 ];
bool newHas = v < nums[i + 1 ] && nums[i + 1 ] > nums[i + 2 ];
if (newHas != oldHas) {
tree.update( 1 , 0 , n - 1 , i + 1 );
}
}
nums[i] = v;
}
return ans;
}
int main() {
vector< int > nums = { 1 , 3 , 2 , 4 };
vector int >> queries = {
{ 1 , 0 , 3 },
{ 2 , 1 , 1 },
{ 1 , 0 , 3 }
};
vector result = countOfPeaks(nums, queries);
cout << "[" ;
for (size_t i = 0 ; i < result.size(); ++i) {
if (i > 0 ) cout << " " ;
cout << result[i];
}
cout << "]" << endl;
return 0 ;
}
![]()
芯谷ai:https://xinguhub.com/sign-up?aff=txk5
我们相信人工智能为普通人提供了一种“增强工具”,并致力于分享全方位的AI知识。在这里,您可以找到最新的AI科普文章、工具评测、提升效率的秘籍以及行业洞察。 欢迎关注“福大大架构师每日一题”,发消息可获得面试资料,让AI助力您的未来发展。