一、可持久化的数据结构
如果想知道数据集在任意时刻的历史状态(即能保存每次改变前的状态和改变后的状态),那么就需要使用可持久化的数据结构
如果在每次操作后都直接拷贝所有数据,那么时空复杂度过大,但是我们会发现,每次进行修改(如线段树的单点修改)时,只会影响一部分节点(如一条链)的值,所以我们可以只创建发生改变的部分的副本,不拷贝其他部分
二、可持久化线段树
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';
}
}