20230629-可持久化数据结构 1
20230629
权值线段树
P3369 【模板】普通平衡树
题目大意
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\)棵之间查询即可
注意:
- 由于常数很大,在找mid时需要一个小技巧
mid=(l>>1)+(r>>1)+(l & r & 1) - 我们需要初始化\(hach[0]=1\),再在查询时每一次\(-M\)
- 数组要开大一点啊
- 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
题目大意
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 强制在线)
题目大意
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个操作,有以下两种操作类型:
- A x:添加操作,表示在序列末尾添加一个数 x,序列的长度 N+1。
- 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;
}
总结
- 数组宜大不易小
- long long 中所有出现的地方都要开
- mid=(l>>1)+(r>>1)+(l&r&1);速度很快
- 线段树中计算式最好不要用#define,直接写就行了