传统方法的局限
在算法竞赛中,树状数组(Fenwick Tree)因其简洁高效的特性被广泛使用。但传统实现需要预先确定数据范围并预分配空间,当值域达到1e18量级时,离散化预处理会带来诸多不便:需要离线处理、增加编码复杂度。
动态空间的黑魔法
我们可以利用unordered_map的无序容器特性配合树状数组的跳跃特性,实现动态内存分配的树状数组,创建出一个1e18甚至更大的树状数组,可以省去离散化的步骤,同时无需离线
#define LL long long
unordered_map<LL, LL> tree;
const LL N = 1e18 + 10;
LL lowbit(LL x) {return x & -x;}
void add(LL x, LL v) {
while(x <= N) {
tree[x] += v;
x += lowbit(x);
}
}
LL sum(LL x) {
LL sum = 0;
while(x) {
sum += tree[x];
x -= lowbit(x);
}
return sum;
}
实现原理分析
- 空间动态分配:unordered_map仅在访问时创建键值对,实际空间复杂度约为O(q logU),其中q是操作次数,U是值域大小
- 时间效率折衷:哈希表访问时间复杂度为均摊O(1),但实际常数比普通数组大,在ICPC比赛中请分析好时间再谨慎使用。
优势与代价
✅ 优势:
- 对于超大值域无需预处理,支持在线操作
- 节省内存空间(稀疏数据场景)
⚠️ 注意事项:
- 时间复杂度常数较大
- 哈希冲突导致时间复杂度最坏至O(n)(可参考自定义哈希篇)
实战
下面是使用超大树状数组做的题
1D Country - AtCoder abc371\_d - Virtual Judge
使用建议
推荐在以下场景使用该技巧:
- 值域极大但操作次数有限(q ≤ 1e5)
- 需要快速实现原型的情况
- 内存资源紧张的环境