树状数组套权值线段树(带修主席树)
前言:
带修主席树的本质并不是主席树,而是树状数组套权值线段树!它并没有可持久化!
所以并没有将这个知识点放到可持久化数据结构的博客里,而是单独拎出来。
正题
刚学习了用可持久化线段树解静态区间第 \(k\) 小,那么拓展一下:如果在此基础上加上单点修改,阁下又该如何应对?
题目大意:
给定一个长度为 \(n\) 的序列,需要完成一种数据结构,支持:
- 单点修改;
- 询问区间 \([l,r]\) 某个值的出现次数。
询问有区间限制,首先想到主席树。
根据主席树的思想,在每次单点修改之后,因为该版本后的所有版本都一次依照前一个版本,所以都要修改,这样就会修改 \(O(n)\) 棵树,每棵树还要修改 \(O(\log n)\) 个节点,每次修改的时间复杂度达到了 \(O(n\log n)\),直接 T 飞。
经过分析可以发现,主席树本质上类似暴力前缀和,即每棵树都为前一棵树的基础上增加一些修改,每次修改必定会使这棵树之后的所有树都受到影响。
为了更快地求前缀和,就需要用到树状数组啦。
我们在树状数组的每一个节点上创建一颗权值线段树,树状数组每个节点存权值线段树的根节点编号。
修改
对于每次修改操作,先预处理出所有涉及到的权值线段树,将原始值从他们中抹去,即 \(-1\)。
然后在原 \(a\) 数组中也将值修改。
最后将新值在涉及的权值线段树中加入。
查询
对于每次查询操作,先预处理出查询“前缀和”时涉及到的权值线段树,再进行查询。
在查询时要注意,因为每次都会涉及到 \(O(\log n)\) 棵权值线段树进行计算,所以每次向下计算时都要将这些权值线段树一起跳到它们的左/右子节点。
最后就是要注意离散化。
时间复杂度 \(O(n\log^2 n)\),空间复杂度 \(O(n\log n)\)。
\(\texttt{Code}\):
#include <vector>
#include <iostream>
#include <algorithm>
#define lowbit(x) x & -x
using namespace std;
const int N = 200010;
int n, m;
int a[N];
struct Node{
char op;
int x, y, k;
}que[N];
struct node{
int ls, rs;
int val;
}tr[N << 6];
int root[N], idx;
vector<int> nums;
int tmp1[N], tmp2[N];
int tt1, tt2;
int find(int x) {
return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}
void modify(int &p, int l, int r, int pos, int v) { //权值线段树单点修改模板
if(!p) p = ++idx;
tr[p].val += v;
if(l == r) return ;
int mid = l + r >> 1;
if(pos <= mid) modify(tr[p].ls, l, mid, pos, v);
else modify(tr[p].rs, mid + 1, r, pos, v);
}
void pre_change(int id, int v) {
int k = find(a[id]);
for(int i = id; i <= n; i += lowbit(i))
modify(root[i], 1, nums.size(), k, v); //通过树状数组的单点修改修改 logn 棵权值线段树
}
int query(int l, int r, int k) {
if(l == r) return l;
int mid = l + r >> 1;
int res = 0;
//将 logn 棵权值线段树的左区间对应相减,统计出左区间中数的总数
for(int i = 1; i <= tt2; i++) res += tr[tr[tmp2[i]].ls].val;
for(int i = 1; i <= tt1; i++) res -= tr[tr[tmp1[i]].ls].val;
if(res >= k) { //如果总数 >= k,则答案在左区间内
for(int i = 1; i <= tt1; i++) tmp1[i] = tr[tmp1[i]].ls;
for(int i = 1; i <= tt2; i++) tmp2[i] = tr[tmp2[i]].ls;
return query(l, mid, k);
}
else { //否则去右区间内查找答案
for(int i = 1; i <= tt1; i++) tmp1[i] = tr[tmp1[i]].rs;
for(int i = 1; i <= tt2; i++) tmp2[i] = tr[tmp2[i]].rs;
return query(mid + 1, r, k - res);
}
}
int pre_query(int l ,int r, int k) { //预处理出查询“前缀和”时涉及到的权值线段树
tt1 = tt2 = 0;
for(int i = l - 1; i >= 1; i -= lowbit(i)) tmp1[++tt1] = root[i];
for(int i = r; i >= 1; i -= lowbit(i)) tmp2[++tt2] = root[i];
return query(1, nums.size(), k);
}
int main() {
scanf("%d%d", &n, &m);
nums.push_back(-0x3f3f3f3f); //离散化后下标从 1 开始,所以插入一个哨兵
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
nums.push_back(a[i]);
}
for (int i = 1; i <= m; ++i) {
char ch = getchar();
while (ch != 'Q' && ch != 'C')
ch = getchar();
que[i].op = ch;
if (ch == 'Q')
scanf("%d%d%d", &que[i].x, &que[i].y, &que[i].k);
else {
scanf("%d%d", &que[i].x, &que[i].y);
nums.push_back(que[i].y);
}
}
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
for(int i = 1; i <= n; i++) pre_change(i, 1);
for(int i = 1; i <= m; i++) {
if(que[i].op == 'Q') printf("%d\n", nums[pre_query(que[i].x, que[i].y, que[i].k)]);
else {
pre_change(que[i].x, -1); //先把修改位置上的值从涉及的权值线段树中抹去
a[que[i].x] = que[i].y;
pre_change(que[i].x, 1); //再把修改后的值加到涉及的权值线段树中去
}
}
return 0;
}
\(\texttt{update on 2024.7.14:}\) 用莫队不香吗?