【问题引入】
给定一个长度为n的数列,询问区间第k大的数。
【分析】
在正式介绍主席树之前,先来介绍一下“值域线段树”。
为便于讲解,举个例子,对于下图的数列,我们可以对它构造这样的“值域线段树”:
后面的数字表示数列在[l,r]中有多少个不同的数。
有了这棵树,我们可以很方便地求出第k大的数。
那么,怎么求区间第k大呢?
我们建立n棵“值域线段树”。第i棵线段树表示[1,i]的状态。这样一来,查询区间[l,r]时,只需将r号线段树与l-1号线段树的对应位置相减即可。
然而,如果真的将这么多棵线段树都造出来,毫无疑问会MLE。
我们发现,建树的过程本质上是对上一棵线段树进行单点查询。回想一下单点查询的过程,可以发现:每次只是修改了一条路径。所以,我们可以只为修改了的节点开辟内存,而对于那些没有修改的节点,和前面的线段树分享即可。
分析一下空间复杂度:由于每次只加入一条链,而每条链的长度是O(logn)级别的。所以空间复杂度是O(nlogn)
【代码】
#include<cstdio>
#include<cstring>
#include<algorithm>
#define mid ((l + r) >> 1)
using namespace std;
const int mn = 200005;
struct seg{
int lch, rch, sum;
}t[mn << 5];
int root[mn], a[mn], b[mn], cnt;
void make_tree(int &rt, int l, int r)
{
rt = ++cnt;
if(l == r)
return;
make_tree(t[rt].lch, l, mid), make_tree(t[rt].rch, mid + 1, r);
}
void edit_tree(int &r1, int &r2, int l, int r, int val)
{
r1 = ++cnt, t[r1] = t[r2], t[r1].sum++;//以r2为上一版本新建节点
if(l == r)
return;
if(val <= mid) //获取儿子节点的编号
edit_tree(t[r1].lch, t[r2].lch, l, mid, val);
else
edit_tree(t[r1].rch, t[r2].rch, mid + 1, r, val);
}
int query(int r1, int r2, int l, int r, int k)
{
int x = t[t[r2].lch].sum - t[t[r1].lch].sum;
if(l == r)
return b[l];
if(x >= k)
return query(t[r1].lch, t[r2].lch, l, mid, k);
else
return query(t[r1].rch, t[r2].rch, mid + 1, r, k - x);
}
int main()
{
int n, m, i, l, r, k;
scanf("%d%d", &n, &m);
for(i = 1; i <= n; i++)
scanf("%d", &a[i]), b[i] = a[i];
sort(b + 1, b + 1 + n);
int siz = unique(b + 1, b + 1 + n) - b - 1; make_tree(root[0], 1, siz);
for(i = 1; i <= n; i++)
{
int val = lower_bound(b + 1, b + 1 + siz, a[i]) - b;//离散化
edit_tree(root[i], root[i - 1], 1, siz, val);//root[i]:前i个位置的状态对应的线段树的根节点
}
while(m--)
{
scanf("%d%d%d", &l, &r, &k);
printf("%d\n", query(root[l - 1], root[r], 1, siz, k));
}
}