可持久化线段树
1. 可持久化线段树
可持久化数据结构允许我们访问数据结构的历史版本,这对于需要查询过去某个时间点或某个操作前的数据状态的问题非常有用。
可持久化线段树通过在每个节点上保存历史版本的信息,使得我们可以在对数时间内访问到任意历史版本的数据结构。这种数据结构在解决区间修改和查询等问题时具有高效性。
每次拓展点是logn,总时间和空间复杂度是nlogn,可以支持查询区间第k大,方法是差分+线段树二分
2. 树上主席树(可持久化值域线段树的一种应用)
树上主席树,作为可持久化值域线段树的一种应用,主要用于解决树上路径的第k小值等问题。它结合了可持久化线段树和树形结构的特点,能够在高效处理树上路径查询的同时,保持数据结构的历史版本。
对于树上路径的第k小值问题,树上主席树通过在每个节点上构建主席树(即可持久化值域线段树),并利用树上差分等技巧(u+v-2*lca对应的主席树版本)来实现路径询问。
在查询时,我们只需在相关节点对应的主席树上进行二分查找(前缀和<=k的最大值),即可快速找到路径上的第k小值。
本质上就是在区间查询第k大基础上加上一个求LCA,然后进行树上差分求链,对这个链进行线段树二分求解
可持久化线段树板子 P3919,P3834
树上主席树有例题 P2633,P9329
洛谷P9329题解
题意理解:一个树,一些边上有若干边权,对于每一条询问,给出承受值k1,k2,求出路径上最多的边权和小于k1的总数,记为cnt,判断总边权数tot-cnt是否小于k2,若小于则输出k2-(tot-cnt),否则输出-1.
然后就可以套用上面说的树上主席树做法,树剖求LCA,然后以值域为区间,以树点为版本,将边映射到对应的点(每个点的父边对应自己,也就是说根节点没有对应边),对于版本继承的处理,每个边(点)第一次都继承父节点,若这条边有不止一个边权,则继续继承本身创建新的版本
还有一个细节就是对于主席树query部分最后到l==r的时候,要注意当前剩余银币能支付的路径数(也就是k/b[l]下取整)并且与这个节点的cnt数取min,具体见代码
#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define mid (l+(r-l)/2)
using namespace std;
const int N=100010;
int n,m,q;
int b[2*N],lenb;//注意这里离散化数组要开2N倍,因为是q+m个
struct PER{
int s,t;
ll x,y;
}per[N];
struct To{
int v,id;
};
vector<To> g[N];
vector<ll> g2[N];
int size[N],fa[N],son[N],dep[N];
int inl[N];//每条边对应的点
int top[N],dfn[N],tot;
void dfs1(int s,int f){
size[s]=1; fa[s]=f; dep[s]=dep[f]+1;
for(To to:g[s]){
if(to.v==f) continue;
dfs1(to.v,s); inl[to.id]=to.v;
if(size[to.v]>size[son[s]]) son[s]=to.v;
size[s]+=size[to.v];
}
}
void dfs2(int s,int Top){
top[s]=Top; dfn[s]=++tot;
if(son[s]) dfs2(son[s],Top);
for(To to:g[s]){
if(to.v==fa[s]||to.v==son[s]) continue;
dfs2(to.v,to.v);
}
}
int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
return y;
}
struct node{
int ls,rs;
int cnt;
ll sum;
}d[30*N];
int root[N],tot2;
void change(int &p,int v,int l,int r,int pos,ll num){
if(!p) p=++tot2;
d[p]=d[v]; d[p].cnt++; d[p].sum+=num;
if(l==r) return;
if(pos<=mid) d[p].ls=0,change(d[p].ls,d[v].ls,l,mid,pos,num);
else d[p].rs=0,change(d[p].rs,d[v].rs,mid+1,r,pos,num);
}
int query(int x,int y,int lca,int l,int r,ll k){//查找出前缀和小于等于 k的最大值
if(l==r){
ll cnt=d[x].cnt+d[y].cnt-d[lca].cnt*2;
ll sum=d[x].sum+d[y].sum-d[lca].sum*2;//要开ll
if(b[l]==0) return cnt;//注意这里的细节处理
else return min(k/b[l],cnt);//对应剩余银币最多支付数量
}
ll sum=d[d[x].ls].sum+d[d[y].ls].sum-2*d[d[lca].ls].sum;
int cnt=d[d[x].ls].cnt+d[d[y].ls].cnt-2*d[d[lca].ls].cnt;
if(sum>=k) return query(d[x].ls,d[y].ls,d[lca].ls,l,mid,k);
else return query(d[x].rs,d[y].rs,d[lca].rs,mid+1,r,k-sum)+cnt;
//得到可以用银币支付的数量
}
void build(int s){
root[s]=root[fa[s]];
for(ll now:g2[s]){
int pos=lower_bound(b+1,b+lenb+1,now)-b;
int tmp=root[s]; root[s]=0;
change(root[s],tmp,1,lenb,pos,now);
}
for(To to:g[s]){
if(to.v==fa[s]) continue;
build(to.v);
}
}
int main(){
ios::sync_with_stdio(0);
cin>>n>>m>>q;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].pb({v,i});
g[v].pb({u,i});
}
dfs1(1,0);
dfs2(1,1);
for(int i=1;i<=m;i++){
int p,c;
cin>>p>>c;
g2[inl[p]].pb(c);
b[++lenb]=c;
}
for(int i=1;i<=q;i++){
cin>>per[i].s>>per[i].t;
cin>>per[i].x>>per[i].y;
b[++lenb]=per[i].y;
}
sort(b+1,b+lenb+1);
lenb=unique(b+1,b+lenb+1)-(b+1);
build(1);
for(int i=1;i<=q;i++){
int u=per[i].s,v=per[i].t;
int lca=LCA(u,v);
int cost=query(root[u],root[v],root[lca],1,lenb,per[i].y);
int cnt=d[root[u]].cnt+d[root[v]].cnt-2*d[root[lca]].cnt;
int left=per[i].x-(cnt-cost);
if(left<0) cout<<-1<<'\n';
else cout<<left<<'\n';
}
return 0;
}
P4216
一道我用树剖加主席树的在线nlognlogn的做法,感觉针不戳,自己想出了一种不同于题解的优质算法,主席树以时间为版本,以人为区间,其中有些细节,具体看代码
#include<bits/stdc++.h>
#define pb push_back
#define mid (l+(r-l)/2)
using namespace std;
const int N=200010;
int n,q;
vector<int> g[N];
int fa[N],Root;
int dep[N],size[N],son[N];
int top[N],dfn[N],tot;
void dfs1(int s,int f){
dep[s]=dep[f]+1; size[s]=1;
for(int to:g[s]){
//由于是单向边,不会存在父边
dfs1(to,s);
if(size[son[s]]<size[to]) son[s]=to;
size[s]+=size[to];
}
}
void dfs2(int s,int Top){
top[s]=Top; dfn[s]=++tot;
if(son[s]) dfs2(son[s],Top);
for(int to:g[s]){
if(to==son[s]) continue;
dfs2(to,to);
}
}
int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
return y;
}
struct node{
int ls,rs;
int cnt;
}d[30*N];
bool vis[N];
int root[N],tot2;
void change(int &p,int v,int l,int r,int pos){
if(!p) p=++tot2;
d[p]=d[v]; d[p].cnt++;
if(l==r) return;
if(pos<=mid) d[p].ls=0,change(d[p].ls,d[v].ls,l,mid,pos);
else d[p].rs=0,change(d[p].rs,d[v].rs,mid+1,r,pos);
}
int query(int p,int l,int r,int s,int t){
if(s<=l&&r<=t) return d[p].cnt;
int cnt=0;
if(s<=mid) cnt+=query(d[p].ls,l,mid,s,t);
if(t>mid) cnt+=query(d[p].rs,mid+1,r,s,t);
return cnt;
}
int getp(int x,int y,int k){
if(k==0) return 0;
int cnt=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
cnt+=query(root[k],1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
cnt+=query(root[k],1,n,dfn[y],dfn[x]);
return cnt;
}
int main(){
// freopen("s.in","r",stdin);
// freopen("s.out","w",stdout);
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>fa[i];
if(!fa[i]) Root=i;
g[fa[i]].pb(i);//建单向边
}
dfs1(Root,0);
dfs2(Root,1);
cin>>q;
for(int i=1;i<=q;i++){
int op;
cin>>op;
if(op==1){
int u,v,c;
cin>>u>>v>>c;
int lca=LCA(u,v);
root[i]=root[i-1];
cout<<dep[u]+dep[v]-dep[lca]-(dep[lca]-1)<<' '<<getp(u,v,max(0,i-c-1))<<'\n';
//注意细节,因为需要i-p>c, 则p<i-c,又因为维护的主席树是包括当天标记的人的,所以要-1变成p<=i-c-1
//注意lca点处不要统计两遍
}
else{
int t;
cin>>t;
if(vis[dfn[t]]) continue;
vis[dfn[t]]=1;
change(root[i],root[i-1],1,n,dfn[t]);
}
}
return 0;
}

浙公网安备 33010602011771号