引入
在有些题目中,在进行一系列更改以后,我们需要访问之前某次更改的版本。对于传统的线段树来说,这自然不易达成。然而我们可以记录一下每一次修改的版本。每改一次直接开一棵新的线段树即可。下面通过一道例题,阐述一下可持久化线段树的思想与实现。
SPOJ TTM - To the moon
题意翻译
一个长度为 的数组 , 种操作 :
:区间 中的数都加 ,同时当前的时间戳加 。
:查询当前时间戳区间 中所有数的和 。
:查询时间戳 区间 的和 。
:将当前时间戳置为 。
所有操作均合法 。
:刚开始时时间戳为
输入格式,一行 和 ,接下来 行每行一个操作
输出格式:对每个查询输出一行表示答案
数据保证:,,,。在刚开始没有进行操作的情况下时间戳为 ,且保证 操作不会访问到未来的时间戳。
思路:
如果没有 和 操作的话,这一道题用普通的线段树就能很好地解决。现在,我们需要记录每一个历史版本,且要求能够对历史版本进行修改,即完全可持久化。
如何记录每一个版本?首先我们要建一棵动态开点的线段树,目的是为了节省空间和便于修改。假设我们现在有一个长度为 的数组,每个节点记录了该区间的区间和,如下图:

其中,一号节点对应区间 ,二号节点对应区间 ,三号节点对应区间 ,以此类推。
假设我们现在要给区间 的所有数加上一个数,由于七号节点对应的位置的数组下标是二,五号节点对应的数组位置下标为三,那么值将要被修改的节点只有 ,,,, 这五个节点。我们新建五个点,将 ,,,, 几个节点的信息一一复制到新开的五个点中,再在这新建的五个点上修改,新建点后的树如下图:

