好像是经典题(?),那就写一波题解罢。
考虑查询,集合里的元素每个有两个指标:所在集合编号和自身权值,那么查询容易想到二分,二分的 chk 其实就是个二维数点。是动态的(实际上这个修改比动态加点强),二分的 chk 这玩意可以看作半在线,就暂时考虑在线动态维护二维数点了吧。
那就考虑树套树,自然的想法是外层维护集合编号,内层维护权值。这样带来两个问题:修改操作相当于 \(1\times ?\) 矩形增加,不会做,\(?\times 1\) 就会做了(就找到那一列在所在内层线段树上区修,之于外层 BIT 来说是单点修改);以及复杂度是 3log 的,感觉要 2log 才能过。
其实让孩子搞基把内外层维护的 swap 一下(就跟 wjz 出的那个 taking a walk 一样的套路(那题题解鸽了快半年了,既然这样就永远鸽下去吧!))可以解决所有问题。前者不 solve 自 solve,后者的话,由于二分的是权值,而外层维护权值,所以可以在外层 BIT 上倍增,把 log 平行化掉。
这个方法啥都好,就是空间是 2log 的。
#include<bits/stdc++.h> using namespace std; #define int long long int lowbit(int x){return x&-x;} const int N=100010,LOG_N=18; int n,qu; int sz; struct node{signed lson,rson,lz;int cnt;}nd[N*200]; #define lson(p) nd[p].lson #define rson(p) nd[p].rson #define lz(p) nd[p].lz #define cnt(p) nd[p].cnt int nwnd(){return nd[++sz]=node({0,0,0,0}),sz;} struct segtree{ int root; void init(){root=nwnd();} void sprdwn(int p,int l,int r){ if(lz(p)){ if(!lson(p))lson(p)=nwnd();if(!rson(p))rson(p)=nwnd(); int mid=l+r>>1; cnt(lson(p))+=1ll*lz(p)*(mid-l+1),lz(lson(p))+=lz(p); cnt(rson(p))+=1ll*lz(p)*(r-mid),lz(rson(p))+=lz(p); lz(p)=0; } } void sprup(int p){cnt(p)=cnt(lson(p))+cnt(rson(p));} void add(int l,int r,int p=-1,int tl=1,int tr=n){~p||(p=root); if(l<=tl&&r>=tr)return cnt(p)+=tr-tl+1,lz(p)++,void(); sprdwn(p,tl,tr); int mid=tl+tr>>1; if(l<=mid){ if(!lson(p))lson(p)=nwnd(); add(l,r,lson(p),tl,mid); } if(r>mid){ if(!rson(p))rson(p)=nwnd(); add(l,r,rson(p),mid+1,tr); } sprup(p); } int _cnt(int l,int r,int p=-1,int tl=1,int tr=n){~p||(p=root); if(l<=tl&&r>=tr)return cnt(p); sprdwn(p,tl,tr); int mid=tl+tr>>1,res=0; if(l<=mid)res+=_cnt(l,r,lson(p),tl,mid); if(r>mid)res+=_cnt(l,r,rson(p),mid+1,tr); return res; } }; struct bitree{ segtree cnt[N]; void init(){ for(int i=1;i<=2*n+1;i++)cnt[i].init(); } void add(int x,int l,int r){ while(x<=2*n+1)cnt[x].add(l,r),x+=lowbit(x); } int fd(int l,int r,int x){ int res=0,_cnt=0; for(int i=LOG_N-1;~i;i--)if(res+(1<<i)<=2*n+1&&_cnt+cnt[res+(1<<i)]._cnt(l,r)<x) res+=1<<i,_cnt+=cnt[res]._cnt(l,r); return res+1; } }bit; signed main(){ // cout<<sizeof(nd[0]); cin>>n>>qu; bit.init(); while(qu--){ int tp,l,r,x; scanf("%lld%lld%lld%lld",&tp,&l,&r,&x); if(tp==1)bit.add(-x+n+1,l,r); else printf("%lld\n",-(bit.fd(l,r,x)-n-1)); } return 0; }
由于允许离线,考虑整体二分。
二分区间 \([l,r]\) 的时候,我们 chk 要看的事情就是对每个询问 \(([l_0,r_0],x)\) 看 \([l_0,r_0]\) 内小于等于 \(mid\) 的个数是否小于等于 \(x\)。我们需要动态求这玩意,这其实就很简单了:按顺序扫所有的 query,如果是修改 \(([l_0,r_0],x)\) 并且 \(x\leq mid\) 那么它有贡献,区间 \([l_0,r_0]\) 加;如果是查询那就线段树里面查就可以了。
但目前有个问题:\(x\) 特别小的修改有可能在很多很多整体二分的子问题里面贡献非零,这样它一路走的路径就不是一条直链!这里涉及二分的一个常用技巧:当前层如果某个询问 \(([l_0,r_0],x)\) 查出来个数是 \(y\) 且 \(x>y\),那 duck 令 \(x\gets x-y\),这样以后就不用考虑前面的修改的贡献了。那这样每层处理完后,不光查询会被分到左右两部分中的一部分(这个在任何整体二分中都恒成立),修改也是被分到恰好(或者说最多?)一部分,没被分到就是贡献必定为 \(0\)。
这样一来每个整体二分的子问题就是关于被分到的 query 个数(查询 + 修改)的 1log,总复杂度就是 2log。注意每次不能给线段树暴力 init(那样复杂度就加上总值域,就被破坏了),需要每次处理完加上负值抵消掉。
#include<bits/stdc++.h> using namespace std; #define int long long #define pb push_back const int N=100010; int n,qu; struct query{int tp,l,r,x,id;}qry[N]; struct segtree{ struct node{int cnt,lz;}nd[N<<2]; #define cnt(p) nd[p].cnt #define lz(p) nd[p].lz void init(){memset(nd,0,sizeof(nd));} void sprdwn(int p,int l,int r){ if(lz(p)){ int mid=l+r>>1; cnt(p<<1)+=1ll*lz(p)*(mid-l+1),lz(p<<1)+=lz(p); cnt(p<<1|1)+=1ll*lz(p)*(r-mid),lz(p<<1|1)+=lz(p); lz(p)=0; } } void sprup(int p){cnt(p)=cnt(p<<1)+cnt(p<<1|1);} void add(int l,int r,int v,int p=1,int tl=1,int tr=n){ if(l<=tl&&r>=tr)return cnt(p)+=v*(tr-tl+1),lz(p)+=v,void(); sprdwn(p,tl,tr); int mid=tl+tr>>1; if(l<=mid)add(l,r,v,p<<1,tl,mid); if(r>mid)add(l,r,v,p<<1|1,mid+1,tr); sprup(p); } int _cnt(int l,int r,int p=1,int tl=1,int tr=n){ if(l<=tl&&r>=tr)return cnt(p); sprdwn(p,tl,tr); int mid=tl+tr>>1,res=0; if(l<=mid)res+=_cnt(l,r,p<<1,tl,mid); if(r>mid)res+=_cnt(l,r,p<<1|1,mid+1,tr); return res; } }segt; int ans[N]; void solve(int l,int r,vector<query> v){ if(l==r){ for(int i=0;i<v.size();i++){ int tp=v[i].tp,id=v[i].id; if(tp==2)ans[id]=l; } return; } int mid=l+r>>1; vector<query> lft,rit; for(int i=0;i<v.size();i++){ int tp=v[i].tp,l=v[i].l,r=v[i].r,x=v[i].x; if(tp==1){ if(x<=mid)segt.add(l,r,1),lft.pb(v[i]); else rit.pb(v[i]); } else{ int fd=segt._cnt(l,r); if(x<=fd)lft.pb(v[i]); else v[i].x-=fd,rit.pb(v[i]); } } for(int i=0;i<v.size();i++){ int tp=v[i].tp,l=v[i].l,r=v[i].r,x=v[i].x,id=v[i].id; if(tp==1&&x<=mid)segt.add(l,r,-1); } solve(l,mid,lft),solve(mid+1,r,rit); } signed main(){ cin>>n>>qu; for(int i=1;i<=qu;i++)scanf("%lld%lld%lld%lld",&qry[i].tp,&qry[i].l,&qry[i].r,&qry[i].x),qry[i].id=i; vector<query> v; for(int i=1;i<=qu;i++)qry[i].tp==1&&(qry[i].x*=-1),v.pb(qry[i]); segt.init(); solve(-n,n,v); for(int i=1;i<=qu;i++)if(qry[i].tp==2)printf("%lld\n",-ans[i]); return 0; }
上面的上面那个在线动态二维数点考虑一波二进制分组 + 主席树。如果主席树的历史版本编号是权值的话,那照样是只能 3log 的;如果颠倒过来的话,依然可以在一棵虚空中的线段树(其实就是只有位置信息)上二分,每次把 2 倍 log 棵线段树的该位置加(减)起来,就相当于 log 个静态区间第 k 大并起来做一样,是可以做到 2log 的。但好像比较难写,就不写了 2333。