主席树
全称为:可持久化线段树
我们考虑一件事情,如果说我们对于一个线段树来说,希望它可以查询一些修改之前的序列(我们称之为历史版本)
我们当然可以每一次把线段树复制一遍,但是时空复杂度并不能够接受
所以我们应该进行一些可持久化的处理:
我们发现对于单点修改来说,线段树最多改变的就是log2n个节点
所以我们可以把更改的几个节点单独拿出来存储起来,没有改变的部分就指向之前的节点,改变了就指向改变的节点
然后从根节点记录一下这是哪个历史版本(因为根节点一定会改变)
然后就没了
P3919 【模板】可持久化线段树 1(可持久化数组)
这个题是个板子
我们考虑,如果说对于一个数组来说,进行多次更改的复杂度为1,但是记录不同的状态就会变成n
那么主席树就可以把这些的复杂度均摊下来变成log2n对吧
然后就复杂度就变好了
就是个板子嘛过了过了
P3834 【模板】可持久化线段树 2
这个就比较经典(其实并不是很算板子的
我们考虑,静态区间第k小……
如果对于一个区间,我们把它放在桶里,是不是可以进行二分求出来第k小
那么线段树上二分是一个比较经典的套路:如果说权值线段树根节点的左端点数字个数大于等于k说明第k小的数字是在左端点的,否则就是右端点(懂了吧
我们就可以对于每一个都开一个线段树(但是并不是很优秀
那我们想到,如果说对于的区间来说,如果说前面r个数字中有sum1个数字大于等于m,l个数字中有sum2个数字大于等于m,那么说明这个区间里面有sum1-sum2个数字大于等于m
那么我们就可以在主席树上面二分了
我们考虑在主席树上面存储上所有前缀的桶(当然要离散化)
然后再主席树上面二分答案就可以了
这个题还是挺不错的
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 10;
int n, m, q;
int lc[maxn << 5], rc[maxn << 5], cnt, val[maxn << 5], rt[maxn];
int change;
int a[maxn], b[maxn];
void build(int &rt, int l, int r)
{
int mid = l + r >> 1;
rt = ++cnt;
if (l == r)
return;
build(lc[rt], l, mid);
build(rc[rt], mid + 1, r);
return;
}
int add(int lst, int l, int r)
{
int rt = ++cnt;
lc[rt] = lc[lst], rc[rt] = rc[lst], val[rt] = val[lst] + 1;
if (l == r)
return rt;
int mid = l + r >> 1;
if (change <= mid)
lc[rt] = add(lc[rt], l, mid);
else
rc[rt] = add(rc[rt], mid + 1, r);
return rt;
}
int ask(int L, int R, int l, int r, int k)
{
int mid = l + r >> 1, x = val[lc[R]] - val[lc[L]];
if (l == r)
return l;
if (x >= k)
return ask(lc[L], lc[R], l, mid, k);
return ask(rc[L], rc[R], mid + 1, r, k - x);
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin >> n >> q;
for (int i = 1; i <= n; i++)
cin >> a[i], b[i] = a[i];
sort(b + 1, b + 1 + n);
m = unique(b + 1, b + 1 + n) - b - 1;
build(rt[0], 1, m);
for (int i = 1; i <= n; i++)
{
change = lower_bound(b + 1, b + 1 + m, a[i]) - b;
rt[i] = add(rt[i - 1], 1, m);
}
while (q--)
{
int l, r, k;
cin >> l >> r >> k;
cout << b[ask(rt[l - 1], rt[r], 1, m, k)] << endl;
}
return 0;
}
P2633 Count on a tree
这是一道非常经典的树上主席树的题目
首先我们考虑:如果对于一棵树上面两点间的路径来说,他们的路径可以说是他们各自到根的路径减去lca到根的路径再减去lca的父亲到根的路径
那么我们会发现,一个点到根的路径包含的权值在dfs的时候是可以权值主席树搞出来的(不过当然是需要离散化的了
既然这样直接二分就可以了
[P3168 CQOI2015]任务查询系统
这个就是一个板子题,如果说之前的主席树2是单点修改区间查询的话,这个就是区间修改单点查询
那么我们就可以把这个东西转化成一个差分上面前缀和的方式
对于每一个操作我们都把他转化成前端加后端减的操作就好了
P4559 [JSOI2018]列队
这个题由于什么排序不等式的东西(tbl题解)我们会发现,其实我们左边会有一部分的人向右跑,右边会有一部分人向左跑,并且相对顺序并不发生改变……
那么我们权值主席树存一下每一个前缀位置对应的人都在哪些位置
然后再主席树上面二分一下断点(左右跑的断点)然后用一些等差数列什么的东西求一下贡献
P3293 [SCOI2016]美味
我们会发现,这个题在某些意义上面和最大异或和还挺像的
还是像最大异或和一样从高位到低位考虑贡献贪心
我们考虑,如果说前面的几位已经考虑过了,是不是应该考虑这一位是不是有和这一位异或起来等于1的一位
就先把前面的几位答案搞出来,然后再权值主席树里面查找一下是不是有前面几位和答案一样,这意味不一样的数字,有的话这一位就可以变成1没有就不能
然后完了
P2839 [国家集训队]middle
又是一道调了一天的题啊(写主席树写多了一定要记住不是所有的主席树都是权值线段树
我们会发现,如果说一个数字是这个序列的中位数的话(在这道题中)(因为这个题里面偶数序列的中位数是两个数后面的一个
把所有的比中位数大的数字变成1,小于的变成-1,这个序列的和大于等于0
那么我们就可以二分这个中位数,然后再权值线段树上面找
我们会发现这个区间里的所有数字必定选,其他的选前后缀
那么我们就在线段树上面找到区间和,区间最大前缀,区间最大后缀
然后合并check二分就好了