这两天看了看可持久化数据结构那篇paper,看看大牛的博客,自己想了想,感觉对函数式线段树有点领悟了。
虽然对我来说很难理解,也还是在慢慢理解。
但还是不太懂可持久化思想,也不太清楚函数式具体指什么,留给之后的我再考虑吧。
具体来看这题,我用的是函数式线段树,似乎也称为主席树。
做法是这样的,建立n棵线段树,每棵线段树维护的区间都是[1,sz],其中sz是序列a[1],a[2]...a[n]去重离散后个数。
任意a[i], 离散后必然在[1,sz]之间,所以对于第i棵线段树,就将离散后的a[1]~a[i]插入线段树。
比如对于a[j](1 <= j <= i),离散后是x, 那么第i棵线段树的从根到第x个叶子节点的维护值就相应+1。
非叶子节点的维护值就是区间和。
这种最朴素的做法方便理解,但时空复杂度都难以忍受,所以就有下面的优化啦。
假如已经建立第i-1棵线段树,那么第i棵就是在第i-1棵的基础上插入了离散后的a[i],对于非叶子节点,要么在左边插入,要么在右边插入,那么没有插入的子树和第i-1的子树的结构完全一样。所以其实有很多节点是不用新建的,就标记一下,指向第i-1棵线段树相同的儿子就行了。
一开始初始第0棵就好了。
然后是查询操作,为什么这么做就可以查询[L,R]第k大呢?
原因是对于区间[L,R],查询时只关注两棵线段树,root[L-1]和root[R]。这两棵树的不同是由于root[R]插入了离散后的a[L]~a[R],由于线段树内的值是经过离散化的,第k大就相当于第k个数,然后找到这个数就行啦,插入过程相当于计数排序。
以上是我的理解,欢迎指正和交流。
函数式线段树和划分树跑的时间一样...空间稍大,但编程复杂度严重降低,是种适合竞赛的优秀数据结构。我会说划分树我只会套模板么。
附代码:
#include<cstdio> #include<algorithm> using namespace std; const int N = 30005; int root[N<<5], ls[N<<5], rs[N<<5], sum[N<<5]; int a[N], lisan[N]; int cnt = 0; void build(int &rt, int l, int r){ rt = ++cnt; if(l == r) return; int m = (l+r) >> 1; build(ls[rt], l, m); build(rs[rt], m+1, r); } void update(int u, int &v, int l, int r, int x){ v = ++cnt; ls[v] = ls[u], rs[v] = rs[u], sum[v] = sum[u] + 1; if(l == r) return; int m = (l+r) >> 1; if(x <= m) update(ls[u], ls[v], l, m, x); //更新左边 else update(rs[u], rs[v], m+1, r, x); //更新右边 } int query(int u, int v, int l, int r, int x){ if(l == r) return l; int tmp = sum[ls[v]] - sum[ls[u]]; int m = (l+r) >> 1; if(x <= tmp) return query(ls[u], ls[v], l, m, x); else return query(rs[u], rs[v], m+1, r, x-tmp); } int main(){ int n, m, tmp; while(scanf("%d %d", &n, &m) != EOF){ for(int i = 1; i <= n; ++i){ scanf("%d", &a[i]); lisan[i] = a[i]; } sort(lisan+1, lisan+n+1); int sz = unique(lisan+1, lisan+n+1) - lisan-1; build(root[0], 1, sz); //初始化第0棵树 for(int i = 1; i <= n; ++i){ int tmp = lower_bound(lisan+1, lisan+sz+1, a[i]) - lisan; update(root[i-1], root[i], 1, sz, tmp); //在前一棵树的基础上建立新树 减少空间 } for(int i = 1; i <= m; ++i){ scanf("%d", &tmp); int x = query(root[0], root[tmp], 1, sz, i); printf("%d\n", lisan[x]); } } }