20230629-可持久化数据结构 1

发布时间 2023-07-19 22:51:30作者: H_W_Y

20230629

权值线段树

P3369 【模板】普通平衡树

题目大意

传送门
image

Solution

可以用平衡树实现
但用权值线段树代码量更少且速度更快

注意前驱和后继的写法

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define lb(x) lower_bound(b+1,b+cnt+1,x)-b

const int maxn=1e5+10;
int n,tr[maxn*4],cnt,b[maxn],tot=0;
struct node{
  int op,x;
}a[maxn];

namespace SGT{
  #define mid (l+r)/2
  #define lson l,mid,rt<<1
  #define rson mid+1,r,rt<<1|1
  #define lc rt<<1
  #define rc rt<<1|1
  inline void pushup(int rt){tr[rt]=tr[lc]+tr[rc];}
  inline void update(int x,int val,int l=1,int r=cnt,int rt=1){//加入与删除 
  	if(l==r){
  	  tr[rt]+=val;
	  return;	
	}
	if(x<=mid) update(x,val,lson);
	else update(x,val,rson);
	pushup(rt); 
  }
  inline int find_rank(int x,int l=1,int r=cnt,int rt=1){//排名为x的数 
  	if(l==r) return l;
  	if(x<=tr[lc]) return find_rank(x,lson);
  	else return find_rank(x-tr[lc],rson);
  }
  inline int x_rank(int x,int l=1,int r=cnt,int rt=1){//x的排名 
  	if(l==r) return 1;
  	if(x<=mid) return x_rank(x,lson);
  	else return tr[lc]+x_rank(x,rson);
  }
  inline int findr(int l,int r,int rt){
  	if(l==r) return l;
  	if(tr[rc]) return findr(rson);
  	return findr(lson);
  } 
  inline int findl(int l,int r,int rt){
    if(l==r) return l;
    if(tr[lc]) return findl(lson);
    return findl(rson);
  }
  inline int x_pre(int x,int l=1,int r=cnt,int rt=1){//前驱 
  	if(r<x){
  	  if(tr[rt]) return findr(l,r,rt);
	  return 0;	
	}   
  	if(l==r) return l;
	int res=0;
  	//cout<<"pre:"<<l<<' '<<r<<' '<<rt<<' '<<x<<endl;
  	if(x>mid+1&&tr[rc]&&(res=x_pre(x,rson))) return res;
  	else return x_pre(x,lson);
  }
  inline int x_nxt(int x,int l=1,int r=cnt,int rt=1){//后继 
  	if(l>x){
  	  if(tr[rt]) return findl(l,r,rt);
	  return 0;	
	}  
  	if(l==r) return l;
	int res=0;
  	if(x<mid&&tr[lc]&&(res=x_nxt(x,lson))) return res;
  	else return x_nxt(x,rson);
  }
}
using namespace SGT;

int main(){
  /*2023.6.29 H_W_Y P3369 普通平衡树  权值线段树*/ 
  //freopen("P3369_4.in","r",stdin);
  //freopen("P3369.out","w",stdout);
  scanf("%d",&n);
  for(int i=1;i<=n;i++){
  	scanf("%d%d",&a[i].op,&a[i].x);
	if(a[i].op!=4) b[++tot]=a[i].x;
  }
  sort(b+1,b+tot+1);
  cnt=unique(b+1,b+tot+1)-b-1;
  //for(int i=1;i<=cnt;i++) cout<<i<<":"<<b[i]<<endl;
  for(int i=1;i<=n;i++){
  	if(a[i].op==1) update(lb(a[i].x),1);
  	if(a[i].op==2) update(lb(a[i].x),-1);
  	if(a[i].op==3) printf("%d\n",x_rank(lb(a[i].x)));
  	if(a[i].op==4) printf("%d\n",b[find_rank(a[i].x)]);
  	if(a[i].op==5) printf("%d\n",b[x_pre(lb(a[i].x))]);
  	if(a[i].op==6) printf("%d\n",b[x_nxt(lb(a[i].x))]);
  }
  return 0;
}

P1486 [NOI2004] 郁闷的出纳员

题目大意

传送门
第一行有两个非负整数n和min。n表示下面有多少条命令,min表示工资下界。
接下来的n行,每行表示一条命令。命令可以是以下四种之一:
名称 格式 作用
I命令 I_k 新建一个工资档案,初始工资为k。如果某员工的初始工资低于工资下界,他将立刻离开公司。
A命令 A_k 把每位员工的工资加上k
S命令 S_k 把每位员工的工资扣除k
F命令 F_k 查询第k多的工资
_(下划线)表示一个空格,I命令、A命令、S命令中的k是一个非负整数,F命令中的k是一个正整数。
在初始时,可以认为公司里一个员工也没有。

