浅谈 Wavelet Matrix
__staring__ · · 算法·理论
前几天看到了这个数据结构,觉得比较优雅,记录一下。
由于 wavelet matrix 貌似没有正式的译名,这里将其称为小波矩阵。
总的来说,小波矩阵是一种基于值域二进制分解的静态简洁数据结构,其基于小波树 wavelet tree。
关于简洁数据结构 Succinct data structure,简洁位向量 Succinct Indexable Dictionaries,以及其支持的
在竞赛中,小波矩阵能以
一般认为(word-RAM model)计算机字长
约定
下文叙述和代码实现上,对序列和值域位均使用 0-index。
默认序列长度为
小波树是一棵
节点
具体的递归定义如下:将序列
若子节点高度
注意小波树并不存储
不难发现小波树的形态与 01-trie,或者说动态开点线段树非常相似,因此其也支持相关操作。例如查询序列
\opt{id}_c(u,k)
若
b_{u,k}=c ,求对应的元素在a'_{u_c} 中的下标。
不难发现就是
\opt{access}(a,k)
求
a_k 的值。
从根节点开始,维护
一路向下查找即可得到
\opt{rank}_v(a,k)
求
a[0,k] 中v 的个数。
同样从根节点开始,记
递归到叶节点时答案为
\opt{select}_v(a,k)
求
a 中第k 个v 的下标。
考虑
于是
若
小波矩阵
小波矩阵的主要思想是,将小波树每一层的所有位向量,按照一定顺序组成一个长为
\opt{build}
记当前要构建序列
初始有
可以发现这等价于,对于小波树每一层的位向量,所有
注意,小波树的叶层节点是有序的,但小波矩阵最终的
朴素实现的时空复杂度为
void build(int n, int a[])
{
for(int u = A - 1; ~u; u --)
{
for(int i = 1; i <= n; i ++)
b[u][i] = a[i] >> u & 1;
stable_partition(a + 1, a + n + 1,
[&u](int x){return ~x >> u & 1;});
}
}
\opt{id}_c(a'_u,k)
若
a'_{u,k} 的第u 位为c ,求其在a'_{u-1} 的下标。
-
若
c=0 ,那么只有a'_u[0,k-1] 中第u 位为 0 的元素在a'_{u,k} 之前,即\opt{id}_0(a'_u,k)=\opt{rank}_0(b_u,k-1) 。 -
若
c=1 ,那么a'_u 中第u 位为 0 的所有元素,和a'_u[0,k-1] 中第u 位为 1 的元素都在a'_{u,k} 之前,即\opt{id}_1(a'_u,k)=\opt{rank}_0(b_u,n-1)+\opt{rank}_1(b_u,k-1) 。
为了方便描述
\opt{id}^{-1}_c(a'_u,k)
若
a'_{u-1,k} 的第u 位为c ,求其在a'_u 的下标(可以认为是\opt{id} 的逆运算)。
-
若
c=0 ,那么其为b_u 的第k+1 个 0,即\opt{id}^{-1}_0(a'_u,k)=\opt{select}_0(b_u,k+1) 。 -
若
c=1 ,那么其为b_u 的第k-\opt{rank}_0(b_u,n-1)+1 个 1,即\opt{id}^{-1}_1(a'_u,k)=\opt{select}_1(b_u,k-\opt{rank}_0(b_u,n-1)+1) 。
确定如何转移之后,查询的思路与小波树类似。
\opt{access}(a,k)
求
a_k 的值。
初始
\opt{rank}_v(a,l,r)
求
a[l,r] 中v 的个数。
若沿用小波树的思路查询
若
初始
\opt{select}_v(a,l,k)
求
a[l,n-1] 中第k 个v 的下标。
同样地,查询
若
初始
若
优化
在上述基础操作中,维护
或许有读者发现,这正好是简洁位向量 Succinct Indexable Dictionaries 支持的操作。然而竞赛中
我们直接使用
对于
对于 __builtin_popcountll,单次复杂度
对于
于是,我们得到了预处理时间复杂度
struct bitSet
{
using ULL = unsigned long long;
#define ppcll __builtin_popcountll
int len, zero;
vector <pair <ULL, int>> bit;
bitSet(int n): len(n >> 6), zero(n)
{
bit.resize(len + 1, make_pair(0, 0));
}
void set(int k)
{
--zero;
bit[k >> 6].first |= 1ull << (k & 63);
}
void init()
{
for(int i = 1; i <= len; i ++)
bit[i].second = bit[i - 1].second + ppcll(bit[i - 1].first);
}
int access(int k)
{
return bit[k >> 6].first >> (k & 63) & 1;
}
int rank1(int k)
{
return bit[k >> 6].second + ppcll(bit[k >> 6].first << (~k & 63));
}
int rank0(int k)
{
return k - rank1(k);
}
int binarySearch(int v, int k)
{
static constexpr ULL m32 = 0xffffffff;
static constexpr ULL m16 = 0xffff;
static constexpr ULL m8 = 0xff;
static constexpr ULL m4 = 0xf;
static constexpr ULL m2 = 0x3;
static constexpr ULL m1 = 0x1;
int c, pos = 0;
if(c = ppcll(v & m32); c < k) v >>= 32, k -= c, pos |= 32;
if(c = ppcll(v & m16); c < k) v >>= 16, k -= c, pos |= 16;
if(c = ppcll(v & m8 ); c < k) v >>= 8, k -= c, pos |= 8;
if(c = ppcll(v & m4 ); c < k) v >>= 4, k -= c, pos |= 4;
if(c = ppcll(v & m2 ); c < k) v >>= 2, k -= c, pos |= 2;
if(c = ppcll(v & m1 ); c < k) v >>= 1, k -= c, pos |= 1;
return pos;
}
int select1(int k)
{
int pos = 0;
int l = 0, r = len, mid;
while(l <= r)
{
mid = (l + r) >> 1;
if(bit[mid].second < k)
l = mid + 1, pos = mid;
else r = mid - 1;
}
k -= bit[pos].second;
return pos << 6 | binarySearch(bit[pos].first, k);
}
int select0(int k)
{
int pos = 0;
int l = 0, r = len, mid;
while(l <= r)
{
mid = (l + r) >> 1;
if((mid << 6) - bit[mid].second < k)
l = mid + 1, pos = mid;
else r = mid - 1;
}
k -= (pos << 6) - bit[pos].second;
return pos << 6 | binarySearch(~bit[pos].first, k);
}
#undef ppcll
};
\opt{kth-min}(a,l,r,k)
求
a[l,r] 中第k 小的元素。
由小波树可以知道,
-
若
a'_u[l,r] 内第u 位为 0 的元素个数\ge k ,即\opt{rank}_0(b_u,l,r) \ge k ,显然v 这一位为 0,更新l \larr \opt{id}_0(a'_u,l-1)+1, r \larr \opt{id}_0(a'_u,r), u \larr u-1 。 -
否则
v 这一位为 1,去掉这一位为 0 元素的贡献,更新v \larr v+2^u, l \larr \opt{id}_1(a'_u,l-1)+1, r \larr \opt{id}_1(a'_u,r), k \larr k-\opt{rank}_0(b_u,l,r), u \larr u-1 。
\opt{ranking}(a,l,r,v)
求
a[l,r] 中<v 的元素个数。
类似地,从
-
若
v 第u 位为 1,将所有这一位为 0 的元素加入统计,更新k \larr k+ \opt{rank}_0(a'_u,l,r), l \larr \opt{id}_1(a'_u,l-1)+1, r \larr \opt{id}_1(a'_u,r), u \larr u-1 。 -
否则直接更新
l \larr \opt{id}_0(a'_u,l-1)+1, r \larr \opt{id}_0(a'_u,r), u \larr u-1 。
实现
通过上面的一些操作可以发现,小波矩阵继承了小波树带来的 01-trie 相似结构,且能通过简洁位向量进行区间转移。相当于提取区间的 01-trie 进行查询,因此大部分持久化 01-trie 或者说主席树支持的操作,在小波矩阵上稍作修改即可实现。
位向量的统一存储和简洁位向量的线性空间对缓存较为友好,这使得小波矩阵可以以小常数或小空间替代主席树。当然,若是统计信息带权,空间复杂度只能做到
这里给出笔者的实例代码,供读者参考理解。一些建议:
-
朴素实现中的
stable_partition()虽然可以做到O(n) ,但在没有内部临时缓冲区时,是O(n\log{n}) 的复杂度,因此一般应该替换为手写版本。 -
有时初始化中的
tmp0可以不使用,直接在原数组上修改,最后原数组即保存小波矩阵叶层位向量的原始值。 -
所有涉及左端点
l 的操作,都可以将左闭换成左开,来省掉对l 的偏移。 -
内层
bitset和外层wavelet matrix都使用 0-index,但在查询时使用 1-index,因为内层查[0,k) 前缀和比[0,k] 方便很多,查[l,r] 时放到外层偏移掉l 再换成右开,变为[l-1,r] 。
struct waveletMatrix
{
int n, m;
vector <bitSet> bit;
waveletMatrix(int _n, int _m, int a[]): n(_n), m(_m)
{
bit.resize(m, bitSet(n));
vector <int> tmp0(a, a + n + 1), tmp1(n + 1);
for(int u = m - 1; ~u; u --)
{
int x = 0, y = 0;
for(int i = 1; i <= n; i ++)
if(tmp0[i] >> u & 1)
tmp1[++y] = tmp0[i], bit[u].set(i);
else tmp0[++x] = tmp0[i];
bit[u].init();
for(int i = 1; i <= y; i ++)
tmp0[x + i] = tmp1[i];
}
}
int id(int u, int k, int c)
{
return !c ? bit[u].rank0(k) : bit[u].zero + bit[u].rank1(k);
}
int di(int u, int k, int c)
{
return !c ? bit[u].select0(k) : bit[u].select1(k - bit[u].zero);
}
int access(int k)
{
int res = 0;
for(int u = m - 1; ~u; u --)
{
int c = bit[u].access(k);
res |= c << u;
k = id(u, k, c);
}
return res;
}
int rank(int l, int r, int v)
{
for(int u = m - 1; ~u; u --)
{
l = id(u, l - 1, v >> u & 1) + 1;
r = id(u, r, v >> u & 1);
}
return r - l + 1;
}
int select(int l, int k, int v)
{
for(int u = m - 1; ~u; u --)
l = id(u, l - 1, v >> u & 1) + 1;
l += k - 1;
for(int u = 0; u < m; u ++)
l = di(u, l, v >> u & 1);
return l;
}
int kth_min(int l, int r, int k)
{
int res = 0;
for(int u = m - 1; ~u; u --)
{
int c = bit[u].rank0(r) - bit[u].rank0(l - 1);
l = id(u, l - 1, c < k) + 1;
r = id(u, r, c < k);
if(c < k)
{
res |= 1 << u;
k -= c;
}
}
return res;
}
int ranking(int l, int r, int v)
{
int res = 0;
for(int u = m - 1; ~u; u --)
{
int c = v >> u & 1;
if(c) res += bit[u].rank0(r) - bit[u].rank0(l - 1);
l = id(u, l - 1, c) + 1;
r = id(u, r, c);
}
return res;
}
};
以上给出了一份标准化实现代码,比较繁杂且有很多不必要内容。以下则是一份简略版实现。
struct bits
{
int n;
vector <pair <ULL, int>> bit;
#define ppc __builtin_popcountll
bits() {}
bits(vector <ULL> arr)
{
n = (arr.size() >> 6) + 1, bit.resize(n);
for (int i : viota(0, ssize(arr)))
bit[i >> 6].first |= arr[i] << (i & 63);
for (int i : viota(1, n))
bit[i].second = bit[i - 1].second + ppc(bit[i - 1].first);
}
int ask(int k) {return bit[k >> 6].second +
ppc(bit[k >> 6].first & ((1ull << (k & 63)) - 1));}
#undef ppc
};
struct wave
{
int n;
array <bits, 30> bit;
array <int, 30> pos;
wave(vector <int> arr)
{
n = arr.size();
for (int u : viota(0, 30) | vreve)
{
bit[u] = bits (arr | vtran([&] (auto x)
{return x >> u & 1ull;}) | ranges::to <vector> ());
pos[u] = n - ranges::stable_partition
(arr, [&] (int x) {return ~x >> u & 1;}).size();
}
}
int kthmin(int l, int r, int k)
{
int v = 0; --l;
for (int u : viota(0, 30) | vreve)
{
int ll = bit[u].ask(l), rr = bit[u].ask(r);
if (int c = r - rr - l + ll; c >= k) l -= ll, r -= rr;
else v |= 1 << u, k -= c, l = pos[u] + ll, r = pos[u] + rr;
}
return v;
}
int rankin(int l, int r, int v)
{
int k = 0; --l;
for (int u : viota(0, 30) | vreve)
{
int ll = bit[u].ask(l), rr = bit[u].ask(r);
if (int c = r - rr - l + ll; ~v >> u & 1) l -= ll, r -= rr;
else k += c, l = pos[u] + ll, r = pos[u] + rr;
}
return k;
}
};
动态
小波矩阵是静态的数据结构,其维护的信息一般满足可减性或者说有逆元,需要插入删除时可以考虑维护一组负贡献的小波矩阵,可考虑几种经典实现:
-
定期重构,代价为单次修改均摊
O(\sqrt{n\log{V}})\sim O(\sqrt{n}\log{V}) ,支持插入删除。 -
二进制分组,代价为单次修改均摊
O(\log{V}\log{n}) ,支持插入和特殊删除:若删除不可减且为撤销,考虑斜二进制分组,当存在三组相同大小的组时合并其中两组,删除时仍然重构,否则不支持删除。 -
平衡树维护内层位向量,代价为单次修改
O(\log{V}\log{n}) ,支持插入删除。
例题
区间 kth-min
P3834 【模板】可持久化线段树 2
区间 kth-min 模板题,直接使用上面的模板即可。
在线写法直接对原数组修改,无需记录二分过程,返回最后端点即可。
离线则是考虑对每一位,把所有询问的移动统一处理,把小波矩阵做到
在线2 和 主席树2 指的是,以题目的最大值域
在线 248ms 4.21mb
在线2 367ms 4.95mb
离线 206ms 5.78mb
主席树 399ms 42.46mb
主席树2 574ms 76.53mb
区间 ranking
P3810 【模板】三维偏序
详细题解,使用区间 ranking 配合二进制分组即可。
小波矩阵 980ms 5.32mb
cdq 263ms 6.60mb
主席树(内存池垃圾回收) 1.62s 37.40mb
主席树(vec 动态开点) 1.05s 23.36mb
区间 kth-min,ranking
P14513 [NFLSPC #8] 追忆desuwa
详细题解,拆点后先做一遍 kth-min 再做一遍 ranking。
二分带权信息
P12866 [JOI Open 2025] 抽奖 / Lottery
详细题解,这里讲述一下实现。
要解决的问题为:给定长为
考虑类似二分答案的思想,维护可能的答案区间
记当前在第
若不离散化则以
小波矩阵 2.51s 141.86mb 提交时是最优解 rk3,没打过 rk1 rk2 老哥的主席树。
主席树 2.46s 199.91mb 什么叫我的主席树跑到最优解了?
动态区间 kth-min,ranking
P2617 Dynamic Rankings
P3380 【模板】树套树
P4278 带插入区间K小值
详细题解,把内层的 bitset 换成平衡树,看上去能做一车动态问题。
矩形 kth-min
P1527 [国家集训队] 矩阵乘法
详细题解,树套小波矩阵,不彳亍,小波矩阵套小波矩阵,彳亍。
动态矩形 kth-min
P4848 崂山白花蛇草水
详细题解,二进制分组合并常数太大了,根本跑不过带根号做法(悲。打不过就以后加入。
to be continued...
参考资料
小波矩阵——ShwStone
算法学习笔记:Wavelet Tree 求解区间第K小的杀器(一)——OnjoujiToki