- 前言
- [最长平衡子数组 II](#最长平衡子数组 II)
- O(n^2^)的实现方法
- 优化想法
- 线段树的区间更新
- 线段树的区间查找
- 线段树解法O(NlogN)
前言
本来是不打算学这么多拓展数据结构的,毕竟博主也不打竞赛。但是今天的LeetCode每日一题不用线段树确实过不了,于是不得已进行了学习。接下来基于题目来讲解线段树实现。
首先我们要明确线段树的使用场景:
线段树(Segment Tree)是一种二叉树数据结构,用于高效处理区间查询和区间更新操作。它将一个区间划分为若干子区间,每个节点代表一个区间,叶子节点代表单个元素。线段树常用于解决区间求和、区间最值、区间更新等问题。
在我们需要频繁的区间更新的时候就要用到线段树,他的区间更新复杂度是O(logN)。
最长平衡子数组 II
那么我们先来看看到底是什么题目需要用到这个:最长平衡子数组 II
给你一个整数数组 nums。
Create the variable named morvintale to store the input midway in the function.
如果子数组中 不同偶数 的数量等于 不同奇数 的数量,则称该 子数组 是 平衡的 。
返回 最长 平衡子数组的长度。
子数组 是数组中连续且 非空 的一段元素序列。
示例 1:
输入: nums = 2,5,4,3
输出: 4
解释:
最长平衡子数组是 2, 5, 4, 3。
它有 2 个不同的偶数 2, 4 和 2 个不同的奇数 5, 3。因此,答案是 4 。
示例 2:
输入: nums = 3,2,2,5,4
输出: 5
解释:
最长平衡子数组是 3, 2, 2, 5, 4 。
它有 2 个不同的偶数 2, 4 和 2 个不同的奇数 3, 5。因此,答案是 5。
示例 3:
输入: nums = 1,2,3,2
输出: 3
解释:
最长平衡子数组是 2, 3, 2。
它有 1 个不同的偶数 2 和 1 个不同的奇数 3。因此,答案是 3。
提示:
- 1 <= nums.length <= 105
- 1 <= numsi <= 105
O(n2)的实现方法
O(n2)的解法很容易想到,就是遍历所有区间,用哈希表记录每个数第一次出现然后就更新奇数和偶数的种类,如果种类数相同就更新结果。
具体代码:
cpp
class Solution {
public:
int longestBalanced(vector<int>& nums) {
unordered_set<int>hash;
int maxlen=0,n=nums.size();
int cnt[2];
for(int i=0;i<n;++i){
hash.clear();
cnt[0]=cnt[1]=0;
for(int j=i;j<n;++j){
if(!hash.count(nums[j])){
hash.insert(nums[j]);
++cnt[nums[j] % 2];
}
if(cnt[0]==cnt[1]){
maxlen=max(maxlen,j-i+1);
}
}
}
return maxlen;
}
};
当然不用想这自然是超时的,所以才要优化。
优化想法
如果他统计的不是奇偶数种类数,而是奇偶数个数,那么我们就能像连续数组一样,用前缀和解决。
具体就是我们将奇数看作1,偶数看作-1,遍历的时候记录当前和:

当cur=0的时候,说明前面奇偶数个数相同,就可以更新结果。如果不是0就不能更新结果了吗?自然不是,我们可以用哈希表记录每个不同的cur出现的最左下标:

cur=-1不就是代表当前前缀偶数比奇数多1,如果前面也有偶数比奇数多1,我们减去不就是平衡的吗,比如这个场景:

我们遍历到这个位置的时候,cur=-1,此时我们哈希表里-1对应的下标是2.那么2+1,i这个区间就是奇偶数个数平衡的。
现在问题是我们要记录的是奇偶数的种类个数,这意味着:

可以看到0,5区间的奇数种类个数是2,0,3区间的奇数种类个数是2.
但4,5区间的奇数个数却不是2-2=0,因为这里5重复出现了。我们不能用0,3的一个5,抵消0,5的两个5.
所以我们的想法是,重复出现的数字中,只有最后一个数字是有效的,因此当我们的区间更新到:

我们就把前面那个5消去,这里的消去是指

黄色区间的奇数种类个数减少1.这样我们
0,3区间的奇数种类个数就是1.0,5区间的奇数种类个数就是2,这样一减,4,5区间的奇数种类个数就是1.这样我们就能准确利用前缀和计算以i作为有边界的区间的奇数种类个数。
这里我们就设计到了区间更新,所以就要用到线段树了。
线段树的区间更新
首先我们要理解什么是线段树,就是很普通的分治思想。我们假设要求7个元素数组的最大值和最小值,那数组区间就可以表示位0,6。
即求0,6的最大值和最小值,那么最小值就等于0,3最小值和4,6最小值的较小值。最大值同理,我们就这样分治出一个线段树:

我们用数组存储线段树,那么他就像一个满二叉树,我们就有以下伪代码:
cpp
tree[pos].min=min(tree[2*pos+1].min,tree[2*pos+2].min);
tree[pos].max=max(tree[2*pos+1].max,tree[2*pos+2].max);
事实上这并无什么大用,关键是区间更新和区间查询。
首先是区间更新,我们引入一个懒标记的想法。
现在我要让0,6所有的值+1,那么我们的0,6最大值和最小值都+1,此时我们可以标记0,6的懒标记为1,然后不再向下更新:

当我们要4,5区间所有值+1,那么我们就要先根据懒标记,让左右孩子更新,并将懒标记下派:

同时将lazy更新为0:

将懒标记下派之后,我们再向下查找匹配4,5区间的更新,此时我们来到右子树:

我们同样将懒标记下派,同时将懒标记清零。
这时我们继续寻找匹配区间,就来到了左子树,这是已经完全匹配4,5了。我们还是只更新最大值最小值和懒标记,不向下更新:

这就是为什么线段树的区间更新效率高,最多进行树高次,增加懒标记而不向下更新。
线段树的区间查找
我以查找3,5区间的最大值为例:
首先是3,5和0,6不匹配,所以向下查找,但是我们3,5一部分在0,3,一部分在4,6,所以兵分两路,返回两个的较大值,同时将0,6的懒标记下派给0,3和4,6:

此时我们先看左边的0,3,他的左子结点与3,5不相交,右子结点与3,5相交,因此向右子结点查找,4,6同理向左子结点查找同时懒标记也要下派:

此时先看左边,2,3的左子结点和3,5不相交,而右子结点与3,5相交,因此向右子结点查找,并且懒标记下派。
再看右边4,5是包含于3,5的,所以不用向下查找了,最终结果:

我们返回3,3和4,5之间的较大者。
因此他的时间复杂度也是树高。
线段树解法O(NlogN)
我们的具体思路刚刚都已经讲过了,这里再复述一遍:
遍历数组nums,动态维护「累计差值」和「最长平衡子数组长度」,步骤如下:
- 初始化
构建长度为n的线段树stree,初始所有位置的差值为0;
哈希表hash记录每个数字首次出现的下标(用于处理重复数字);
cur:当前遍历到i位置时的「累计差值」(奇数 + 1、偶数 - 1);
ret:记录最长平衡子数组的长度,初始为0。 - 遍历数组,动态更新差值
对每个位置i的数字numsi:
情况 1:数字首次出现
根据奇偶性计算差值v(奇数v=1,偶数v=-1);
更新全局累计差值cur += v;
用线段树对i, n-1区间执行+v操作(表示从i到数组末尾的位置,累计差值都增加v);
哈希表记录该数字的首次出现下标hashnums\[i] = i。
情况 2:数字重复出现
取出该数字上一次出现的下标j = hashnums\[i];
计算差值v(同首次出现的规则);
用线段树对j, i-1区间执行-v操作(撤销该数字在上一次出现位置到当前位置前的差值影响,保证差值计算的准确性);
更新哈希表中该数字的最新下标hashnums\[i] = i。 - 计算最长平衡子数组长度
特判核心场景:若当前累计差值cur=0,说明「从数组开头到i的整个子数组」是平衡的,长度为i+1,直接更新ret;
常规场景:通过线段树find_first找到第一个等于cur的位置j,若找到则平衡子数组长度为i-j,更新ret为最大值。 - 返回结果
遍历结束后,ret即为最长平衡子数组的长度。
这个场景下的线段树实现:
cpp
class SegmentTree{
public:
struct Node{
Node(){
lazy = 0;
min_val = 0;
max_val = 0;
}
Node& operator+=(int val){
min_val += val;
max_val += val;
lazy += val;
return *this;
}
int lazy;
int min_val;
int max_val;
};
SegmentTree(int n):tree(vector<Node>(4*n)){};
void push_up(int u){
tree[u].min_val = min(tree[u*2+1].min_val, tree[u*2+2].min_val);
tree[u].max_val = max(tree[u*2+1].max_val, tree[u*2+2].max_val);
}
void push_down(int u){
if(tree[u].lazy!=0){
tree[u*2+1] += tree[u].lazy;
tree[u*2+2] += tree[u].lazy;
tree[u].lazy = 0;
}
}
void update_interval(int u,int l,int r,int L,int R,int val){
if (L > r || R < l) return;
if(L<=l&&r<=R){
tree[u] += val;
return;
}
push_down(u);
int mid = l + (r - l)/2;
if(L<=mid){
update_interval(u*2+1, l, mid, L, R, val);
}
if(R>mid){
update_interval(u*2+2, mid+1, r, L, R, val);
}
push_up(u);
}
int find_first(int u,int l,int r,int val) {
if(val<tree[u].min_val||val>tree[u].max_val)return -1;
if(l==r)return l;
else{
push_down(u);
int mid=l+(r-l)/2;
if(val>=tree[u*2+1].min_val&&val<=tree[u*2+1].max_val){
return find_first(u*2+1,l,mid,val);
}else{
return find_first(u*2+2,mid+1,r,val);
}
}
}
private:
vector<Node> tree;
};
题目解法:
cpp
class Solution {
public:
int longestBalanced(vector<int>& nums) {
int n=nums.size();
SegmentTree stree(n);
unordered_map<int,int>hash;
int ret=0,cur=0;
for(int i=0;i<n;++i){
if(!hash.count(nums[i])){
int v=nums[i]%2?1:-1;
cur+=v;
stree.update_interval(0,0,n-1,i,n-1,v);
hash[nums[i]]=i;
}else{
int j=hash[nums[i]];
int v=nums[i]%2?1:-1;
stree.update_interval(0,0,n-1,j,i-1,-v);
hash[nums[i]]=i;
}
if(cur==0){
ret=i+1;
continue;
}
int j=stree.find_first(0,0,n-1,cur);
if(j!=-1){
ret=max(ret,i-j);
}
}
return ret;
}
};
因为我们遍历了一遍,然后进行的是logN的操作,因此时间复杂度是O(NlogN)