Solution

考虑用权值线段树来维护每一个出现的次数
发现其值域并不大
所以不用离散化

对于每一个A和S命令
我们考虑对min进行修改
而在下一次加入某个工资时
我们再进行之前的修改后加入即可
为了防止变成负数
我们可以统一加一个较大的数来维护

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;

const int maxn=4e5+10,inf=2e5;
int n,limit,tr[maxn*4],x,tag[maxn*4],val,cnt=0;
char ch[10];

namespace SGT{
  #define mid (l+r)/2
  #define lson l,mid,rt<<1
  #define rson mid+1,r,rt<<1|1
  #define lc rt<<1
  #define rc rt<<1|1
  inline void pushup(int rt){tr[rt]=tr[lc]+tr[rc];}
  inline void pushdown(int rt){
    tr[lc]=tr[rc]=0;
    tag[lc]=tag[rc]=1;
    tag[rt]=0;
  }
  inline void update(int x,int l=1,int r=maxn-10,int rt=1){
  	if(l==r){
	  tr[rt]+=1;
	  return;  
	}
	if(tag[rt]) pushdown(rt);
	if(x<=mid) update(x,lson);
	else update(x,rson);
	pushup(rt);
  } 
  inline void del(int a,int b,int l=1,int r=maxn-10,int rt=1){
  	if(a<=l&&b>=r){
  	  tag[rt]=1;
  	  tr[rt]=0;
  	  return;
	}
	if(tag[rt]) pushdown(rt);
	if(a<=mid&&tr[lc]) del(a,b,lson);
	if(b>mid&&tr[rc]) del(a,b,rson);
	pushup(rt);
  }
  inline int find_rank(int x,int l=1,int r=maxn-10,int rt=1){
  	if(l==r) return l;
  	//cout<<l<<' '<<r<<' '<<rt<<' '<<x<<' '<<tr[rt]<<endl;
  	if(tag[rt]) pushdown(rt);
  	if(x<=tr[rc]) find_rank(x,rson);
  	else find_rank(x-tr[rc],lson);
  }
}
using namespace SGT;

int main(){
  /*2023.6.29 H_W_Y P1486 [NOI2004] 郁闷的出纳员 权值线段树*/ 
  //freopen("P1486_1.in","r",stdin);
  scanf("%d%d",&n,&limit);
  limit+=inf;val=0;
  for(int i=1;i<=n;i++){
  	scanf("%s%d",ch,&x);
  	if(ch[0]=='I'){
  	  x=x-val+inf;
  	  if(x>=limit) update(x),cnt++;//只有进去了cnt才加
  	  //cout<<"x="<<x<<endl;
  	  
	}
	if(ch[0]=='A'||ch[0]=='S'){
	  if(ch[0]=='S') x=-x;
	  limit-=x;
	  val+=x;
	  if(ch[0]=='S'&&limit>0) del(1,limit-1);
	  //cout<<"limit="<<limit<<endl;
	  //cout<<"val="<<val<<endl;
	} 
	if(ch[0]=='F'){
	  if(tr[1]<x) printf("-1\n");
	  else printf("%d\n",find_rank(x)-inf+val);
	}
  }
  printf("%d\n",cnt-tr[1]);
  return 0;
}

主席树(可持久化线段树)

CF707D Persistent Bookcase

题目大意

传送门
维护一个二维零一矩阵(\(n,m \le 1000\)),支持四种操作(不超过\(10^5\)次):
\((i,j)\)置零
\((i,j)\)置一
将第\(i\)行零一反转
回到第\(K\)次操作前的状态
每次操作后输出全局一共有多少个一

Solution

利用可持久化数据结构来维护
对于前三种修改
我们每一次都拷贝一次之前的那一行
再进行修改即可

我们再维护一个数组\(b[i][j]\)表示第\(i\)个版本对应的是哪些行
\(b[i]\)可以由\(b[i-1]\)转移过来
而对于第四种操作
直接将\(b[k]\)拷贝过来即可

