【小结】可持久化线段树
可持久化线段树是能维护历史版本的线段树。
可以通过历史版本维护历史版本、区间等信息。
它本质上是线段树的前缀和操作。
可持久化值域线段树也简称主席树。
P3919 【模板】可持久化线段树 1(可持久化数组)
题目大意:单点修改。查询历史版本上的某个值。
首先暴力的想法是可以每一次都修改,然后新建一棵线段树。
但是这样空间和时间都不够。不如暴力。
所以每一次单点修改,考虑有些值的修改是不是多余的?
我们发现:对于一个点,仅有 \(O(logn)\) 个线段树节点与它有关。
所以每一次修改只需要改这 \(logn\) 个点。
考虑动态开点线段树的想法,每一次新建一个节点,然后这个节点,因为是单点修改,肯定会有一边是不变的。
所以只修改另一边。另一边再如此,不断递归下去\(\dots\)
然后直到修改到叶子。就可以停下。每一次修改记录根节点,就可以啦!
然后查询的时候直接从 \(root_{i}\) 查询即可。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
const int N=1e6+10;
int n,m,i,w[N];
int idx;
int root[N];
int ver,op,x,y;
struct Node{
int l,r,lc,rc,val;
}a[N*25];
int build(int l,int r)
{
int p=++idx;
a[p].l=l;
a[p].r=r;
if (l==r)
{
a[p].val=w[l];
return p;
}
int mid=l+r>>1;
a[p].lc=build(l,mid);
a[p].rc=build(mid+1,r);
return p;
}
int update(int k,int x,int v)
{
int l=a[k].l,r=a[k].r;
int p=++idx;
a[p]=a[k];
if (l==r)
{
a[p].val=v;
return p;
}
int mid=l+r>>1;
if (x<=mid) a[p].lc=update(a[k].lc,x,v);
else a[p].rc=update(a[k].rc,x,v);
return p;
}
int query(int k,int x)
{
int l=a[k].l,r=a[k].r;
if (l==r) return a[k].val;
int mid=l+r>>1;
if (x<=mid) return query(a[k].lc,x);
else return query(a[k].rc,x);
}
int main()
{
scan>>n>>m;
for (i=1;i<=n;i++) scan>>w[i];
root[0]=build(1,n);
for (i=1;i<=m;i++)
{
scan>>ver>>op>>x;
if (op==1)
{
scan>>y;
root[i]=update(root[ver],x,y);
}
else
{
printf("%d\n",query(root[ver],x));
root[i]=root[ver];
}
}
return 0;
}
然后就是经典的主席树。
P3834 【模板】可持久化线段树 2
经典的静态区间 \(k\) 大问题。
考虑值域线段树。
先考虑 \(n\) 个数的时候怎么办。
可以直接查询 \(k\) 大(类似线段树二分)。
那么这题加上了区间的限制。
所以可以维护 \(n\) 个历史版本,每次加入一个数,然后查询的时候,类似前缀和的计算区间内数的个数。
如此值域线段树上二分即可。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
const int N=2e5+10,L=33;
int n,m,i,w[N],l,r,K,b[N],k,root[N],idx;
struct Node{
int l,r,sum,lc,rc;
}a[N*L];
void pushup(int k)
{
a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
}
int build(int l,int r)
{
int p=++idx;
a[p].l=l;
a[p].r=r;
if (l==r)
{
a[p].sum=0;
return p;
}
int mid=l+r>>1;
a[p].lc=build(l,mid);
a[p].rc=build(mid+1,r);
pushup(p);
return p;
}
int update(int k,int x,int v)
{
int p=++idx;
a[p]=a[k];
int l=a[p].l,r=a[p].r;
if (l==x&&r==x)
{
a[p].sum+=v;
return p;
}
int mid=l+r>>1;
if (x<=mid) a[p].lc=update(a[k].lc,x,v);
else a[p].rc=update(a[k].rc,x,v);
pushup(p);
return p;
}
int query(int L,int R,int k)
{
int l=a[L].l,r=a[L].r;
if (l==r) return l;
int cnt=a[a[R].lc].sum-a[a[L].lc].sum;
if (k<=cnt) return query(a[L].lc,a[R].lc,k);
else return query(a[L].rc,a[R].rc,k-cnt);
}
int main()
{
scan>>n>>m;
for (i=1;i<=n;i++) scan>>w[i],b[++k]=w[i];
sort(b+1,b+k+1);
k=unique(b+1,b+k+1)-b-1;
root[0]=build(1,k);
for (i=1;i<=n;i++) w[i]=lower_bound(b+1,b+k+1,w[i])-b;
for (i=1;i<=n;i++) root[i]=update(root[i-1],w[i],1);
while (m--)
{
scan>>l>>r>>K;
printf("%d\n",b[query(root[l-1],root[r],K)]);
}
return 0;
}
这启示我们:可持久化线段树可以帮助我们处理区间问题。
P3567 [POI 2014] KUR-Couriers
一个数,在一个区间内,出现次数超过一半,那么也就是出现次数 \(>\lfloor \frac{r-l+1}{2} \rfloor\)。
如果让相同的数连续,那么这个数一定占据了第 \(\lfloor \frac{r-l+1}{2} \rfloor+1\) 个数。所以排序之后也会占据这个位置。
那么直接查询第 \(\lfloor \frac{r-l+1}{2} \rfloor+1\) 小的数。然后只有这个数有可能 成为众数。
最后看一下出现的次数,如果真的满足条件,那么就是。否则整个序列中一定不存在这样的数。
#include<bits/stdc++.h>
using namespace std;
/*
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
const int N=5e5+10,L=24;
int T,n,m,i,w[N];
int root[N],idx;
int l,r,x;
struct Node{
int l,r,sum,lc,rc;
}a[N*L];
void pushup(int k)
{
a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
}
int build(int l,int r)
{
int p=++idx;
a[p].l=l;
a[p].r=r;
if (l==r)
{
a[p].sum=0;
return p;
}
int mid=l+r>>1;
a[p].lc=build(l,mid);
a[p].rc=build(mid+1,r);
pushup(p);
return p;
}
int update(int k,int x,int v)
{
int p=++idx;
int l=a[k].l,r=a[k].r;
a[p]=a[k];
if (l==x&&r==x)
{
a[p].sum+=v;
return p;
}
int mid=l+r>>1;
if (x<=mid) a[p].lc=update(a[k].lc,x,v);
else a[p].rc=update(a[k].rc,x,v);
pushup(p);
return p;
}
int kth(int L,int R,int x)
{
int l=a[L].l,r=a[L].r;
if (l==r) return l;
int cnt=a[a[R].lc].sum-a[a[L].lc].sum;
if (cnt>=x) return kth(a[L].lc,a[R].lc,x);
else return kth(a[L].rc,a[R].rc,x-cnt);
}
int query(int L,int R,int x)
{
int l=a[L].l,r=a[L].r;
if (l==x&&r==x) return a[R].sum-a[L].sum;
int mid=l+r>>1;
if (x<=mid) return query(a[L].lc,a[R].lc,x);
else return query(a[L].rc,a[R].rc,x);
}
int main()
{
scan>>n>>m;
for (i=1;i<=n;i++) scan>>w[i];
root[0]=build(1,n);
for (i=1;i<=n;i++) root[i]=update(root[i-1],w[i],1);
while (m--)
{
scan>>l>>r;
x=kth(root[l-1],root[r],(r-l+1)/2+1);
if (query(root[l-1],root[r],x)>(r-l+1)/2) printf("%d\n",x);
else printf("0\n");
}
return 0;
}
P3963 [TJOI2013] 奖学金
考虑这种题,可以直接枚举中位数。因为中位数显然是数组中的数。
按 \(a\) 从小到大排序,那么要取的也就是前面的 \(\frac{n-1}{2}\) 个和后面的 \(\frac{n-1}{2}\) 个。
考虑前面,后面是对称的。
做法一:主席树
可以主席树查询出 \(x\) 使得 \(x\) 是第 \(\frac{n-1}{2}\) 小的数。然后算出和 \(S\) 和个数 \(C\)。\(C\) 多出的部分都是 \(x\)。所以最后的答案是 \(S-(C-\frac{n-1}{2})\times x\)。
做法二:普通值域线段树
容易发现,这个只用查询前缀或后缀。那么主席树的“前缀和”就失效了。我们只需要这个“前缀”。
所以直接按上面的步骤插入即可。
做法三:平衡树
把做法一查询 \(\frac{n-1}{2}\) 小的部分用平衡树替换。
做法四:优先队列
最简单的做法。
可以直接加入一个数,然后删除最大的那个。在此过程中维护和 \(sum\)。
记录 \(pre_i\) 和 \(nxt_i\) 然后判断即可。
这是代码中的做法。
启示:可持久化线段树擅长处理双区间或单区间类的二位偏序问题。而莫队擅长处理单区间问题。树状数组擅长处理普通的二维偏序问题。
#include<bits/stdc++.h>
using namespace std;
/*
首先按 a[] 从小到大排序。
那么需要选出 n 个数,使得中位数最大,也就等价于:第 (n+1)/2 个数。
那么枚举这个中位数。左边必须要选 n/2 个,右边也要选 n/2 个。
所以问题等价于求:一段前缀的最小的 k 个数的和。
那么可持久化线段树记录前缀的区间和,记录 num 和 sum 两个参数。
然后前后缀线段树二分即可。
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
#define int long long
const int N=2e5+10,L=33,m=1e5;
int n,c,i,root[N],f;
int idx,sum,ans,pre[N],nxt[N];
priority_queue<int> q;
struct Node{
int a,b;
}d[N];
bool cmp(Node x,Node y){return x.a<y.a;}
signed main()
{
scan>>n>>c>>f;
for (i=1;i<=c;i++) scan>>d[i].a>>d[i].b;
sort(d+1,d+c+1,cmp);
for (i=1;i<=c;i++)
{
if (q.size()>n/2) sum-=q.top(),q.pop();
pre[i]=sum;
q.push(d[i].b);
sum+=d[i].b;
}
while (!q.empty()) q.pop();
sum=0;
for (i=c;i>=1;i--)
{
if (q.size()>n/2) sum-=q.top(),q.pop();
nxt[i]=sum;
q.push(d[i].b);
sum+=d[i].b;
}
ans=-1;
for (i=n/2+1;i<=c-n/2;i++)
{
if (pre[i]+nxt[i]+d[i].b<=f) ans=d[i].a;
}
printf("%lld",ans);
return 0;
}
P2468 [SDOI2010] 粟粟的书架
好题。
考虑特殊的数据范围。
对于 \(50\%\) 的数据,\(R,C\leq 200,M\leq 2\times 10^5\)。
对于另外 \(50\%\) 的数据,\(R=1,C\leq 5\times 10^5,M\leq 2\times 10^4\)。
显然,这两组拼起来就是正解。
第一部分:\(R,C\leq 200,M\leq 2\times 10^5\)。
考虑 \(M\) 非常大,看起来只能有一大堆的预处理加上带 \(log\) 的询问。
那么这里有一个常见处理方法。
令 \(sum_{i,j,k}\) 表示 \((1,1)\) 到 \((i,j)\) 中数值 \(\geq k\) 的数之和。
同理,令 \(cnt_{i,j,k}\) 表示 \((1,1)\) 到 \((i,j)\) 中数值 \(\geq k\) 的数的个数。
那么就可以二分 \(mid\) 表示数的下限,然后查询 \(\geq mid\) 的数的和是否 \(\geq H\)。
找到这个数 \(x\)。考虑会有一些相同的值,被多计入到答案中。所以考虑扣除这些数。因为比 \(x\) 大的数都不合法,所以一定是删 \(x\) 且不可能把 \(x\) 全部删光。
设目前的这个和为 \(S\),个数为 \(C\),那么最终的个数为 \(C-\lfloor\frac{S-H}{x}\rfloor\)。
第二部分:\(R=1,C\leq 5\times 10^5,M\leq 2\times 10^4\)。
这部分就是一个一维的数组问题。给出的是数组,查询的是区间。
设要找 \([l,r]\) 中的东西。
可持久化线段树维护第 \(i\) 个版本。
然后直接主席树上二分找出 \(x\)。\(x\) 为最大的使得和大于等于 \(H\) 的数。
我们现在找出了 \(x\)。
那么再用两个函数分别查询 \([l,r]\) 的 \(S\) 和 \(C\)。
这样,公式还是 \(C-\lfloor\frac{S-H}{x}\rfloor\)。
直接输出即可。
#include<bits/stdc++.h>
using namespace std;
/*
HACK:
5 5 1
14 15 9 26 53
58 9 7 9 32
38 46 26 43 38
32 7 9 50 28
8 41 9 7 17
1 2 5 3 139
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
const int N=210,M=1010,K=5e5+10,L=33;
int n,m,q,i,j,k,sx,sy,tx,ty,H;
int sum[N][N][M],cnt[N][N][M],w[N][N],c[K];
int l,r,mid;
int C,S;
int root[K],idx,x;
struct Node{
int l,r,lc,rc,sum,num;
}a[K*L];
int calcsum(int sx,int sy,int tx,int ty,int k)
{
return sum[tx][ty][k]-sum[sx-1][ty][k]-sum[tx][sy-1][k]+sum[sx-1][sy-1][k];
}
int calccnt(int sx,int sy,int tx,int ty,int k)
{
return cnt[tx][ty][k]-cnt[sx-1][ty][k]-cnt[tx][sy-1][k]+cnt[sx-1][sy-1][k];
}
void pushup(int k)
{
a[k].sum=a[a[k].lc].sum+a[a[k].rc].sum;
a[k].num=a[a[k].lc].num+a[a[k].rc].num;
}
int build(int l,int r)
{
int p=++idx;
a[p].l=l;
a[p].r=r;
if (l==r)
{
a[p].sum=a[p].num=0;
return p;
}
int mid=l+r>>1;
a[p].lc=build(l,mid);
a[p].rc=build(mid+1,r);
pushup(p);
return p;
}
int update(int k,int x)
{
int p=++idx;
a[p]=a[k];
int l=a[p].l,r=a[p].r;
if (l==x&&r==x)
{
a[p].sum+=x;
a[p].num++;
return p;
}
int mid=l+r>>1;
if (x<=mid) a[p].lc=update(a[k].lc,x);
else a[p].rc=update(a[k].rc,x);
pushup(p);
return p;
}
int query(int L,int R,int H)
{
int l=a[L].l,r=a[L].r;
if (l==r) return l;
int mid=l+r>>1;
int sum=a[a[R].rc].sum-a[a[L].rc].sum;
if (sum>=H) return query(a[L].rc,a[R].rc,H);
else return query(a[L].lc,a[R].lc,H-sum);
}
int query_cnt(int k,int x,int y)
{
int l=a[k].l,r=a[k].r;
if (l>=x&&r<=y) return a[k].num;
int mid=l+r>>1,res=0;
if (x<=mid) res+=query_cnt(a[k].lc,x,y);
if (y>mid) res+=query_cnt(a[k].rc,x,y);
return res;
}
int query_sum(int k,int x,int y)
{
int l=a[k].l,r=a[k].r;
if (l>=x&&r<=y) return a[k].sum;
int mid=l+r>>1,res=0;
if (x<=mid) res+=query_sum(a[k].lc,x,y);
if (y>mid) res+=query_sum(a[k].rc,x,y);
return res;
}
void work1()
{
n=m;
for (i=1;i<=n;i++) scan>>c[i];
root[0]=build(1,1002);
for (i=1;i<=n;i++) root[i]=update(root[i-1],c[i]);
while (q--)
{
scan>>sx>>l>>tx>>r>>H;
x=query(root[l-1],root[r],H);
C=query_cnt(root[r],x,1002)-query_cnt(root[l-1],x,1002);
S=query_sum(root[r],x,1002)-query_sum(root[l-1],x,1002);
if (S<H)
{
puts("Poor QLW");
continue;
}
printf("%d\n",C-(S-H)/x);
}
}
void work2()
{
for (i=1;i<=n;i++)
{
for (j=1;j<=m;j++) scan>>w[i][j];
}
for (i=1;i<=n;i++)
{
for (j=1;j<=m;j++)
{
for (k=1002;k>=1;k--)
{
sum[i][j][k]=sum[i-1][j][k]+sum[i][j-1][k]-sum[i-1][j-1][k]+(w[i][j]>=k?w[i][j]:0);
cnt[i][j][k]=cnt[i-1][j][k]+cnt[i][j-1][k]-cnt[i-1][j-1][k]+(w[i][j]>=k?1:0);
}
}
}
while (q--)
{
scan>>sx>>sy>>tx>>ty>>H;
l=1;
r=1002;
while (l<=r)
{
mid=l+r>>1;
if (calcsum(sx,sy,tx,ty,mid)>=H) l=mid+1;
else r=mid-1;
}
//现在已经处理出了数量所在的那个数,所以它就可以是删除一些数。现在要来计算这些数。
//那么现在这个数经过二分确定是 r,所以每一次删数必定是减去 r。
//设目前的个数为 C,和为 S,那么答案为 C-(S-H)/r。
if (!r)
{
puts("Poor QLW");
continue;
}
S=calcsum(sx,sy,tx,ty,r);
C=calccnt(sx,sy,tx,ty,r);
printf("%d\n",C-(S-H)/r);
}
}
int main()
{
scan>>n>>m>>q;
if (n==1) work1();
else work2();
return 0;
}
CF893F Subtree Minimum Query
遇到子树距离不超过 \(k\) 这种问题,可以用可持久化线段树。
考虑树上问题转为序列问题。首先这种题都是要转化为 dfn 序。
这样,“\(x\) 子树内” 这个限制就解决了。
只需要转化为“dfn 序在 \([dfn_x,dfn_x+sz_x-1]\)”即可。
接下来就是考虑距离的问题。
如果没有距离,那么可以直接静态区间线段树查询,甚至可以 ST 表查询。但是加了这个限制。
考虑可持久化线段树。按深度加入。令第 \(i\) 个版本表示 \(i\) 深度前的点加入之后的版本。
总共最多加入 \(n\) 次。所以时空复杂度都是 \(O(nlogn)\) 的。
而且因为只查询了子树中的值,所以一定没有深度比它小的点。
可以通过。
特别注意一个细节:在加入的时候一定要每加一个点就新建一个根,然后覆盖掉原来版本的根。绝不能分类讨论是否为第一个点,第一次新建根,后面直接在根上修改。这样不仅不方便(要写两个函数),而且会错误的修改前面版本的值(因为有些部分与前面是公用的)。所以一定要用第一种写法!
再此对比一下。
放上代码:
#include<bits/stdc++.h>
using namespace std;
/*
直接按深度插入。
对于一个深度 dep,查询的就是 [dep,dep+k-1] 的最小值。
*/
struct FSI{
template<typename T>
FSI& operator >> (T &res){
res=0;T f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
res*=f;
return *this;
}
}scan;
const int N=5e5+10,L=33,inf=1e9+10;
int n,m,r,i,w[N],x,y,k;
int last[N],c,dep[N],sz[N];
int dfn[N],stamp;
int idx;
int maxdep,root[N];
int t;
vector<int> G[N];
struct Edge{
int to,next;
}e[N<<1];
struct Node{
int l,r,lc,rc,mi;
}a[N*L];
void add(int u,int v)
{
e[++c]={v,last[u]};
last[u]=c;
}
void dfs(int x,int fa)
{
int i,to;
sz[x]=1;
dfn[x]=++stamp;
dep[x]=dep[fa]+1;
G[dep[x]].push_back(x);
maxdep=max(maxdep,dep[x]);
for (i=last[x];i;i=e[i].next)
{
to=e[i].to;
if (to==fa) continue;
dfs(to,x);
sz[x]+=sz[to];
}
}
void pushup(int k)
{
a[k].mi=min(a[a[k].lc].mi,a[a[k].rc].mi);
}
int build(int l,int r)
{
int p=++idx;
a[p].l=l;
a[p].r=r;
if (l==r)
{
a[p].mi=inf;
return p;
}
int mid=l+r>>1;
a[p].lc=build(l,mid);
a[p].rc=build(mid+1,r);
pushup(p);
return p;
}
int update(int k,int x,int v)
{
int p=++idx;
a[p]=a[k];
int l=a[p].l,r=a[p].r;
if (l==x&&r==x)
{
a[p].mi=min(a[p].mi,v);
return p;
}
int mid=l+r>>1;
if (x<=mid) a[p].lc=update(a[k].lc,x,v);
else a[p].rc=update(a[k].rc,x,v);
pushup(p);
return p;
}
/*
void motify(int k,int x,int v)//千万不要写这个函数!
{
int l=a[k].l,r=a[k].r;
if (l==x&&r==x)
{
a[k].mi=min(a[k].mi,v);
return;
}
int mid=l+r>>1;
if (x<=mid) motify(a[k].lc,x,v);
else motify(a[k].rc,x,v);
pushup(k);
}
*/
int query(int k,int x,int y)
{
int l=a[k].l,r=a[k].r;
if (l>=x&&r<=y) return a[k].mi;
int mid=l+r>>1,res=inf;
if (x<=mid) res=min(res,query(a[k].lc,x,y));
if (y>mid) res=min(res,query(a[k].rc,x,y));
return res;
}
int main()
{
scan>>n>>r;
for (i=1;i<=n;i++) scan>>w[i];
for (i=1;i<n;i++)
{
scan>>x>>y;
add(x,y);
add(y,x);
}
dfs(r,0);
root[0]=build(1,stamp);
for (i=1;i<=maxdep;i++)
{
for (int x:G[i])
{
//这下面就是我错的地方,else 下面不能写 motify。
if (!root[i]) root[i]=update(root[i-1],dfn[x],w[x]);
else root[i]=update(root[i],dfn[x],w[x]);
}
}
scan>>m;
while (m--)
{
scan>>x>>k;
x=(x+t)%n+1;
k=(k+t)%n;
t=query(root[min(dep[x]+k,maxdep)],dfn[x],dfn[x]+sz[x]-1);
printf("%d\n",t);
}
return 0;
}

浙公网安备 33010602011771号