道长的算法笔记:区间查询问题

(一) 关于二进制

 大多算法都跟二进制有关,位运算、二分、倍增的思想贯穿于各种数据结构的设计。任意正整数 \(x\) 均可表示为二进制 \(a_{k+1}...a_{3}a_{2}a_{1}a_{0}\),其中的 \(\{a_{i_1},a_{i_2},a_{i_3},...,a_{i_m}\}\) 位置等于 \(1\),那么整数能被二进制分解变为:

\[x = 2^{i_{1}}+2^{i_{2}}+2^{i_{3}}+...+2^{i_{m}} \]

 不妨假设 \(i_{1}>i_{2}>i_{3}>...>i_{m}\),那么,区间 \([1,x]\) 能被分解变为若干小区间,区间个数:\(\log x\),区间长度分别等于\(2^{i_{1}},2^{i_{2}},2^{i_{3}}...,2^{i_{m}}\),这些区间的共同特点是以 \(\xi(\xi\leq x)\) 结尾时,则其长度等于 \(lowbit(\xi)\),例如 \(7=2^2+2^1+2^0\),区间 \([1,7]\) 能被分成 \([1,4]\)\([5,6]\)\([7,7]\),三个长度分别 \(\text{lowbit(4)=4}\)\(\text{lowbit(6)=2}\)\(\text{lowbit(7)=1}\),给定一个整数 \(x\),通过下面的代码能把整数区间 \([1,x]\) 划分:

while(x){
    printf("%d %d\n", x - lotbit(x) + 1, x);
    x -= lowbits(x);
}

(二) 区间查询与修改的需求

 对于数列 \(v\),我们可能会对区间 \([a,b]\) 批量的修改、查询。查询区间 \([a,b]\) 平均复杂度\(O(n)\),假如查询条数 \(m\),复杂度则为 \(O(mn)\),如果数据无需频繁修改,使用前缀和即可把查询的复杂度变为 \(O(1)\),但对区间的批量修改又达到了 \(O(n)\),如果使用差分数组修改的复杂度变成了\(O(1)\),但是查询又变成了 \(O(n)\)。自然而然的,我们希望找到一种折中的方案,使得更新与修改的复杂度都低于 \(O(n)\),树状数组与线段树的查询与修改都是 \(O(\log n)\) 级别的,符合我们的要求。

(三) 树状数组

 树状数组的作用是维护一个数列的前缀和,树状数组 \(t\)能被看作一个树形结构,其满足下列几项性质:

  • 每个内部节点 \(t[x]\) 保存以它为根的子树的所有叶节点和
  • 每个内部节点 \(t[x]\) 子节点个数等于\(\text{lowbit(x)}\)位数
  • 每个内部节点,除了树根以外都有父节点 \(t[x+\text{lowbit(x)}]\)
  • 树深度 \(O(\log n)\)
  • 如果数列的长度不等于 \(2\) 整次幂,也即 \(n\neq 2^k\),树状数组则为具有同上性质的森林。

(3.1) 树状数组基本操作

 树状数组的两项基本操作是按索引修改某个点位,或查询某段区间,详见下列代码

LLong v[MAXN], t[MAXN];

inline int lowbit(int x){
   return (x & (-x));
}

// 更新数列v[x],以及其所有父节点维护的前缀和,相当于一个向上爬树的过程
inline void update(LLong tree[], int x, LLong c){
   for (int i = x; i <= n; i += lowbit(i)){
       tree[i] += c;
   }
}

// 查询索引[1,x]前缀和,相当于一个向下爬树的过程
inline LLong query(LLong tree[], int x){
   LLong ans = 0;
   for (int i = x; i; i -= lowbit(i)){
       ans += tree[i];
   }
   return ans;
}

(3.2) 树状数组维护区间

 树状数组其实亦可用于区间维护,区间的批量加减可以使用差分数组 \(d\) 维护,对于区间 \([L,R]\) 每个元素都增加 \(c\),只要修改两侧端点 \(d[L]+c\), \(d[R+1]-c\) 即可,当求数列 \(v\) 前缀和的时候,会有:

\[\begin{align*} \sum_{i=1}^{x}v[i] &= \sum_{i=1}^{x}\sum_{j=1}^{i}d[j]\\&= \sum_{i=1}^{x}d[i](x-i+1)\\&= \sum_{i=1}^{x}(x+1)d[i] -\sum_{i=1}^{x} id[i] \end{align*} \]

 因而只要维护两个 \({d[i]}\)\(i \times d[i]\) 两个数组的前缀和即可实现树状数组的区间修改与查询,类似的树状数组亦可解决RMQ问题。

(四) 线段树

 线段树是一种常用结构,通过基于分治思想,利用额外的空间后序遍历实现了的区间查询的复杂度的优化。

(4.1) 线段树榫卯式写法

Waiting...

(4.2) 线段树覆盖式写法

Waiting...

posted @ 2022-07-18 13:21  道长陈牧宇  阅读(94)  评论(0)    收藏  举报