树状数组套权值线段树(带修主席树)

前言:

带修主席树的本质并不是主席树,而是树状数组套权值线段树!它并没有可持久化!

所以并没有将这个知识点放到可持久化数据结构的博客里,而是单独拎出来。

正题

刚学习了用可持久化线段树解静态区间第 \(k\) 小,那么拓展一下:如果在此基础上加上单点修改,阁下又该如何应对?

P2464 [SDOI2008] 郁闷的小 J

题目大意:

给定一个长度为 \(n\) 的序列,需要完成一种数据结构,支持:

  1. 单点修改;
  2. 询问区间 \([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:}\) 用莫队不香吗?

posted @ 2024-07-23 14:37  Brilliant11001  阅读(7)  评论(0编辑  收藏  举报