前言
可持久化
可持久化,即对于每次更改,我们都期望记录它的历史信息。
进行可持久化的数据结构通常满足,在修改操作时,数据结构本身的拓扑序没有改变,即形态没有改变;
例如线段树,Trie 树,数组等都可以容易地进行可持久化。
关于主席树和可持久化线段树的区别
主席树全称是可持久化权值线段树,参见知乎讨论。
关于 和 的区别
在数学中(尤其是纯数学领域),
在计算机科学中,
可持久化线段树
非常经典的可持久化权值线段树入门题——静态区间第
引入
给定
我们能想到用整体二分或者二分+分块去解决问题;
若直接建立权值线段树,发现是无法解绝问题的,考虑使用主席树,其的主要思想就是:保存权值线段树上每次插入操作时的历史版本,以便查询区间第
解析
我们分析一下,发现每次修改操作修改的点的个数是一样的,只更改了
(例如下图,修改了

运用动态开点,保存每个节点的左右儿子编号,在记录左右儿子的基础上,保存插入每个数的时候的根节点就可以实现持久化了。
简化一下问题:求区间
我们可以发现,主席树统计的信息也满足前缀和的性质,所以只需要用
复杂度
时间复杂度
建树和离散化的时间复杂度都为
空间复杂度
考虑所有建立的结点的个数,建树有
提示:千万不要吝啬空间(大多数题目中空间限制都较为宽松,因此一般不用担心空间超限的问题)!大胆一点,直接上个
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,m,a[N],unq[N];
struct Tree {
int val;
int ls,rs;
} tree[N<<5];
int id[N],iCnt;
inline void build(int &rt,int l,int r) {
rt=++iCnt;
if(l==r) return ;
int mid=(l+r)>>1;
build(tree[rt].ls,l,mid);
build(tree[rt].rs,mid+1,r);
return ;
}
inline void update(int &rt,int pre,int l,int r,int p) {
rt=++iCnt;
tree[rt]=tree[pre];
tree[rt].val++;
if(l==r)
return ;
int mid=(l+r)>>1;
if(p<=mid) update(tree[rt].ls,tree[pre].ls,l,mid,p);
if(p>mid) update(tree[rt].rs,tree[pre].rs,mid+1,r,p);
return ;
}
inline int query(int nl,int nr,int l,int r,int k) {
if(l==r)
return unq[l];
int x=tree[tree[nr].ls].val-tree[tree[nl].ls].val;
int mid=(l+r)>>1;
if(x>=k) return query(tree[nl].ls,tree[nr].ls,l,mid,k);
if(x<k) return query(tree[nl].rs,tree[nr].rs,mid+1,r,k-x);
}
signed main() {
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%lld",&a[i]);
unq[i]=a[i];
}
sort(unq+1,unq+n+1);
int num=unique(unq+1,unq+n+1)-unq-1;
build(id[0],1,num);
for(int i=1;i<=n;i++) {
int p=lower_bound(unq+1,unq+num+1,a[i])-unq;
update(id[i],id[i-1],1,num,p);
}
for(int i=1;i<=m;i++) {
int l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
printf("%lld\n",query(id[l-1],id[r],1,num,k));
}
return 0;
}[国家集训队] middle
前言
如果支持离线算法,或者带修,还能离线后整体二分做???
简要题意
给出一个长度为
Solution
先想到一个小trick:求中位数可以优先考虑二分答案,二分到
若
中间区间
由于二分到不同的
怎么优化呢?对于两个线段树版本
最后在二分答案的时候直接查询
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e4+10;
int n,Q,q[5];
struct Node {
int v,pos;
} a[N];
struct Tree {
int sum;
int lmax,rmax;
int ls,rs;
} tr[N*20];
int id[N],icnt;
inline void push_up(int rt) {
tr[rt].sum=tr[tr[rt].ls].sum+tr[tr[rt].rs].sum;
tr[rt].lmax=max(tr[tr[rt].ls].lmax,tr[tr[rt].ls].sum+tr[tr[rt].rs].lmax);
tr[rt].rmax=max(tr[tr[rt].rs].rmax,tr[tr[rt].rs].sum+tr[tr[rt].ls].rmax);
return ;
}
inline void build(int &rt,int l,int r) {
rt=++icnt;
if(l==r) {
tr[rt].sum=tr[rt].lmax=tr[rt].rmax=1;
return ;
}
int mid=(l+r)>>1;
build(tr[rt].ls,l,mid);
build(tr[rt].rs,mid+1,r);
push_up(rt);
return ;
}
inline void insert(int &rt,int pre,int l,int r,int x,int k) {
rt=++icnt;
tr[rt]=tr[pre];
if(l==r) {
tr[rt].sum=tr[rt].lmax=tr[rt].rmax=k;
return ;
}
int mid=(l+r)>>1;
if(x<=mid) insert(tr[rt].ls,tr[pre].ls,l,mid,x,k);
else insert(tr[rt].rs,tr[pre].rs,mid+1,r,x,k);
push_up(rt);
return ;
}
inline Tree Merge(Tree x,Tree y) {
Tree z;
z.sum=x.sum+y.sum;
z.lmax=max(x.lmax,x.sum+y.lmax);
z.rmax=max(y.rmax,y.sum+x.rmax);
return z;
}
inline Tree query(int rt,int l,int r,int s,int t) {
if(s<=l&&r<=t)
return tr[rt];
int mid=(l+r)>>1;
if(s<=mid&&mid<t)
return Merge(query(tr[rt].ls,l,mid,s,t),query(tr[rt].rs,mid+1,r,s,t));
if(s<=mid)
return query(tr[rt].ls,l,mid,s,t);
return query(tr[rt].rs,mid+1,r,s,t);
}
inline bool check(int x,int l1,int r1,int l2,int r2) {
int Sum=0;
if(r1+1<=l2-1)
Sum+=query(id[x-1],1,n,r1+1,l2-1).sum;
Sum+=query(id[x-1],1,n,l1,r1).rmax;
Sum+=query(id[x-1],1,n,l2,r2).lmax;
if(Sum>=0)
return true;
return false;
}
inline bool cmp(Node x,Node y) {
return x.v<y.v;
}
signed main() {
scanf("%lld",&n);
for(int i=1;i<=n;i++) {
scanf("%lld",&a[i].v);
a[i].pos=i;
}
sort(a+1,a+n+1,cmp);
build(id[0],1,n);
for(int i=1;i<=n;i++)
insert(id[i],id[i-1],1,n,a[i].pos,-1);
scanf("%lld",&Q);
int lastans=0;
while(Q--) {
for(int i=1;i<=4;i++) {
scanf("%lld",&q[i]);
q[i]=(q[i]+lastans)%n+1;
}
sort(q+1,q+4+1);
int l=0,r=n+1;
while(l+1<r) {
int mid=(l+r)>>1;
if(check(mid,q[1],q[2],q[3],q[4]))
l=mid;
else r=mid;
}
printf("%lld\n",a[l].v);
lastans=a[l].v;
}
return 0;
}[COCI 2020/2021 #3] Specijacija
简要题意
给定一棵
其中一个节点有两个儿子;
其余节点都只有一个儿子;
所有节点按从上到下,从左到右的顺序编号。
给出每一层有两个儿子的节点编号
Solution
我们钦定儿子个数不为
考虑建立一棵新的数将 稀点 连接起来,称这课树为 稀树。
将原树上相邻两个 稀点 之间的所有的点构成一条链,链上的点必然是编号小作为编号大的祖先。
定义
若
和 在同一条链上,则答案必然为编号更小的点; 令
: 若
,则答案为 ; 若
,同理; 若以上皆不满足,则答案为链
的底部的点的编号。
令
题目转化成了如何快速求每个点所在的链的编号,发现相邻两层之间节点的个数差为
不妨从下往上维护每一层的信息,每往上一层,需要支持 单点删除,单点修改,查询第
在最底层建树,把线段树可持久化掉,每一层在上一层的版本上基础上更新新版本,具体的:
线段树上的点维护区间内链的个数,并在叶子节点上维护所在链的编号,对于当前层上的 稀点
这里把
空间复杂度:
建树
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
const int M=520520;
int n,q,t,a[N];
struct Tree {
int id,c;
int ls,rs;
} tr[N*3*20];
struct Node {
int to,nxt;
Node() {
to=nxt=0;
}
Node(int a,int b) {
to=a,nxt=b;
}
} adj[M<<1];
int head[M],idx;
int id[N],icnt;
int num[N],tot,p[M];
int dep[M],f[M][22];
inline int calc(int x) {
return x*(x+1)/2;
}
inline void add(int x,int y) {
adj[++idx]=Node(y,head[x]);
head[x]=idx;
return ;
}
inline void dfs(int u,int fa) {
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<=20;i++)
f[u][i]=f[f[u][i-1]][i-1];
for(int i=head[u];i;i=adj[i].nxt) {
int v=adj[i].to;
if(v==fa)
continue;
dfs(v,u);
}
return ;
}
inline int lca(int x,int y) {
if(dep[x]<dep[y])
swap(x,y);
for(int i=20;i>=0;i--) {
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
}
if(x==y)
return x;
for(int i=20;i>=0;i--) {
if(f[x][i]!=f[y][i]) {
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
inline void push_up(int rt) {
tr[rt].c=tr[tr[rt].ls].c+tr[tr[rt].rs].c;
return ;
}
inline void build(int &rt,int l,int r) {
rt=++icnt;
if(l==r) {
tr[rt].id=++tot;
tr[rt].c=1;
return ;
}
int mid=(l+r)>>1;
build(tr[rt].ls,l,mid);
build(tr[rt].rs,mid+1,r);
push_up(rt);
return ;
}
inline void clone(int &rt,int pre) {
rt=++icnt;
tr[rt]=tr[pre];
return ;
}
inline void update(int &rt,int pre,int l,int r,int x,int k,int c) {
clone(rt,pre);
if(l==r) {
tr[rt].id=k;
tr[rt].c=c;
return ;
}
int mid=(l+r)>>1;
if(x<=tr[tr[rt].ls].c) update(tr[rt].ls,tr[pre].ls,l,mid,x,k,c);
else update(tr[rt].rs,tr[pre].rs,mid+1,r,x-tr[tr[rt].ls].c,k,c);
push_up(rt);
return ;
}
inline int query(int rt,int l,int r,int x) {
if(l==r)
return tr[rt].id;
int mid=(l+r)>>1;
if(x<=tr[tr[rt].ls].c) return query(tr[rt].ls,l,mid,x);
return query(tr[rt].rs,mid+1,r,x-tr[tr[rt].ls].c);
}
signed main() {
scanf("%lld%lld%lld",&n,&q,&t);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
int m=calc(n+1);
for(int i=1;i<=n+1;i++)
num[i]=calc(i);
for(int i=1;i<=n+1;i++)
p[i]=calc(n)+i;
build(id[n+1],1,n+1);
for(int i=n;i>=1;i--) {
int pos=a[i]-calc(i-1);
p[++tot]=a[i];
int x=query(id[i+1],1,n+1,pos);
int y=query(id[i+1],1,n+1,pos+1);
add(tot,x),add(x,tot);
add(tot,y),add(y,tot);
clone(id[i],id[i+1]);
update(id[i],id[i],1,n+1,pos+1,0,0);
update(id[i],id[i],1,n+1,pos,tot,1);
}
dfs(tot,0);
int lastans=0;
while(q--) {
int x,y;
scanf("%lld%lld",&x,&y);
x=(x-1+t*lastans)%m+1;
y=(y-1+t*lastans)%m+1;
int dx=lower_bound(num+1,num+n+1+1,x)-num;
int dy=lower_bound(num+1,num+n+1+1,y)-num;
int px=query(id[dx],1,n+1,x-calc(dx-1));
int py=query(id[dy],1,n+1,y-calc(dy-1));
if(px==py) {
lastans=min(x,y);
printf("%lld\n",lastans);
continue;
}
int pz=lca(px,py);
if(px==pz)
lastans=x;
else {
if(py==pz)
lastans=y;
else lastans=p[pz];
}
printf("%lld\n",lastans);
}
return 0;
}Count on a tree
闲话
做完这题,对可持久化线段树的最主要用法:查询静态区间第
一开始想了一种类似于上面的【middle】的二分+树链剖分+可持久化线段树的
简要题意
给定一棵
Solution
结构其实和模版题差不多,只不过把序列问题转化为了树上问题。
对于点对
从根开始遍历整棵树,对于当前节点
反思
一开始想的是树链剖分的建树,并按 dfn 序的顺序继承版本,很错误还很复杂。so,有时候不要被树链剖分给带偏了方向!!!
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m,a[N];
struct Node {
int to,nxt;
Node() {
to=nxt=0;
}
Node(int a,int b) {
to=a,nxt=b;
}
} adj[N<<1];
struct Tree {
int sum;
int ls,rs;
} tr[N<<5];
int head[N],idx;
int num,unq[N];
int id[N],icnt;
int dep[N],f[N][22];
inline void add(int x,int y) {
adj[++idx]=Node(y,head[x]);
head[x]=idx;
return ;
}
inline void push_up(int rt) {
tr[rt].sum=tr[tr[rt].ls].sum+tr[tr[rt].rs].sum;
return ;
}
inline void build(int &rt,int l,int r) {
rt=++icnt;
if(l==r)
return ;
int mid=(l+r)>>1;
build(tr[rt].ls,l,mid);
build(tr[rt].rs,mid+1,r);
push_up(rt);
return ;
}
inline void update(int &rt,int pre,int l,int r,int x) {
rt=++icnt;
tr[rt]=tr[pre];
if(l==r) {
tr[rt].sum=1;
return ;
}
int mid=(l+r)>>1;
if(x<=mid) update(tr[rt].ls,tr[pre].ls,l,mid,x);
if(mid<x) update(tr[rt].rs,tr[pre].rs,mid+1,r,x);
push_up(rt);
return ;
}
inline int query(int nx,int ny,int nz,int nf,int l,int r,int k) {
if(l==r)
return unq[l];
int mid=(l+r)>>1;
int sl=tr[tr[nx].ls].sum+tr[tr[ny].ls].sum-tr[tr[nz].ls].sum-tr[tr[nf].ls].sum;
if(k<=sl) return query(tr[nx].ls,tr[ny].ls,tr[nz].ls,tr[nf].ls,l,mid,k);
return query(tr[nx].rs,tr[ny].rs,tr[nz].rs,tr[nf].rs,mid+1,r,k-sl);
}
inline void dfs(int u,int fa) {
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<=20;i++)
f[u][i]=f[f[u][i-1]][i-1];
int p=lower_bound(unq+1,unq+num+1,a[u])-unq;
update(id[u],id[fa],1,num,p);
for(int i=head[u];i;i=adj[i].nxt) {
int v=adj[i].to;
if(v==fa)
continue;
dfs(v,u);
}
return ;
}
inline int lca(int x,int y) {
if(dep[x]<dep[y])
swap(x,y);
for(int i=20;i>=0;i--) {
if(dep[f[x][i]]>=dep[y])
x=f[x][i];
}
if(x==y)
return x;
for(int i=20;i>=0;i--) {
if(f[x][i]!=f[y][i]) {
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
signed main() {
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%lld",&a[i]);
unq[i]=a[i];
}
for(int i=1;i<n;i++) {
int x,y;
scanf("%lld%lld",&x,&y);
add(x,y),add(y,x);
}
sort(unq+1,unq+n+1);
num=unique(unq+1,unq+n+1)-unq-1;
build(id[0],1,num);
dfs(1,0);
int lastans=0;
while(m--) {
int x,y,k;
scanf("%lld%lld%lld",&x,&y,&k);
x^=lastans;
int z=lca(x,y);
lastans=query(id[x],id[y],id[z],id[f[z][0]],1,num,k);
printf("%lld\n",lastans);
}
return 0;
}总结
可持久化线段树是一种类似前缀和的数据结构,具有和前缀和类似的区间加减及差分等性质,常通过记录历史版本实现查询静态区间第
也可以将离线时线段树的维护操作可持久化掉,强转在线;
多去观察题目中不同历史版本之间的关系,若新版本可以由上一个版本通过单点/区间的修改/删除操作更新,可考虑使用可持久化线段树优化。
可持久化栈
引入
可持久优化栈时一种支持 栈顶修改,栈顶查询,回退历史版本 的一种数据结构。
由于手写栈依赖于数组,也可以用可持久优化 数组(或线段树) 来模拟栈,单次插入/删除时间复杂度
省去随机访问的需求后,可持久化栈可以做到单次修改时间复杂度
[USACO10OPEN] Time Travel S
Solution
插入
向栈顶加入新元素,并记录这次操作的栈顶编号和前驱编号。
stk[++top]=x;
id[i]=top;
pre[id[i]]=id[i-1];删除
只需要把上一次加入的元素(前驱)复制到当前即可。
id[i]=pre[id[i-1]];版本跳跃
注意题目要求,此题时回到第
id[i]=id[x-1];查询
第
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10;
int n,id[N],pre[N];
int stk[N],top;
signed main() {
scanf("%lld",&n);
for(int i=1;i<=n;i++) {
char op;
scanf(" %c",&op);
if(op=='a') {
int x;
scanf("%lld",&x);
stk[++top]=x;
id[i]=top;
pre[id[i]]=id[i-1];
}
if(op=='s')
id[i]=pre[id[i-1]];
if(op=='t') {
int x;
scanf("%lld",&x);
id[i]=id[x-1];
}
if(!id[i]) printf("-1\n");
else printf("%lld\n",stk[id[i]]);
}
return 0;
}后记
可能有些的不足的地方,多多包涵!!!