树状数组维护区间最大值
树状数组维护区间最大值
1.关于树状数组
树状数组是一种支持单点修改和区间查询的数据结构。普通树状数组维护的信息及运算要满足结合律且可差分,如加法(和)、乘法(积)、异或等。 ——OI WIKI
顾名思义,树状数组,即数组中的每个元素都可以逻辑上对应为树上的一个节点,一棵子树的根节点保存整棵子树的信息,从而通过查询根节点而不是遍历一棵子树的方式减少运行时间。
具体而言,数组中下标为x的元素保存了下标\([x-lowbit(x)+1, x]\)区间内的信息。
以维护部分和为例,下面是一张OI WIKI的示意图:
可以看出:
1.下标为x的节点的父节点下标为\(x+lowbit(x)\)
2.下标为x的节点的子节点下标为\(x-2^0, x-2^1, ... x-lowbit(x)/2\)
若查询前x个元素的前缀和,已知c[x]保存了下标\([x-lowbit(x)+1, x]\)的区间和,如果\(x-lowbit(x) = 0\),则c[x]为前x个元素的前缀和;否则只需要接着查询前\(x-lowbit(x)\)元素的和,再加上c[x]即可。如此递归调用,可以证明时间复杂度为\(O(logN)\)。
其他操作和应用详见这篇博客。
以上文的思想,若想用树状数组维护区间最大值,可以尝试维护数组tr,使tr下标为x的元素保存了数组\(a[x-lowbit(x)+1, x]\)区间内的最大值。
2.建树
与维护部分和相同,每次用子节点更新父节点即可。时间复杂度为\(O(N)\)。
void build()
{
for(int i=1;i<=n;i++)
{
tr[i] = max(tr[i], a[i]);
int j = i + lowbit(i);
if (j <= n) tr[j] = max(tr[j], tr[i]);
}
}
3.单点修改
首先用d修改a[x]和tr[x],然后用x的子节点更新tr[x],接着用tr[x]更新至x的父节点。如此递归直到超出数组范围。
时间复杂度为\(O(logN)\)。
// 把a[x]修改为d
void upd(int x, int d)
{
tr[x] = a[x] = d;
for (int i=1; i<lowbit(x); i<<=1)
tr[x] = max(tr[x], tr[x-i]);
for(int i=x;i+lowbit(i)<=n;i+=lowbit(i))
{
int j = lowbit(i);
tr[i+j] = max(tr[i+j], tr[i]);
}
}
4.区间查询
首先,最大值这个性质是不可差分的,即不能通过\([1, r]\), 和\([1, l]\)的最大值得出\([l,r]\)的最大值。
所以,我们只能在\(x-lowbit(x)+1 >= l\)时参考tr[x]。否则,若\(x=l\)可直接更新;若\(x>l\)需要用\([l,x-1]\)来更新。
可以证明:减一操作做最多做\(logN\)次,而每次减一都要伴随重新而来的\(logN\)次减\(lowbit\)操作,故时间复杂度为遗憾的\(O(logN)^2\)。
int get(int l, int r)
{
int res = -INF;
while (l <= r)
{
for (; l <= r && r-lowbit(r)+1 >= l; r -= lowbit(r))
res = max(tr[r], res);
if(l > r) break;
res = max(a[r], res);
r --;
}
return res;
}
或者先减一,保证\(l<=r\)再用a[r]来更新。
int get(int l, int r)
{
int ans = -INF;
while (l <= r)
{
ans = max(a[r], ans);
r --;
// 注意判断条件为r-lowbit(r) >= l而不是r-lowbit(r)+1 >= l
// 是因为要保证下一轮的r-lowbit(r) ==0时退出
// 否则若l==1会死循环
for (; r-lowbit(r) >= l; r -= lowbit(r))
ans = max(tr[r], ans);
}
return ans;
}
虽然,指定区间查询的复杂度为\(O(logN)^2\),但假如是从数组第一个个元素开始查询,依然可以\(O(logN)\)内完成。代码如下:
int query(int x)//返回val[1]~val[x]中的最大值
{
int res=-INF;
for(; x; x-=x&(-x))
res=max(res,T[x]);
return res;
}