从主席树到小波矩阵
摘要
区间第
不过如果经常看 Codeforces、ICPC 选手的代码库,或者翻一些国外资料,会遇到另一个名字:小波树,或者更常写成数组形式的小波矩阵。它处理的是同一类静态区间值域统计问题,能在
小波矩阵本质上就是:把序列按二进制位一层一层稳定划分,同时记录每层前缀里有多少个
引入
考虑一个静态数组
如果只看第一个问题,很容易想到主席树。每个前缀建一棵权值线段树,查询
这个思路很顺,但代码里要维护 rt、ls、rs、sum,还要离散化,节点数组也要估好大小。平时写熟了还好,一旦题目本身有别的细节,主席树的代码量就有点压人。
小波矩阵换了一个角度。它不显式建很多棵权值线段树,而是直接把原序列按二进制位分层重排。每一层只做一件事:当前位是
这个稳定划分,就是整个结构的核心。
从主席树的判断说起
主席树求区间第
如果这个数量不少于
小波矩阵做的判断很像。只不过这里的“左半边”和“右半边”变成了当前二进制位的
从最高位往最低位看。当前位为
和主席树相比,这里没有节点,也没有版本。每一层只需要知道:某个前缀里有多少个
设第
当前位为
这一层划分后,所有
走
走
刚开始看这两个式子会有点不舒服,建议按含义记。
去
只要每层都是稳定划分,原来区间里的元素到了下一层后仍然是一段连续区间,所以查询可以一直往下做。
建立小波矩阵
假设所有数满足:
从第
- 扫描当前序列,记录前缀
1 的个数; - 把当前位为
0 的数按原顺序放入临时数组; - 再把当前位为
1 的数按原顺序接到后面; - 记下这一层
0 的总数mid_d ; - 用重排后的数组进入下一层。
这里有一个容易忽略的点:一定要稳定划分。不能随便排序,也不能只按这一位粗暴打乱。因为后面的查询依赖“区间映射后仍然连续”这个性质。
查询第 k 小
现在考虑 kth(l,r,k),其中
在第
说明答案当前位是
否则答案当前位是
同时给答案加上:
然后进入
这样从高位走到低位,答案的每一位都被确定了。这个过程和主席树求第
查询小于 x 的个数
再看 rank_less(l,r,x),也就是查询区间
仍然从高位到低位考虑。
如果
如果
最后得到:
于是区间内值落在
值
和主席树的对比
两者复杂度其实很接近。
| 数据结构 | 预处理 | 单次查询 | 空间 | 更顺手的地方 |
|---|---|---|---|---|
| 主席树 | 国内题常见,扩展到版本思路自然 | |||
| 小波矩阵 | 接口统一,不用动态开点 |
小波矩阵比较舒服的地方是,很多操作都能落到 kth 和 rank_less 上。它不像主席树那样需要一直想着“两个版本相减”,也不用担心节点开少了。
当然,它也有明显限制。它最自然的形态是静态序列。如果题目有单点修改、区间修改,那就不适合直接套这个结构了。还有就是它的下标映射一开始很容易写错,第一次写建议拿小数组手推一遍。
所以它不是用来替代主席树的。更准确地说,它是另一个处理静态范围秩问题的模板。主席树会写,小波矩阵也会写,遇到题时选择就会多一点。
代码
下面给一份普通 OI 风格的实现。默认 LOG 改大。
kth(l,r,k):返回[l,r] 的第k 小,k 从1 开始;rank_less(l,r,x):返回[l,r] 中小于x 的数的个数;count_x(l,r,x):返回[l,r] 中等于x 的数的个数。
#include<bits/stdc++.h>
using namespace std;
const int N=200005;
const int LOG=30;
const int LIM=1<<30;
int n,q;
int a[N],cur[N],tmp[N];
int sum[LOG][N],mid[LOG];
inline void build(){
for(int i=1;i<=n;i++) cur[i]=a[i];
for(int d=LOG-1;d>=0;d--){
int c0=0,c1=0;
sum[d][0]=0;
for(int i=1;i<=n;i++){
int bit=(cur[i]>>d)&1;
sum[d][i]=sum[d][i-1]+bit;
if(!bit) tmp[++c0]=cur[i];
}
mid[d]=c0,c1=c0;
for(int i=1;i<=n;i++) if((cur[i]>>d)&1) tmp[++c1]=cur[i];
for(int i=1;i<=n;i++) cur[i]=tmp[i];
}
}
inline int kth(int l,int r,int k){
int ret=0;
for(int d=LOG-1;d>=0;d--){
int one=sum[d][r]-sum[d][l-1];
int zero=r-l+1-one;
if(k<=zero){
l=l-sum[d][l-1];
r=r-sum[d][r];
}else{
ret|=1<<d,k-=zero;
l=mid[d]+sum[d][l-1]+1;
r=mid[d]+sum[d][r];
}
}
return ret;
}
inline int rank_less(int l,int r,int x){
if(x<=0) return 0;
if(x>=LIM) return r-l+1;
int ret=0;
for(int d=LOG-1;d>=0;d--){
int one=sum[d][r]-sum[d][l-1];
int zero=r-l+1-one;
if((x>>d)&1){
ret+=zero;
l=mid[d]+sum[d][l-1]+1;
r=mid[d]+sum[d][r];
}else{
l=l-sum[d][l-1];
r=r-sum[d][r];
}
if(l>r) break;
}
return ret;
}
inline int count_x(int l,int r,int x){
return rank_less(l,r,x+1)-rank_less(l,r,x);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++) cin>>a[i];
build();
while(q--){
int op,l,r,x;
cin>>op>>l>>r>>x;
if(op==1) cout<<kth(l,r,x)<<'\n';
else if(op==2) cout<<rank_less(l,r,x)<<'\n';
else cout<<count_x(l,r,x)<<'\n';
}
return 0;
}
正确性说明
建结构时,每一层都按照当前二进制位做稳定划分。因此,一个区间里所有走向同一分支的元素,在下一层仍然是一段连续区间。查询时维护的
求第
求小于
count_x 由小于
复杂度分析
设值域二进制位数为
建结构时每一层扫一遍数组,时间复杂度为:
每次查询只走
空间主要是每层的前缀和数组,复杂度为:
如果取
适用场景
小波矩阵适合序列静态、询问很多、询问内容和区间值域排名有关的题。比较常见的关键词有:
- 区间第
k 小; - 区间内小于
x 的个数; - 区间内值在
[L,R] 的个数; - 区间内某个值的出现次数;
- 一些前驱、后继、分位数的变形。
如果题目一看就是动态修改,那别硬套。小波矩阵最强的地方是静态查询。把它放进工具箱,是为了遇到范围秩问题时多一种写法,而不是见到主席树就全部换掉。
参考资料
- USACO Guide:Wavelet Tree
- Codeforces:Wavelet tree problems
- Codeforces:Wavelet Tree Implementation(Array Based)
- arXiv:Range Quantile Queries: Another Virtue of Wavelet Trees