可持久化线段树 2
题目描述
核心思路
这题可以使用可持久化权值线段树来实现,也就是主席树来实现。 主席树是一种可持久化的数据结构——可持久化权值线段树(因为其有函数的性质也叫函数式线段树)
可持久化就是有多个版本。但是我们考虑一下,如果对于每个版本都新建一个线段树,那么空间复杂度就会爆炸。可以发现,两版本之间有很多的相同之处,如果能利用上这些相同的部分,那么就可以节省很大一部分空间。
我们可以在构建新版本的线段树的时候,把没有变动的部分连回到上一个版本的那个部分上
如下图:
红色部分就是发生变动修改的节点,我们只需要在新版本中将发生变动的节点动态生成出来,我们并不需要把没有发生变动的节点也生成出来,直接利用上一个版本没有发生变动的节点就好了
对于这道题,我们思考朴素做法:每次对于[L,R]区间的副本进行排序,然后输入这个区间内的第K个数, 时间复杂度:排序O(nlogn),m次询问,总:O(mnlogn),数据范围 1 ≤ N , M ≤ 2 e 5 1\leq N,M\leq 2e5 1≤N,M≤2e5,绝对会超时
发明者的原话:对于原序列的每一个前缀[1···i]建立出一棵线段树维护值域上每个数出现的次数,则其树是可减的
权值线段树就是对一个值域上值的个数进行维护的线段树。简单理解就是,它是一棵线段树,节点内的值表示的是某个值域内值的个数,叶子节点就是单纯的一个值,所以其节点的值就是1,非叶子节点它表示的就是某一段区间中值的个数了。
举个栗子,对于1,1,2,3,3,3,4,4,建立它的权值线段树如下:
值域区间[1,1]它这个叶子节点内的值为2,表示的是数1出现的次数为2,值域区间[1,2]它这个非叶子节点内的值为3,表示的是数1,数2出现的次数为3.
对于一段数字序列,每次询问其第K小值, 那么先建树然后对于每次询问递归查找即可。要找的数如果在左子树上,就递归查找左子树上的第k大;如果在右子树上,那么就递归查找右子树上的第(k-左子树值)大。
对于这道题来说, 我们需要先对数据进行离散化。为什么要离散化呢?因为权值线段树维护的是值域上值的个数,数据范围是 − 1 e 9 ≤ a [ i ] ≤ 1 e 9 -1e9\leq a[i]\leq 1e9 −1e9≤a[i]≤1e9,显然开不出这么大的线段树。因为问题只与数的大小相关,而与数本身是多少无关,所以我们可以对数进行离散化,只保存其大小关系即可。
例:25957 6405 15770 26287 26465 离散化结果:3 1 2 4 5
然后我们的主要思想是:用主席树构造一颗可持久化权值线段树,对于每个数字,将其离散化后,新建一个版本的权值线段树,然后插入这个离散化后的数字。例如对于上述序列,我们就要依次建立5个版本的权值线段树,分别插入3 1 2 4 5这五个数。这就是“对原序列的每一个前缀建树”。
如何进一步节省空间?比如现在有一个值域区间是[1,4]的空主席树,我们依次插入离散化后的结果3 2 1 4
我们发现每个版本的根节点表示的值域区间是 [ 1 , i ] [1,i] [1,i],那么我们该如何求出区间 [ L , R ] [L,R] [L,R]呢? 对于查询操作L,R,我们利用主席树的函数性质,用R那个版本的权值线段树减去L-1那个版本的权值线段树,在得到的权值线段树中查找第K小值就可以了。有点类似前缀和的道理。 这样为什么是正确的呢?因为权值线段树储存的是值域上值的个数,我们用R版本的权值线段树减去L-1版本的权值线段树,得到的就是维护[L,R]上值的个数的权值线段树。
那么我们该如何存储主席树呢?我们可以建立一个数组int root[maxn],来储存每个版本根节点的编号。对于儿子结点,可以看出主席树不像线段树可以用当前结点编号乘2和乘2加1来得到左右儿子的结点编号(因为我们是采用动态开点节省空间的形式),于是我们可以这样做:用一个struct储存当前结点的左右儿子结点的编号和当前结点的值,然后用这个struct开一个内存池,每新建一个主席树的结点,就从内存池里取一块新的空间送给这个结点。取空间从tr[1]开始,tr[0]充当NULL
struct Node{
//左子树 右子树 节点个数
//该节点的左儿子是tr[l] 右儿子是tr[r]
int l,r,sum;
}tr[N*40]; //这里的乘40只是个人习惯,写32,40,50的都有
//idx是给每个节点分配编号 数组a存储的是原来数组的值
//root[i]记录的是第i个版本的树根
int idx,a[N],root[N];
问题: 用不用像线段树那样先构建好整个主席树然后再执行操作?
不用,准确地说,是程序已经帮我们构建好了。因为一开始时树里什么都没有,所以所有节点的l,r,sum都是0,而全局变量和数组都是默认赋值为0的。所以我们直接边插入边建树就OK了。
问题: 如何进行两树之间的减法?用不用新建一个权值线段树令其等于主席树两版本之差然后对这个权值线段树进行询问?
不用,我们可以在询问的时候边递归边减。
代码
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int N=2e5+10;
struct Node{
//左子树 右子树 节点个数
int l,r,sum;
}tr[N*40];
vector<int>v; //用来弄离散化
//idx是给每个节点分配编号 数组a存储的是原来数组的值
//root[i]记录的是第i个版本的树根
int idx,a[N],root[N];
int n,m;
//返回数x离散化后的得到的值
int find(int x)
{
//因为一般线段树下标都是从1开始 所以我们这里要+1
return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
//[l,r]是区间 往区间中插入数x u表示当前版本 pre是上一个版本
void insert(int l,int r,int pre,int &u,int x)
{
//新开一个版本tr[++idx] 我们先把上一个版本复制给这个新的版本
tr[++idx]=tr[pre];
u=idx; //让u成为当前版本
tr[u].sum++; //由于插入了一个数x 所以当前版本的线段树中多了一个节点
//到了叶子节点 就是递归边界
if(l==r)
return;
int mid=l+r>>1; //分界点
//将x插入到左子树中
if(x<=mid)
insert(l,mid,tr[pre].l,tr[u].l,x);
//将x插入到右子树中
else
insert(mid+1,r,tr[pre].r,tr[u].r,x);
}
//查询区间[l,r]中的第k小的数 这里L是指第L个版本 R是指第R个版本
int query(int l,int r,int L,int R,int k)
{
//查询到了叶子节点 则返回答案
if(l==r)
return l;
int mid=l+r>>1; //分界点
//cnt统计的是第R个版本左子树和第L个版本左子树的节点数目之差
//那么此时cnt就是 区间[l,r]中节点个数
int cnt=tr[tr[R].l].sum-tr[tr[L].l].sum;
if(k<=cnt) //往左子树寻找第k小
return query(l,mid,tr[L].l,tr[R].l,k);
else //往右子树寻找第k小
return query(mid+1,r,tr[L].r,tr[R].r,k-cnt);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
a[i]=x;
v.push_back(a[i]);
}
sort(v.begin(),v.end()); //排序
v.erase(unique(v.begin(),v.end()),v.end()); //去重
for(int i=1;i<=n;i++)
insert(1,n,root[i-1],root[i],find(a[i]));
//处理这m个询问
while(m--)
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
//这里得到的是离散化后
int x=query(1,n,root[l-1],root[r],k);
//由于我们在离散化时+1了 所以还原回去时需要-1
//比如v此时存储的就是6405(0) 15770(1) 25957(2) 26287(3) 26465(4) 括号内的是下标
//但是我们离散化时把下标+1了 因此假设此时得到的结果是x=3 那么这个x是离散化后的 而且它是+1的
//那么它在v中的真实下标应该是x-1 也就是下标2 然后通过这个下标就可以找到其对应的值是25957
printf("%d\n",v[x-1]);
}
return 0;
}