最后再维护每一个版本的\(sum[i]\)记录答案
但是这样会MLE(所以不要总相信PPT

我们考虑把这些操作建立到一张图上
如果\(i\)版本是由\(k\)版本转移过来
我们就建立一条\(k \to i\) 的边
最后进行dfs统计每一次的答案
深搜遍历每一条边,边搜边记录即可

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;

const int maxn=1e5+10;
int n,m,q,cnt=0,head[maxn],sum[maxn],tot=0,res=0,op[maxn],x[maxn],y[maxn],s[maxn];
struct edge{
  int nxt,v;
}e[maxn*10];
bool vis[1005][1005],rev[1005];

void add(int u,int v){
  e[++tot]=(edge){head[u],v};
  head[u]=tot;
}

void dfs(int u,int res){
  int tmp=0,i=u;
  if(op[u]==1)
    if(vis[x[i]][y[i]]==rev[x[i]]) res++,tmp=1,vis[x[i]][y[i]]=!rev[x[i]],s[x[i]]++;
  if(op[u]==2)
    if(vis[x[i]][y[i]]==!rev[x[i]]) res--,tmp=1,vis[x[i]][y[i]]=rev[x[i]],s[x[i]]--;
  if(op[u]==3) res+=m-s[x[i]]-s[x[i]],rev[x[i]]^=1,s[x[i]]=m-s[x[i]];
  sum[u]=res;
  //cout<<"hi:"<<u<<' '<<res<<endl;
  for(int i=head[u];i;i=e[i].nxt) dfs(e[i].v,res);i=u;
  if(op[u]==1&&tmp) res--,vis[x[i]][y[i]]=rev[x[i]],s[x[i]]--;
  if(op[u]==2&&tmp) res--,vis[x[i]][y[i]]=!rev[x[i]],s[x[i]]++;
  if(op[u]==3) res-=m-s[x[i]]-s[x[i]],rev[x[i]]^=1,s[x[i]]=m-s[x[i]];
}

int main(){
  /*2023.6.29 H_W_Y CF707D Persistent Bookcase 可持久化数据结构*/ 
  scanf("%d%d%d",&n,&m,&q);
  for(int i=1;i<=q;i++){
  	scanf("%d%d",&op[i],&x[i]);
  	if(op[i]<3) scanf("%d",&y[i]);
    if(op[i]==4) add(x[i],i);
    else add(i-1,i);
  }
  dfs(0,0);
  for(int i=1;i<=q;i++) printf("%d\n",sum[i]);
  return 0;
}

BZOJ3207 花神的嘲讽计划 I

题目大意

传送门
给你N,M,K,长度为N的串,M次询问,每次给你一个区间[x,y]和一个长度为K的串,问你询问串是否在区间[x,y]中出现过。

Solution

这里长度为\(k\)的串是子串
那我们考虑把原序列中每一个长度为\(k\)的哈希
然后维护一棵主席树
里面是前\(i\)个的子串
每一次查询直接在第\(r\)棵和第\(l+k-1\)棵之间查询即可

注意:

  1. 由于常数很大,在找mid时需要一个小技巧
    mid=(l>>1)+(r>>1)+(l & r & 1)
  2. 我们需要初始化\(hach[0]=1\),再在查询时每一次\(-M\)
  3. 数组要开大一点啊
  4. ull的最大值是\(2^{64}-1\)而不是\(2^{64}\)
H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long

const int maxn=1e6+10;
int n,m,k,a[maxn],rt[maxn],tr[maxn*10],tot=0,ch[maxn*10][2];
ull M=1ull,p=29ull,hach[maxn];
const ull inf=18446744073709551615ull;//最大是2^64-1 

void update(int &rt,int pre,ull l,ull r,ull x){
  rt=++tot;
  ch[rt][0]=ch[pre][0];
  ch[rt][1]=ch[pre][1];
  tr[rt]=tr[pre]+1;
  if(l==r){
  	tr[rt]++;
  	return ;
  }
  ull mid=(l>>1)+(r>>1)+(l&r&1);
  if(x<=mid) update(ch[rt][0],ch[pre][0],l,mid,x);
  else update(ch[rt][1],ch[pre][1],mid+1,r,x);
}

int query(int x,int y,ull l,ull r,ull val){
  if(l==r) return tr[x]-tr[y];
  ull mid=(l>>1)+(r>>1)+(l&r&1);
  if(val<=mid) return query(ch[x][0],ch[y][0],l,mid,val);
  else return query(ch[x][1],ch[y][1],mid+1,r,val);
}

int main(){
  /*2023.6.30 H_W_Y BZOJ3207 花神的嘲讽计划 I 可持久化线段树*/ 
  scanf("%d%d%d",&n,&m,&k);
  hach[0]=1; 
  for(int i=1;i<=n;i++) scanf("%d",&a[i]);
  for(int i=1;i<=k;i++) M=M*p;
  for(int i=1;i<=n;i++) hach[i]=hach[i-1]*p+(ull)a[i];
  for(int i=k;i<=n;i++) update(rt[i],rt[i-1],0,inf,hach[i]-hach[i-k]*M);
  for(int i=1;i<=m;i++){
  	int l,r;ull x,s=1;
  	scanf("%d%d",&l,&r);
  	for(int j=1;j<=k;j++) scanf("%ulld",&x),s=s*p+x;
  	s-=M;
  	if(r-l+1<k) printf("Yes\n");
  	else if(query(rt[r],rt[l+k-2],0,inf,s)) printf("No\n");
  	else printf("Yes\n");
  }
  return 0;
}

P3567 [POI2014] KUR-Couriers

题目大意

传送门
image

Solution

一道可持久化线段树(主席树)的模板题

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define mid ((l+r)>>1)
#define ls tr[i].lson
#define rs tr[i].rson
#define Ls tr[j].lson
#define Rs tr[j].rson

const int maxn=5e5+10;
int n,m,a[maxn],b[maxn],cnt=0,len,rt[maxn];
struct node{
  int val,lson,rson;
}tr[maxn*20];

void update(int &i,int j,int l,int r,int x){
  i=++cnt;
  tr[i]=tr[j];
  tr[i].val++;
  if(l==r) return ;
  if(x<=mid) update(ls,Ls,l,mid,x);
  else update(rs,Rs,mid+1,r,x);
} 

int query(int i,int j,int l,int r,int k){
  if(l==r) return tr[i].val>k?l:0;
  int s=tr[ls].val-tr[Ls].val,x=tr[rs].val-tr[Rs].val;
  if(s>k) return query(ls,Ls,l,mid,k);
  else if(x>k) return query(rs,Rs,mid+1,r,k);
  return 0;
}

int main(){
  /*2023.3.4 hewanying P3567 KUR-Couriers 可持久化线段树*/ 
  scanf("%d%d",&n,&m);
  for(int i=1;i<=n;i++) scanf("%d",&a[i]),b[i]=a[i];
  sort(b+1,b+n+1);
  len=unique(b+1,b+n+1)-b-1;
  rt[0]=0;b[0]=0;
  for(int i=1;i<=n;i++) update(rt[i],rt[i-1],1,len,lower_bound(b+1,b+len+1,a[i])-b);
  for(int i=1,l,r;i<=m;i++){
  	scanf("%d%d",&l,&r);
  	int k=(r-l+1)/2;
  	printf("%d\n",b[query(rt[r],rt[l-1],1,len,k)]);
  }
  return 0;
}

SPOJ10628 Count on a tree(P2633 强制在线)

题目大意

传送门
image

Solution

看到要求第\(k\)小的
不难想到是权值线段树
维护\(u \to v\)路径上的点
我们可以在把权值线段树可持久化
这样每个节点维护它到根节点的路径上的权值
最后询问\(rt[u]+rt[v]-rt[lca(u,v)]-rt[fa[lca(u,v)]\)即可
计算式最好不要用#define——后果就是调一天!

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;
#define lb(x) lower_bound(b+1,b+len+1,x)-b

const int maxn=1e5+10;
int n,m,a[maxn],tr[maxn*100],ch[maxn*100][2],tot=0,u,v,b[maxn];
int head[maxn],cnt=0,fa[maxn],dep[maxn*10],f[maxn][20],k,len,lastans=0;
struct edge{
  int nxt,v;
}e[maxn*2];

inline int read(){
  int x=0,ff=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') ff=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*ff;
}

inline void add(int u,int v){
  e[++tot]=(edge){head[u],v};
  head[u]=tot;
  e[++tot]=(edge){head[v],u};
  head[v]=tot;
} 

int rt[maxn];
namespace SGT{
  #define mid (l+r)/2
  //#define lc tr[ch[u][0]]+tr[ch[v][0]]-tr[ch[x][0]]-tr[ch[y][0]] 不要用啊
  inline void update(int &rt,int pre,int x,int l=1,int r=len){
  	rt=++cnt;
  	ch[rt][0]=ch[pre][0];
  	ch[rt][1]=ch[pre][1];
  	tr[rt]=tr[pre]+1;
  	if(l==r) return;
	if(x<=mid) update(ch[rt][0],ch[pre][0],x,l,mid);
	else update(ch[rt][1],ch[pre][1],x,mid+1,r);
  }
  inline int query(int u,int v,int x,int y,int k,int l=1,int r=len){
  	if(l==r) return l;
  	int lc=tr[ch[u][0]]+tr[ch[v][0]]-tr[ch[x][0]]-tr[ch[y][0]];//直接定义啊
  	if(k<=lc) query(ch[u][0],ch[v][0],ch[x][0],ch[y][0],k,l,mid);
  	else query(ch[u][1],ch[v][1],ch[x][1],ch[y][1],k-lc,mid+1,r);
  }
}

inline void dfs(int u,int ff){
  dep[u]=dep[ff]+1;
  fa[u]=ff;f[u][0]=ff;
  SGT::update(rt[u],rt[fa[u]],lb(a[u]));
  for(int i=1;i<=19;i++) f[u][i]=f[f[u][i-1]][i-1];
  for(int i=head[u];i;i=e[i].nxt){
  	int v=e[i].v;
  	if(v==ff) continue;
  	dfs(v,u);
  }
}

inline int LCA(int u,int v){
  if(dep[u]<dep[v]){
  	int t=u;
  	u=v;v=t;
  }
  for(int i=19;i>=0;i--){
  	if(dep[f[u][i]]>=dep[v]) u=f[u][i];
  	if(u==v) return u;
  }
  if(u==v) return u;
  for(int i=19;i>=0;i--)
    if(f[u][i]!=f[v][i]){
      u=f[u][i];
      v=f[v][i];
	}
  return f[u][0];
}

int main(){
  /*2023.7.1 H_W_Y SPOJ10628 Count on a tree 可持久化线段树*/ 
  n=read();m=read();
  for(int i=1;i<=n;i++) a[i]=read(),b[i]=a[i];
  sort(b+1,b+n+1);
  len=unique(b+1,b+n+1)-b-1;
  for(int i=1;i<n;i++){
  	u=read();v=read();
  	add(u,v);
  }
  dfs(1,0);
  for(int i=1;i<=m;i++){
  	u=read();v=read();k=read();
  	int lca=LCA(u,v);
  	printf("%d\n",b[SGT::query(rt[u],rt[v],rt[lca],rt[fa[lca]],k)]);
  }
  return 0;
}

可持久化Trie

P4735 最大异或和

题目大意

传送门
给定一个非负整数序列 {a},初始长度为 N。
有 M个操作,有以下两种操作类型:

  1. A x:添加操作,表示在序列末尾添加一个数 x,序列的长度 N+1。
  2. Q l r x:询问操作,你需要找到一个位置 p,满足 l<=p<=r,使得:

\(a[p] \oplus a[p+1] \oplus \dots \oplus a[N] \oplus x\) 最大,输出最大是多少。

Solution

一道可持久化Trie的模板题

H_W_Y-Coding
#include <bits/stdc++.h>
using namespace std;

const int maxn=6e5+10;
int n,m,sum[maxn],x,l,r,cnt=0;
int ch[maxn*25][2],ver[maxn*25],root[maxn];
char s;

void add(int nw,int pre,int x){
  ver[nw]=x;
  for(int i=23;i>=0;i--){
  	int c=(sum[x]>>i)&1;
    ch[nw][!c]=ch[pre][!c];
    ch[nw][c]=++cnt;
    nw=ch[nw][c];pre=ch[pre][c];
    ver[nw]=x;
  }
}

int query(int nw,int limit,int val){
  int res=0;
  for(int i=23;i>=0;i--){
    int c=(val>>i)&1;
    if(ver[ch[nw][!c]]>=limit) nw=ch[nw][!c],res+=(1<<i);
    else nw=ch[nw][c];
  }
  return res;
}

int main(){
  /*2023.5.1 hewanying P4735 最大异或和 可持久化字典树*/ 
  scanf("%d%d",&n,&m);
  ver[0]=-1;
  root[0]=++cnt;
  add(root[0],0,0);
  for(int i=1;i<=n;i++){
  	scanf("%d",&x);
  	root[i]=++cnt;
  	sum[i]=sum[i-1]^x;
  	add(root[i],root[i-1],i);
  }
  for(int i=1;i<=m;i++){
    scanf(" %c",&s);
    if(s=='A'){
      scanf("%d",&x);
      root[++n]=++cnt;
	  sum[n]=sum[n-1]^x;
      add(root[n],root[n-1],n);
	}
	else{
	  scanf("%d%d%d",&l,&r,&x);
	  printf("%d\n",query(root[r-1],l-1,x^sum[n]));
	}
  }
  return 0;
}

总结

  1. 数组宜大不易小
  2. long long 中所有出现的地方都要开
  3. mid=(l>>1)+(r>>1)+(l&r&1);速度很快
  4. 线段树中计算式最好不要用#define,直接写就行了