12.9日记
对顶堆
功能:动态维护区间第k大,支持插入和删除。小根堆储存大数,大根堆储存小数。
- P1801:插入+输出第k大。
#include<bits/stdc++.h> using namespace std; const int M=2e5+20; int a[M]; priority_queue<int> qb; priority_queue<int,vector<int>,greater<int> > qs; inline void operate(int num){ while(qb.size()<num) qb.push(qs.top()),qs.pop(); while(qb.size()>num) qs.push(qb.top()),qb.pop(); } inline void insert(int x){ if (!qs.empty()&&x>qs.top()) qs.push(x); else qb.push(x); } int main(){ int m,n; scanf("%d%d",&m,&n); for(int i=1;i<=m;++i) scanf("%d",&a[i]); int q=0,p=0; for(int i=1;i<=n;++i){ int ca; scanf("%d",&ca),++q; while(p<ca) insert(a[++p]); operate(q); printf("%d\n",qb.top()); } return 0; }
主席树
- P1801:
拿大炮打苍蝇……
#include<bits/stdc++.h> using namespace std; #define mid (l+r)/2 const int M=8e6+20,Mm=2e5+20; int cnt,a[Mm],b[Mm],v[M],L[M],R[M],root[Mm]; unordered_map<int,int> rev; int build(int l,int r){ int rt=++cnt; v[rt]=0; if (l==r) return rt; build(l,mid),build(mid+1,r); return rt; } int operate(int idp,int l,int r,int pos,int x){ int rt=++cnt; L[rt]=L[idp],R[rt]=R[idp],v[rt]=v[idp]+x; if (l==r) return rt; if (pos<=mid) L[rt]=operate(L[idp],l,mid,pos,x); else R[rt]=operate(R[idp],mid+1,r,pos,x); return rt; } int query(int lid,int nid,int l,int r,int k){ if (l==r) return l; int Lnum=v[L[nid]]-v[L[lid]]; if (Lnum>=k) return query(L[lid],L[nid],l,mid,k); else return query(R[lid],R[nid],mid+1,r,k-Lnum); } int main(){ int m,n; scanf("%d%d",&m,&n); for(int i=1;i<=m;++i) scanf("%d",&a[i]),b[i]=a[i]; sort(b+1,b+m+1); int len=unique(b+1,b+m+1)-(b+1); for(int i=1;i<=len;++i) rev[b[i]]=i; root[0]=build(1,len); int p=0; for(int i=1;i<=n;++i){ int ca; scanf("%d",&ca); while(p<ca) root[p+1]=operate(root[p],1,len,rev[a[p+1]],1),++p; printf("%d\n",b[query(root[0],root[ca],1,len,i)]); } return 0; }
- P3919:可持久化数组
构造:叶子节点val表示对应下标的值为val,其他节点没用。
功能:在历史版本基础上单点修改,单点查询某一版本的值。
#include<bits/stdc++.h> using namespace std; #define mid (l+r)/2 const int M=3.2e7+10,Mm=1e6+10; int cnt,root[Mm],a[Mm]; struct Tree{ int l,r,val; Tree(int a=0,int b=0,int c=0):l(a),r(b),val(c){} }v[M]; int build(int l,int r){ int rt=++cnt; if (l==r){ v[rt].val=a[l]; return rt; } v[rt].l=build(l,mid); v[rt].r=build(mid+1,r); return rt; } int operate(int idp,int l,int r,int pos,int x){ int rt=++cnt; v[rt]=v[idp]; if (l==r){ v[rt].val=x; return rt; } if (pos<=mid) v[rt].l=operate(v[idp].l,l,mid,pos,x); else v[rt].r=operate(v[idp].r,mid+1,r,pos,x); return rt; } int query(int idp,int l,int r,int pos){ if(l==r) return v[idp].val; if(pos<=mid) return query(v[idp].l,l,mid,pos); else return query(v[idp].r,mid+1,r,pos); } int main(){ int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) scanf("%d",&a[i]); root[0]=build(1,n); int ver=0; for(int i=1;i<=m;++i){ int num,op; scanf("%d%d",&num,&op); if (op==1){ int a,b; scanf("%d%d",&a,&b); root[++ver]=operate(root[num],1,n,a,b); } else{ int a; scanf("%d",&a); printf("%d\n",query(root[num],1,n,a)); root[++ver]=root[num]; } } return 0; }
- P3835:可持久化平衡树。对各个以往历史版本进行,插入,删除(可能不存在),查询x排名(可能不存在),查询排名为x的数,求x前驱(可能不存在),求x后继(可能不存在)。
构造:正常的值域线段树,节点表示当前区间数的个数。
注意:一定要注意可能不存在的情况!!!目前来看,值域线段树可以做到一下几点
- 查询值=pos的数有几个
- 查询值<pos的数有几个。以上两个结合可以知道各种<=,<,>,>=等等的数有几个。
- 查询排名为k的数是谁。
如果想再维护一个最大值最小值的话应该也不麻烦。
#include<bits/stdc++.h> using namespace std; #define mid ((l+r)>>1) const int M=5e5+20; int cnt,len,rk,num; struct Tree{ int l,r,cnt; Tree(int a=0,int b=0,int c=0):l(a),r(b),cnt(c){} }v[55*M]; int build(int l,int r){ int rt=++cnt; if (l==r) return rt; v[rt].l=build(l,mid),v[rt].r=build(mid+1,r); return rt; } int query_num(int idp,int l,int r,int pos){//查询值为pos的数有几个 if (l==r) return v[idp].cnt; if (pos<=mid) return query_num(v[idp].l,l,mid,pos); else return query_num(v[idp].r,mid+1,r,pos); } int operate(int idp,int l,int r,int pos,int x){ int rt=++cnt; v[rt]=v[idp],v[rt].cnt+=x; if (l==r) return rt; if (pos<=mid) v[rt].l=operate(v[idp].l,l,mid,pos,x); else v[rt].r=operate(v[idp].r,mid+1,r,pos,x); return rt; } int query_rk(int idp,int l,int r,int pos){//查询比pos小的数有几个 if (l==r) return 0; if (pos<=mid) return query_rk(v[idp].l,l,mid,pos); else return query_rk(v[idp].r,mid+1,r,pos)+v[v[idp].l].cnt; } int query_sa(int idp,int l,int r,int k){//查询排名为k的数 int &Lnum=v[v[idp].l].cnt; if (l==r) return l; if (Lnum>=k) return query_sa(v[idp].l,l,mid,k); else return query_sa(v[idp].r,mid+1,r,k-Lnum); } struct Opt{ int ver,op,x; Opt(int a=0,int b=0,int c=0):ver(a),op(b),x(c){} }opt[M]; int lsh[M],root[M]; unordered_map<int,int> rev; int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d%d%d",&opt[i].ver,&opt[i].op,&opt[i].x),lsh[i]=opt[i].x; sort(lsh+1,lsh+n+1); len=unique(lsh+1,lsh+n+1)-(lsh+1); for(int i=1;i<=len;++i) rev[lsh[i]]=i; root[0]=build(1,len); for(int i=1;i<=n;++i) if (opt[i].op==1) root[i]=operate(root[opt[i].ver],1,len,rev[opt[i].x],1); else if (opt[i].op==2){ if (query_num(root[opt[i].ver],1,len,rev[opt[i].x])) root[i]=operate(root[opt[i].ver],1,len,rev[opt[i].x],-1); else root[i]=root[opt[i].ver]; } else if (opt[i].op==3){ root[i]=root[opt[i].ver]; printf("%d\n",query_rk(root[opt[i].ver],1,len,rev[opt[i].x])+1); } else if (opt[i].op==4){ root[i]=root[opt[i].ver]; printf("%d\n",lsh[query_sa(root[opt[i].ver],1,len,opt[i].x)]); } else if (opt[i].op==5){ root[i]=root[opt[i].ver]; rk=query_rk(root[opt[i].ver],1,len,rev[opt[i].x]); if (rk==0) printf("-2147483647\n"); else printf("%d\n",lsh[query_sa(root[opt[i].ver],1,len,rk)]); } else if (opt[i].op==6){ root[i]=root[opt[i].ver]; rk=query_rk(root[opt[i].ver],1,len,rev[opt[i].x]); num=query_num(root[opt[i].ver],1,len,rev[opt[i].x]); if (rk==v[root[opt[i].ver]].cnt||(rk==v[root[opt[i].ver]].cnt-1&&num)) printf("2147483647\n"); else if (num) printf("%d\n",lsh[query_sa(root[opt[i].ver],1,len,rk+2)]); else printf("%d\n",lsh[query_sa(root[opt[i].ver],1,len,rk+1)]); } return 0; }
动态开点线段树
- P1908:求逆序对个数
思路:访问之前先看有没有,如果没有就加上,询问的时候如果没有就直接0。
注意:
- cnt初始化必须是1,不然当场爆炸。
- M能开多大就开多大。
#include<bits/stdc++.h> using namespace std; const int M=1e7+10; #define mid ((l+r)>>1) struct Tree{ int l,r,val; Tree(int a=0,int b=0,int c=0):l(a),r(b),val(c){} }v[M]; int cnt=1;//千万别忘了!!! void operate(int &id,int l,int r,int pos,int x){ if (!id)// id=++cnt;// v[id].val+=x; if (l==r) return; if (pos<=mid) operate(v[id].l,l,mid,pos,x); else operate(v[id].r,mid+1,r,pos,x); } int query(int id,int l,int r,int ql,int qr){ if (!id)// return 0;// if (ql<=l&&r<=qr) return v[id].val; int sum=0; if (ql<=mid) sum+=query(v[id].l,l,mid,ql,qr); if (mid<qr) sum+=query(v[id].r,mid+1,r,ql,qr); return sum; } int main(){ int n,root=1; scanf("%d",&n); long long ans=0; for(int i=1;i<=n;++i){ int c; scanf("%d",&c); operate(root,1,1e9,c,1); ans+=query(1,1,1e9,c+1,1e9); } printf("%lld\n",ans); return 0; }
- CF915E:待补,据说是动态开点线段树,lazy标记处理如下:(很抱歉忘了是哪个博主的博客了……如有侵权会立刻删除)
inline void pushdown(int now,int l,int r) { if(lazy[now]==-1) return; int k=lazy[now],m=(l+r)>>1; if(!lson[now]) lson[now]=++tot; sum[lson[now]]=k*(m-l+1); lazy[lson[now]]=lazy[now]; if(!rson[now]) rson[now]=++tot; sum[rson[now]]=k*(r-(m+1)+1); lazy[rson[now]]=lazy[now]; lazy[now]=-1; }
并查集
- P3367:合并+询问是否在同一集合。
路径压缩很容易理解,这里再用一下按秩合并,后面可持久化并查集需要用。记录每个并查集的大小(或者说是并查集关系树的树高(最下面的节点是1,表示这个节点的儿子找到他需要经过的最长步数,或者说是最深的儿子)。然后其实也比较简单据说如果两个都用的话可以达到线性。
#include<bits/stdc++.h> using namespace std; const int M=2e5+10; int fa[M],rk[M]; int find(int x){ return fa[x]==x?x:find(fa[x]);//return fa[x]==x?x:fa[x]=find(fa[x]);这是换成路径压缩 } void merge(int x,int y){ x=find(x),y=find(y); if (x!=y) if (rk[x]<=rk[y]) fa[x]=y,rk[y]=max(rk[y],rk[x]+1); else fa[y]=x,rk[x]=max(rk[x],rk[y]+1); } int main(){ int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) fa[i]=i,rk[i]=1; for(int i=1;i<=m;++i){ int op,x,y; scanf("%d%d%d",&op,&x,&y); if (op==1) merge(x,y); else printf("%c\n",find(x)==find(y)?'Y':'N'); } return 0; }
- P3402:可持久化并查集
思路基本一样,就是把fa和rk树上放在可持久化数组上搞,这样每次修改或者询问数组上的值就是\(O(\log n)\)的,所以不能用路径压缩(因为每次find的都是都要进行一大堆修改操作,完全可以构造数据卡掉)。
看一些题解,感觉部分算法不能可持久化的原因是时间复杂度是均摊的。如果再加上一个log,那么均摊之后很有可能就不是原先的复杂度了。
总之这个模板还是非常有用的(感觉)。
#include<bits/stdc++.h> using namespace std; #define mid ((l+r)>>1) const int M=2e5+20; int cnt,root[M],n; struct Tree{ int l,r,fa,rk; Tree(int a=0,int b=0,int c=0,int d=0):l(a),r(b),fa(c),rk(d){} }v[36*M]; int build(int l,int r){ int rt=++cnt; if (l==r){ v[rt].fa=l,v[rt].rk=1; return rt; } v[rt].l=build(l,mid),v[rt].r=build(mid+1,r); return rt; } int operate_rk(int idp,int l,int r,int pos,int x){ int rt=++cnt; v[rt]=v[idp]; if (l==r){ v[rt].rk=x; return rt; } if (pos<=mid) v[rt].l=operate_rk(v[idp].l,l,mid,pos,x); else v[rt].r=operate_rk(v[idp].r,mid+1,r,pos,x); return rt; } int operate_fa(int idp,int l,int r,int pos,int x){ int rt=++cnt; v[rt]=v[idp]; if (l==r){ v[rt].fa=x; return rt; } if (pos<=mid) v[rt].l=operate_fa(v[idp].l,l,mid,pos,x); else v[rt].r=operate_fa(v[idp].r,mid+1,r,pos,x); return rt; } Tree query(int idp,int l,int r,int pos){ if(l==r) return v[idp]; if(pos<=mid) return query(v[idp].l,l,mid,pos); else return query(v[idp].r,mid+1,r,pos); } int find(int idp,int x){ int fax=query(idp,1,n,x).fa; return fax==x?x:find(idp,fax);//return fa[x]==x?x:fa[x]=find(fa[x]);这是换成路径压缩 } int merge(int idp,int x,int y){ x=find(idp,x),y=find(idp,y); if (x!=y){ int rkx=query(idp,1,n,x).rk,rky=query(idp,1,n,y).rk; if (rkx<=rky) return operate_rk(operate_fa(idp,1,n,x,y),1,n,y,max(rky,rkx+1)); else return operate_rk(operate_fa(idp,1,n,y,x),1,n,x,max(rkx,rky+1)); } return idp; } int main(){ int m; scanf("%d%d",&n,&m); root[0]=build(1,n); int now=0; for(int i=1;i<=m;++i){ int op; scanf("%d",&op); if (op==2){ int c; scanf("%d",&c); root[now+1]=root[c],++now; } else if (op==1){ int a,b; scanf("%d%d",&a,&b); root[now+1]=merge(root[now],a,b),++now; } else{ int a,b; scanf("%d%d",&a,&b); printf("%d\n",find(root[now],a)==find(root[now],b)?1:0); root[now+1]=root[now],++now; } } return 0; }
总结
今天写了好多题啊,主要整了一下可持久化的数据结构,现在还剩带修的主席树(虽然和主席树无关)和二逼平衡树没有搞。
感觉自己最近训练特别菜的原因,是自己一直在做一些模板题,真正有提升的部分应该是和qz爷一样,刷难题,动脑子,而不是天天只在学习而不去练习,这样虽然练了手,但是脑子却一直锈着,自然面对思维乱搞题直接起飞。
但是之前的比赛也给了我们经验,码力不够的话,连签到题都不会做。就是这样。
所以还是要从基础连起,就是签到题。多写,才知道怎么搞数据结构,代码具体是怎么实现的,原理是什么,以及什么地方可以做修改。
只不过,对我来说,如果想冲一冲EC,时间是真的不太够了呢。
明年继续吧,只能这么说了,眼光放到明年的话,那么现在的一切都还是非常有意义的。
1年,我就不信上不了红?试试看。
数据结构部分感想
其实数据结构主要考察的是一些思想。
比如可持久化数据结构,其实本质都是复用已有信息,本质上和线段树也没什么区别,写成结构体的话,都是l,r,val。只不过l,r不一样了。
再有,只要涉及区间加减的操作,那么就可以考虑差分,变成单点的操作,最后树状数组再合并。这一点17ECFinal那个J题。不说了,真实太思博了(指了指自己)。
千万不能像之前那样什么东西只能用一个去做,其实看了那么多题解,真的不是这样的,主要靠思考,而不是套用(比如主席树——区间第k小)之类的。实际上发现,值域线段树可以代替平衡树的基本操作,但有些地方就不行了,比如说文艺平衡树,好像就必须得用那些传统的平衡树结构来操作了。同样,带修主席树根本就不是可持久化的数据结构,只不过因为树套树肯定会MLE,所以动态开点。本质上其实就是一种新思路,只不过在不带修改的时候,时间复杂度不够好,有更加优秀的主席树。对于带修的问题,根本没法用主席树,所以就只能树套树了。这样看的话,其实香港H题就是个人太菜了……如果之前做过这些题的话,H题就纯一裸题。
明日计划
- 动态开点树状数组——>带修主席树
- 替罪羊树
- 三维扫描线
- CDQ代替树状数组做单点修改+区间求和
来源:https://www.cnblogs.com/diorvh/p/12014406.html