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
",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
",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
",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
",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
",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
");
else
printf("%d
",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
");
else if (num)
printf("%d
",lsh[query_sa(root[opt[i].ver],1,len,rk+2)]);
else
printf("%d
",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
",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
",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
",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代替树状数组做单点修改+区间求和