可持久化线段树求区间第 k k k小值(静态主席树)
这道题考虑使用静态主席树就好了
考虑一个问题:给定
n
n
n个数字,之后询问在
[
l
,
r
]
[l,r]
[l,r]内的第
k
k
k小的值是多少。
暴力写法
考虑暴力写法。简单来说,给这几个数字直接sort一下,就求到了。
然后
t
t
t组询问直接把时间复杂度卡上天。
主席树
主席树一种基于线段树的数据结构,全称是可持久化权值线段树(然而思想更重要)。
对于求区间第k小,可以使用主席树(如果想尝试每一次都开一棵线段树也行,MLE)
使用主席树求区间第k小问题
先不考虑怎么求区间第k小。先考虑普通线段树如何求得整个序列第k小的数字。
普通线段树求序列第k小
一个很简单的方法。假定这列数字是从 1 1 1到 n n n的(就算不是,离散化一下就好了)。如果我们可以按照其大小一个一个插入线段树中,让每个叶节点记录这个数字的个数(具体来讲,就是叶节点管理的那个数字的个数),然后直接按个数找就可以了(其实就是权值线段树,每个叶节点都是个桶)。
普通线段树求区间第k小
那么根据上面的应用,是不是可以在每一次插入一个数的时候,复制出一棵新的树,然后再插进去。根据类似前缀和的思想,找第 r r r棵树和第 l − 1 l-1 l−1棵树,前一个减去后一个的信息不就可以找到了嘛。(当然,会MLE,很显然,但是也很暴力)
使用可持久化线段树
如果注意到可持久化是怎么实现可持久化的话,可以发现,对于修改操作,线段树只有一部分结点会受到影响。对于上面(第二个)例子来说,每次插入一个数的时候也只有一个结点会受到影响。也就是说,可以不用每一次造出一棵树,而是可以使用可持久化思想建树,这样就避免了空间的浪费,查询区间第k小只需要找第 r r r个版本和第 l − 1 l-1 l−1个版本的根节点即可。
前置知识
- 线段树
- 线段树的可持久化版本
- 动态开点
建立主席树
建立主席树之前,先声明主席树的结点(为了方便,拆开了,没有包装)
声明
下面这个结点声明是基于求区间第k小而声明的(不同题目各有不同,这点非常重要)
const int MAXN = 2e5 + 10;
struct Node {
int cnt;
int lc, rc;
}tr[MAXN << 5];
int tot = 0;
int ver[MAXN];
上面的声明有几个要点:
cnt
是要当桶存数量的MAXN << 5
的作用是要开足空间,可持久化线段树需要一定的空间(所以只要没炸就开大点)tot
是动态开点用的ver
,即为version
,就是历史版本
现在可以开始建树了。
建树
由于问题是求区间第k小,需要先建立一个空的线段树(也就是啥都没有,前缀和思想经常用到的 l − 1 l-1 l−1)。建树过程和可持久化线段树一样。
void build(int l, int r, int now) {
if (l == r) return;
int mid = l + ((r - l) >> 1);
tr[now].lc = ++ tot;
build(l, mid, tr[now].lc);
tr[now].rc = ++ tot;
build(mid + 1, r, tr[now].rc);
}
插入
对于主席树来说,每一次插入都需要建立一个新的历史版本。
注意一下,这里需要一个更新(pushup啥的)。
inline void update(int now) {
tr[now].cnt = tr[tr[now].lc].cnt + tr[tr[now].rc].cnt;
}
void Insert(int val, int l, int r, int oldv, int newv) {
if (l == r) {
tr[newv].cnt = tr[oldv].cnt + 1; // 从上个版本继承下来
return;
}
int mid = l + ((r - l) >> 1);
tr[newv].lc = tr[oldv].lc; tr[newv].rc = tr[oldv].rc;
if (val <= mid) {
tr[newv].lc = ++ tot;
Insert(val, l, mid, tr[oldv].lc, tr[newv].lc);
} else {
tr[newv].rc = ++ tot;
Insert(val, mid + 1, r, tr[oldv].rc, tr[newv].rc);
}
update(newv);
}
查询
这个就是查询的程序了。
int querykth(int k, int l, int r, int lv, int rv) {
if (l == r) return l; // 因为直接开的桶
int x = tr[tr[rv].lc].cnt - tr[tr[lv].lc].cnt; // 检查左边有多少个数
int mid = l + ((r - l) >> 1);
if (k <= x)
return querykth(k, l, mid, tr[lv].lc, tr[rv].lc);
else
return querykth(k - x, mid + 1, r, tr[lv].rc, tr[rv].rc); // 去右边查要剪一下
}
这样就可以了。
例题代码
下面就是这道例题的解决代码。
constexpr int MAXN = 2e5 + 10;
struct Node {
int val, lc, rc;
}tr[MAXN << 5];
int tot = 1;
int a[MAXN], b[MAXN];
int ver[MAXN];
inline void update(int now) {
tr[now].val = tr[tr[now].lc].val + tr[tr[now].rc].val;
}
void build(int pll, int prr, int now) {
if (pll == prr)
return;
int mid = pll + ((prr - pll) >> 1);
tr[now].lc = ++ tot;
build(pll, mid, tr[now].lc);
tr[now].rc = ++ tot;
build(mid + 1, prr, tr[now].rc);
}
void modify(int pos, int pll, int prr, int old, int newv) {
if (pll == prr) {
tr[newv].val = tr[old].val + 1;
return;
}
tr[newv].lc = tr[old].lc; tr[newv].rc = tr[old].rc;
int mid = pll + ((prr - pll) >> 1);
if (pos <= mid) {
tr[newv].lc = ++ tot;
modify(pos, pll, mid, tr[old].lc, tr[newv].lc);
} else {
tr[newv].rc = ++ tot;
modify(pos, mid + 1, prr, tr[old].rc, tr[newv].rc);
}
update(newv);
}
int querykth(int k, int pll, int prr, int lefv, int rigv) {
if (pll == prr)
return pll;
int mid = pll + ((prr - pll) >> 1);
int x = tr[tr[rigv].lc].val - tr[tr[lefv].lc].val;
if (x >= k)
return querykth(k, pll, mid, tr[lefv].lc, tr[rigv].lc);
return querykth(k - x, mid + 1, prr, tr[lefv].rc, tr[rigv].rc);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, t;
cin >> n >> t;
for (int i = 1; i <= n; ++ i) {
cin >> a[i]; b[i] = a[i];
}
sort(b + 1, b + 1 + n);
int len = unique(b + 1, b + 1 + n) - b - 1; // 注意这题要离散化
ver[0] = tot;
build(1, len, ver[0]);
for (int i = 1; i <= n; ++ i) {
a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b;
ver[i] = ++ tot; // 新建个树
modify(a[i], 1, len, ver[i - 1], ver[i]);
}
while (t --) {
int l, r, k;
cin >> l >> r >> k;
cout << b[querykth(k, 1, len, ver[l - 1], ver[r])] << '\n';
}
return 0;
}