其中, 号节点对应 号节点, 号节点对应 号节点。其它节点同理。容易发现,每进行一次区间修改,只会影响到有限个数的点。这样,我们每次修改所新建的节点个数就是 级别的,在接受的范围内。
在新建节点中,我们还在 和 , 和 之间分别连接了一条边。为什么要这么做呢?
其实,我们只复制了需要更改的点,并将他们"替换"到以前的树上,这样,就可以以较小的修改得到完整的树。同时,为了记录每一个版本的树,我们需要一个 数组。 表示第 个版本的树的根节点的编号。
在这里还有一个问题:如果这样去写,区间标记的下传会变得很麻烦,这里采用标记永久化即可。
代码如下:
查询代码:
并查集的结构是一个森林,因此只需建 棵树,类比上述代码即可。
部分可持久化:
完全可持久化要求对历史版本能进行操作和查值。它的空间复杂度一般比较大。在一些题目中,我们只需要查找值,不需要对过去版本进行修改,那么,这种只查找,在最新版本上进行修改的操作就叫部分可持久化。
部分可持久化其实很好实现。假设你有一个数组,你只需要给每一个位置开一个 ,其中第一关键字记录其版本号,第二关键字记录其值,每修改一次就 一下,查找的时候二分一个版本号即可,这样就能实现部分可持久化。
第 大问题
第 大问题,即要求一个数列在区间 中第 大的数是多少,且一般带有修改操作,强制在线。用可持久化线段树能较好地解决这些问题。
先看看不带修改操作的。
从左至右,每插入一个数,就构成了一个新的版本。我们建立 棵值域线段树,第 棵线段树表示从左至右插入了 个数的情况。那么,对于区间 的情况,只需看看从第 棵线段树到第 棵线段树增加了多少元素,再在增加的这一部分元素形成的线段树上进行二分,求得第 大。当然实际操作中我们并不需要真的把那棵线段树求出来,只需要在两个版本的值域线段树上同时二分即可。
实现如下:
如果需要处理带修改的区间第 大,考虑到如果改变 位置的数,那么第 个版本及以后的线段树必然会需要修改,这也是一个区间操作,所以一般采用树状数组套在线段树外面。但其修改,查询思想还是类似的。
代码:
#include<bits/stdc++.h>
#define mid (tl + tr >> 1)
using namespace std;
const int MAXN = 3e6;
int n,m,a[MAXN + 5],l,r,k,tot,root[MAXN + 5],lseg[MAXN + 5],rseg[MAXN + 5];;
string s;
vector<int> lsh;
struct node{
int ls,rs,sum;
}tree[4 * MAXN + 5];
struct Mes{
bool flag;
int l,r,k;
}mes[MAXN + 5];
void insert(int i,int tl,int tr,int val,int ad){
if(tl == tr){
tree[i].sum += ad;
return;
}
if(mid >= val){
if(tree[i].ls == 0)tree[i].ls = ++tot;
insert(tree[i].ls,tl,mid,val,ad);
}
else{
if(tree[i].rs == 0)tree[i].rs = ++tot;
insert(tree[i].rs,mid + 1,tr,val,ad);
}
tree[i].sum = tree[tree[i].ls].sum + tree[tree[i].rs].sum;
}
int lowbit(int i){
return i & (-i);
}
void add(int pos,int num){
for(int i = pos; i <= n; i += lowbit(i)){
if(root[i] == 0)root[i] = ++tot;
insert(root[i],1,lsh.size(),num,1);
}
}
void change(int pos,int num){
for(int i = pos; i <= n; i += lowbit(i)){
insert(root[i],1,lsh.size(),a[pos],-1);
}
for(int i = pos; i <= n; i += lowbit(i)){
insert(root[i],1,lsh.size(),num,1);
}
a[pos] = num;
}
int query(int tl,int tr,int k)
{
if(tl==tr)return tl;
int sum=0;
for(int i=1;i<=rseg[0];++i){
int now = rseg[i];
sum += tree[tree[now].ls].sum;
}
for(int i=1;i<=lseg[0];++i){
int now = lseg[i];
sum -= tree[tree[now].ls].sum;
}
if(k<=sum)
{
for(int i=1;i<=rseg[0];++i)
rseg[i]=tree[rseg[i]].ls;
for(int i=1;i<=lseg[0];++i)
lseg[i]=tree[lseg[i]].ls;
return query(tl,mid,k);
}
else
{
for(int i=1;i<=rseg[0];++i)
rseg[i]=tree[rseg[i]].rs;
for(int i=1;i<=lseg[0];++i)
lseg[i]=tree[lseg[i]].rs;
return query(mid + 1,tr,k-sum);
}
}
int get_ans(int l,int r,int k)
{
lseg[0]=rseg[0]=0;
l--;
while(l)
{
lseg[++lseg[0]]=root[l];
l-=lowbit(l);
}
while(r)
{
rseg[++rseg[0]]=root[r];
r-=lowbit(r);
}
return query(1,lsh.size(),k);
}
signed main(){
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++){
scanf("%d",&a[i]);
lsh.push_back(a[i]);
}
for(int i = 1; i <= m; i++){
cin >> s;
if(s[0] == 'Q'){
scanf("%d%d%d",&l,&r,&k);
mes[i].flag = 1;
mes[i].l = l,mes[i].r = r,mes[i].k = k;
}
else {
scanf("%d%d",&l,&k);
mes[i].l = l,mes[i].k = k;
lsh.push_back(k);
}
}
sort(lsh.begin(),lsh.end());
lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());
for(int i = 1; i <= n; i++){
a[i] = lower_bound(lsh.begin(),lsh.end(),a[i]) - lsh.begin() + 1;
}
for(int i = 1; i <= n; i++)add(i,a[i]);
for(int i = 1; i <= m; i++){
if(mes[i].flag){
int ans = get_ans(mes[i].l,mes[i].r,mes[i].k);
printf("%d\n",lsh[ans - 1]);
}
else{
int k = lower_bound(lsh.begin(),lsh.end(),mes[i].k) - lsh.begin() + 1;
change(mes[i].l,k);
}
}
}
可持久化线段树的综合应用
T1(bzoj4504):
兔子们在玩 个串的游戏。首先,它们拿出了一个长度为 的数字序列,选出其中的一个连续子串,然后统计其子串中所有数字之和(注意这里重复出现的数字只被统计一次)。兔子们想知道,在这个数字序列所有连续的子串中,按照以上方式统计其所有数字之和,第 大的和是多少。
关于可持久化线段树的题,我想最关键的的就是寻找问题中的 阶段性条件。即可以从之前的状态一步步推到后面的状态的条件,这一点与动态规划有一定相似之处。在这个题中,我们假设一个数组 表示左端点在区间 中,右端点在 这个位置上时区间的最大和(我们在具体实现的时候采用主席树实现这个功能)。容易发现,由 能够推出 ,只需要加上 这个位置的数即可。通过枚举右端点,就可以如此一层层地推完整个 。
具体实现来说,我们按照右端点的位置为线段树编号。当右端点位置在 时,它对应的版本就是 。 表示第 个版本的线段树的根节点编号。
同时,这个题还要求的是对区间数去重后求和,对此,还需要引入一个 数组记录数 在 位置前最后的出现位置。具体使用参见代码中的注释。主席树中这种pre数组的思想也是很常见的,具体还有以下题目:MEX,Boring Queries。
为了求第 大的和,我们需要优先队列,并引入一个五元组 , 表示区间和, 表示左端点范围, 表示右端点的位置, 表示对应的根节点编号。优先队列每次弹出 最大的一个五元组,对于这个五元组, 的次大值只会存在 ,中,再将它们扔进去即可。反复 次,就可以求出第 大值。
具体实现如下:
#include<bits/stdc++.h>
#define int long long
#define mid (tl + tr >> 1)
using namespace std;
const int MAXN = 3e6;
int tot,n,k,a[MAXN + 5],root[MAXN + 5];
map<int,int> pre;
struct no{
long long v;
int x,l,r,p;
bool operator<(const no a)const{
return this->v < a.v;;
}
no(){}
no(int a,int b,int c,int d,int e){
v =a;
x = b;
l = c;
r = d;
p = e;
}
}tmp;
struct node{
int ls,rs,tag;
pair<int,int> v;
}tree[4 * MAXN + 5];
priority_queue<no> q;
int add(int y,int p){
int x = ++tot;
tree[x] = tree[y];
tree[x].v.first += p;
tree[x].tag += p;
return x;
}
void push_down(int i){
tree[i].ls = add(tree[i].ls,tree[i].tag);
tree[i].rs = add(tree[i].rs,tree[i].tag);
tree[i].tag = 0;
}
int build(int tl,int tr){
int x = ++tot;
tree[x].v = make_pair(0,tl);
if(tl == tr)return x;
tree[x].ls = build(tl,mid);
tree[x].rs = build(mid + 1,tr);
return x;
}
int modify(int i,int tl,int tr,int l,int r,int p){
if(tl >= l && tr <= r)return add(i,p);
if(tree[i].tag)push_down(i);
int x = ++tot;
tree[x] = tree[i];
if(l <= mid)tree[x].ls = modify(tree[i].ls,tl,mid,l,r,p);
if(r > mid)tree[x].rs = modify(tree[i].rs,mid + 1,tr,l,r,p);
tree[x].v = max(tree[tree[x].ls].v,tree[tree[x].rs].v);
return x;
}
pair<int,int> query(int i,int tl,int tr,int l,int r){
if(tl == l && tr == r)return tree[i].v;
if(tree[i].tag)push_down(i);
if(r <= mid)return query(tree[i].ls,tl,mid,l,r);
else if(l > mid)return query(tree[i].rs,mid + 1,tr,l,r);
return max(query(tree[i].ls,tl,mid,l,mid),query(tree[i].rs,mid +1 ,tr,mid + 1,r));
}
void extend(int i,int l,int r){
if(l > r)return;
pair<int,int> t = query(i,1,n,l,r);
q.push(no(t.first,i,l,r,t.second));
}
signed main(){
scanf("%lld%lld",&n,&k);
root[0] = build(1,n);
for(int i = 1; i <= n; i++){
scanf("%lld",&a[i]);
root[i] = modify(root[i - 1],1,n,pre[a[i]] + 1,i,a[i]);
pre[a[i]] = i;
extend(root[i],1,i);
}
while(k--){
tmp = q.top();
q.pop();
extend(tmp.x,tmp.l,tmp.p - 1);
extend(tmp.x,tmp.p + 1,tmp.r);
}
cout << tmp.v << "\n";
}
T2 P2839 [国家集训队]middle
传送门
这里先介绍一个二分求中位数的方法:
对于一个序列 ,我们假设 为这个数列的中位数,另开一个数组 ,如果 ,那么 就赋值为 ,否则赋值为 。之后对整个 数组求和,如果结果大于零,那么说明 小于真正的中位数,如果小于零,那么 大于真正的中位数,当等于零时,就找到了中位数。如此进行二分即可。
基于这个思想来思考这个题。我们同样地引入 数组,二分中位数 。假设求出的最优区间的左端点为 在区间 内,右端点 在区间 内。可见 一定会包含区间 。当我们对 数组中的 区间求和时,也必将会求和 这一段,因此我们需要用线段树来维护区间和。
另一方面,我们还希望求得尽可能大的中位数。那么也就需要在对 数组求出的区间和尽量大,因此还需要维护 数组的区间最大值。当然,对于一个不同的 , 数组是会不同的。因此我们有不同版本的 数组,这里引入可持久化线段树进行维护。当 转为 时,有使 等于 的 ,这些 所对应的 会变为 。如此挨个修改,得到不同版本的 数组。
每一次得到一个 ,我们就在 对应的 数组版本上求含区间 的最大区间和,如果这个和大于 ,说明 还可以更大,否则就将 变小点,如此不断二分,就能得到满足题意的最大中位数。
代码:
using namespace std;
const int MAXN = 5e6;
int tot,n,a[MAXN + 5],root[MAXN + 56],q;
vector<int> lsh;
int que[10],pos[20005][1000];
struct node{
int mx,mn,ls,rs,sum,lsx,rsx,cl,cr;
}tree[4 * MAXN + 5];
void push_up(int i){
tree[i].sum = tree[tree[i].ls].sum + tree[tree[i].rs].sum;
tree[i].lsx = max(tree[tree[i].ls].lsx,tree[tree[i].ls].sum + tree[tree[i].rs].lsx);
tree[i].rsx = max(tree[tree[i].rs].rsx,tree[tree[i].rs].sum + tree[tree[i].ls].rsx);
tree[i].mn = min(tree[tree[i].ls].mn,tree[tree[i].rs].mn);
tree[i].mx = max(tree[tree[i].ls].mx,tree[tree[i].rs].mx);
}
int build(int tl,int tr){
int x = ++tot;
if(tl == tr){
tree[x].sum = tree[x].lsx = tree[x].rsx = 1;
tree[x].mn = tree[x].mx = a[tl];
return x;
}
tree[x].ls = build(tl,mid);
tree[x].rs = build(mid + 1,tr);
push_up(x);
return x;
}
int update(int x,int y,int tl,int tr,int to,int val,bool flag){
if(!flag)x = ++tot;
if(tl == tr){
tree[x].sum = tree[x].lsx = tree[x].rsx = val;
tree[x].mx = -1e9;
tree[x].mn = 1e9;
return x;
}
if(to <= mid){
if(!tree[x].cr)tree[x].rs = tree[y].rs;
if(!tree[x].cl){
tree[x].cl = 1;
tree[x].ls = update(x,tree[y].ls,tl,mid,to,val,0);
}
else tree[x].ls = update(tree[x].ls,tree[y].ls,tl,mid,to,val,1);
}
else{
if(!tree[x].cl)tree[x].ls = tree[y].ls;
if(!tree[x].cr){
tree[x].cr = 1;
tree[x].rs = update(x,tree[y].rs,mid + 1,tr,to,val,0);
}
else tree[x].rs = update(tree[x].rs,tree[y].rs,mid + 1,tr,to,val,1);
}
push_up(x);
return x;
}
node query(int i,int tl,int tr,int l,int r){
node aa,b,ans = {(int)-1e9,(int)1e9,(int)-1e9,(int)-1e9,0,(int)-1e9,(int)-1e9,(int)-1e9,(int)-1e9};
if(l > r)return ans;
if(tl >= l && tr <= r)return tree[i];
if(tl > r || tr < l)return ans;
aa = query(tree[i].ls,tl,mid,l,r),b = query(tree[i].rs,mid + 1,tr,l,r);
ans.lsx = max(aa.lsx,aa.sum + b.lsx);
ans.rsx = max(b.rsx,b.sum + aa.rsx);
ans.sum = aa.sum + b.sum;
ans.mn = min(aa.mn,b.mn);
ans.mx = max(aa.mx,b.mx);
return ans;
}
int Query(int aa,int b,int c,int d){
int l = 0,r = lsh.size() + 1;
node k = query(root[1],1,n,aa,d);
l = k.mn - 1,r = k.mx + 1;
bool flag = 0;
while(l + 1 < r){
int m = (l + r) / 2;
node B = query(root[m],1,n,b + 1,c - 1),A = query(root[m],1,n,aa,b),C = query(root[m],1,n,c,d);
int sum = B.sum + A.rsx + C.lsx;
if(sum >= 0){
flag = 1;
l = m;
}
else r = m;
}
if(!flag)return 0;
return l;
}
int main(){
// freopen("data","r",stdin);
// freopen("ans1","w",stdout);
scanf("%d",&n);
for(int i = 1; i <= n; i++){
scanf("%d",&a[i]);
lsh.push_back(a[i]);
}
sort(lsh.begin(),lsh.end());
lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());
for(int i = 1; i <= n; i++){
a[i] = lower_bound(lsh.begin(),lsh.end(),a[i]) - lsh.begin() + 1;
pos[a[i]][++pos[a[i]][0]] = i;
//pos[a[i]].push_back(i);
}
root[1] = build(1,n);
for(int i = 2; i <= (int)lsh.size(); i++){
for(int j = 1; j <= pos[i - 1][0]; j++){
int to = pos[i - 1][j];
root[i] = update(root[i],root[i - 1],1,n,to,-1,root[i] > 0);
}
}
scanf("%d",&q);
int lastans = 0;
int aa,b,c,d;
for(int i = 1; i <= q; i++){
scanf("%d%d%d%d",&aa,&b,&c,&d);
aa = (aa + lastans) % n;b = (b + lastans) % n;c = (c + lastans) % n;d = (d + lastans) % n;
que[0] = 0;que[1] = aa;que[2] = b;que[3] = c;que[4] = d;
sort(que + 1,que + 5);
aa = que[1],b = que[2],c = que[3],d = que[4];
aa++;b++;c++;d++;
int ans = Query(aa,b,c,d);
if(ans == 0){
cout << "0\n";
lastans = 0;
continue;
}
printf("%d\n",lsh[ans - 1]);
lastans = lsh[ans - 1];
}
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)