数据结构(可持久化线段树)

一、可持久化的数据结构

如果想知道数据集在任意时刻的历史状态(即能保存每次改变前的状态和改变后的状态),那么就需要使用可持久化的数据结构

如果在每次操作后都直接拷贝所有数据,那么时空复杂度过大,但是我们会发现,每次进行修改(如线段树的单点修改)时,只会影响一部分节点(如一条链)的值,所以我们可以只创建发生改变的部分的副本,不拷贝其他部分

二、可持久化线段树

1、可持久化线段树:又称函数式线段树(意味着我们可以像函数一样访问每个n个版本的线段树),在每次单点更新时,都会产生O(logn)个新节点,也就是一条链,我们需要额外保存的就是这条链

因为可持久化线段树不再是一颗完全二叉树,所以我们不再能向普通线段树一样按照层次序编号,而是改为记录每个节点左、右子节点的编号

为了节省空间,我们不再记录每个节点代表的区间[l,r],而是作为递归参数传递

可持久化线段树写起来类似普通线段树,只需要更改一下左右子树的访问方式

int build(int l, int r)
{
	int now = ++cnt;
	if (l == r)
	{
		tree[now].val = a[l];
		return;
	}
	int mid = (l + r) >> 1;
	tree[now].lt = build(l, mid);
	tree[now].rt = build(mid + 1, r);
	tree[now].val = tree[tree[now].lt].val + tree[tree[now].rt].val;
	return now;
}

2、(以主席树为例)主席树,又称可持久化权值线段树,可以分为可持久化和权值两部分

权值线段树:是指线段树不维护数组a中第1~n个数,而是去维护a[1]~a[n]这些值出现的次数,这时我们通常需要去离散化

由于是对权值的维护,所以主席树多与前缀和相关

例1:求区间第k小数

思路:求出数组a中每个前缀a[1]~a[i]对应的权值线段树,然后线段树中对应区间的位置相减就是这个区间有几个数,由此找出第k小,而这多个线段树由主席树实现

struct node
{
	int lt, rt;
	int val;	//对应“权值线段树”维护区间l~r的值,也就是l~r大小的数出现的次数和
}tree[maxn * 40];        //开40倍保险
int root[maxn * 40], cnt;	//root代表根节点的编号,有多少次修改就有多少个链就有多少个root
int a[maxn], b[maxn];
void insert(int pre, int& now, int l, int r, int pos, int w)	//单点修改(插入)
{		//pre是上一个结点的编号,now是当前结点的编号,pos是要插入的位置
	now = ++cnt;						//cnt++创建下一个结点
	tree[now] = tree[pre];		//新节点依附于上一个结点
	tree[now].val += w;
	if (l == r)
		return;
	int mid = (l + r) >> 1;
	//如果插入位置在左半段,那么就新建左结点(更改的区间),依托右节点(无需更改的区间)
	if (pos <= mid)
		insert(tree[now].lt, tree[pre].lt, l, mid, pos, w);
	else
		insert(tree[now].rt, tree[pre].rt, mid + 1, r, pos, w);
}
ll query(int pre, int now, int l, int r, int k)
{		//询问时保证同时访问两个版本的线段树进行相同区间的val加减
	if (l == r)
		return l;
	int x = tree[tree[now].lt].val - tree[tree[pre].lt].val;		
	//pre和now两个版本的主席树对应结点相减,求第k小用对应左子树的值相减
	int mid = (l + r) >> 1;
	if (x >= k)		//判断向左还是向右走
		return	query(tree[pre].lt, tree[now].lt, l, mid, k);	//左子树的第k大
	else
		return query(tree[pre].rt, tree[now].rt, mid + 1, r, k - x);//右子树的第k-x大
}
void solve()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		b[i] = a[i];
	}
	sort(b + 1, b + 1 + n);
	int num = unique(b + 1, b + 1 + n) - b - 1;
	for (int i = 1; i <= n; i++)
	{
		int t = lower_bound(b + 1, b + 1 + num, a[i]) - b;
		insert(root[i - 1], root[i], 1, num, t, 1);
	}
	while (m--)
	{
		int x, y, z;
		cin >> x >> y >> z;
		int t = query(root[x - 1], root[y], 1, num, z);
		cout << b[t] << '\n';
	}
}

例2:区间内保证不超过k个相同数时能取的最多数的个数(k=1时->区间不同数的个数puls),强制在线

思路:

(1)、如果本题是允许离线,并且是询问区间不同数个数

①那么我们可以先对所有询问区间按照右端点升序排序

②然后从1~n遍历a[i]维护线段树,线段树的值为在位置i是否出现过数字a[i]

③维护过程中如果一个数没出现过,则置1,如果出现过就把前面的置0,当前位置置1

④如果在当前位置有询问的右端点,那么就进行查询,没有则继续历遍

(2)、如果在(1)的基础上改为区间取最多k个相同数

我们只需要维护一个vector<int>appear[]和pos[i],appear[x]代表数曾出现的位置,当该数(数的值为i)的个数达到k+1个时,删除第pos[i]个数,也就是说维持树中同一个树不超过k个

(3)、如果在(2)的基础上改为在线询问

那我们只能先处理好数组再解决询问,那么我们保存好n个版本的线段树,然后进行询问操作,询问时调用对应时间的历史版本,这n个线段树我们用主席树来实现

struct node
{
	int lt, rt;
	int val;
}tree[maxn * 40];
int root[maxn * 40];
int cnt, pos[maxn];
vector<int>appear[maxn];
void insert(int& now, int pre, int l, int r, int pos, int w)
{
	now = ++cnt;
	tree[now] = tree[pre];
	tree[now].val += w;
	if (l == r)
		return;
	int mid = (l + r) >> 1;
	if (pos <= mid)
		insert(tree[now].lt, tree[pre].lt, l, mid, pos, w);
	else
		insert(tree[now].rt, tree[pre].rt, mid + 1, r, pos, w);
}
ll query(int now, int l, int r, int pos)    //该函数不是很板子,需要理解
{
	if (l == r)
		return tree[now].val;
	int mid = (l + r) >> 1;
	if (pos <= mid)
		return	query(tree[now].lt, l, mid, pos) + tree[tree[now].rt].val;
	else
		return query(tree[now].rt, mid + 1, r, pos);
}
void solve()
{
	int n, m, k;
	cin >> n >> k;
	int x;
	for (int i = 1; i <= n; i++)
	{
		cin >> x;
		insert(root[i], root[i - 1], 1, n, i, 1);
		appear[x].push_back(i);		//记录数字x出现的下标
		if (appear[x].size() > k)
		{
			insert(root[i], root[i], 1, n, appear[x][pos[x]], -1);
			pos[x]++;		//第x数字再出现且已经到达k个应该删除的位置
		}
	}
	cin >> m;
	int last = 0, l, r;
	for (int i = 0; i < m; i++)
	{
		cin >> l >> r;
		l = (last + l) % n + 1, r = (last + r) % n + 1;
		if (l > r)
			swap(l, r);
		last = query(root[r], 1, n, l);
		cout << last << '\n';
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值