传送门
解析:
我当时好像是受了一点刺激来写的这道题来着。。。
首先这道题写平均 O ( n log n ) O(\sqrt n\log n) O(nlogn),最好 O ( n log n ) O(\sqrt n\log n) O(nlogn)的块状链表就卡的过去了。
但是有一些操作是可以优化为 O ( n ) O(\sqrt n) O(n)的。
当然,您不用批评博主代码写得丑,要是您有过这种自己调试这种毒瘤数据结构题的经历,您肯定明白博主这种“由于各种丑代码在调试和找题解的时候见惯了而什么代码都能看懂却完全不懂什么代码写得丑什么代码写得美”的这种心情。
您要是无法接受博主的码风,您可以选择出门左转看其他题解,或者只看讲解不看代码。
以上内容利益无关,心情相关。
以上,如果您觉得有所冒犯,出门左转,谢谢。
总之博主在写这篇题解和做这道题的时候憋了一口气。
思路:
本来写了一个极其慢的块状链表。 20 s 20s 20s才跑完。。。
然后去学习了一下怎么用 l i s t list list和 v e c t o r vector vector来实现块状链表。卡进了 5 s 5s 5s。
之后又写了一个 O D T ODT ODT,卡进了 2 s 2s 2s。
这篇题解主要讲一讲怎么用块状链表来解决这种数据结构毒瘤题。
块状链表基础简介以及为什么要用块状链表:
考虑最基础的数据结构,线性表。
线性表的两种实现方式:链表和数组。
链表能够支持最坏 O ( n ) O(n) O(n)的随机访问,和严格 O ( 1 ) O(1) O(1)的插入删除。
数组能够支持严格 O ( 1 ) O(1) O(1)的随机访问,和最坏 O ( n ) O(n) O(n)的插入删除。
当然静态数组我们可以用分块进行一定程度的优化。
但是到了动态的数组这里,块状链表就是一种将链表和数组结合起来的利器。
块状链表的外层数据结构是链表,而链表的每一个节点维护一个原数组的区间。一般来说块的大小以及块的数量我们都会选择 O ( n ) O(\sqrt n) O(n)。当然,某些情况下可能 O ( n log n ) O(\sqrt {n\log n}) O(nlogn)会更加优秀。
这样我们能够在最坏 O ( n ) O(\sqrt n) O(n)时间里支持所有的随机访问,以及平均 O ( n ) O(\sqrt n) O(n)时间内支持所有的插入删除。
当然要支持一些奇怪的东西的话还可能需要排序等等,复杂度增加一个 O ( log n ) = O ( log n ) O(\log\sqrt n)=O(\log n) O(logn)=O(logn)。
基础操作:
这一部分操作的介绍是基于普通的块状链表,不是题目中所要求的操作。
1. s p l i t split split拆分:
简单来说就是将一个块拆分成两个或者多个块。
我们有的时候需要将区间进行移动(比如翻转或者循环位移)。
或者我们认为直接 s p l i t split split后对整块进行操作要比对散块进行操作更便捷的话。
或者我们就是懒得写散块的操作的话
我们显然需要将某一个区间从它原来所属的块中分离出来。
这个就是 s p l i t split split操作的产生原因了。
具体来讲,我们只需要申请一个新块的内存,更改链表的 n x t nxt nxt指针和 p r e pre pre指针。然后将需要分离的数组部分放到新块当中。原来块内的内存该回收的就尽量回收就行了。
平均复杂度 O ( n ) O(\sqrt n) O(n)
2. m e r g e merge merge合并:
简单来说就是讲两个或多个块合并成一个。
似乎没有什么操作本身要求需要进行合并。
不过接下来介绍的基础操作就必须依靠 m e r g e merge merge来实现。
m e r g e merge merge一般是将当前块与下一块进行合并。
总之只需要先更改链表的指针,然后将改复制的信息复制过来,之后更新就可以了。注意回收内存。
平均复杂度 O ( n ) O(\sqrt n) O(n)
3. m a i n t a i n maintain maintain维护
为了保证块状链表的复杂度我们需要保证块的大小以及块的数量。
而显然 s p l i t split split操作会使得块越来越散,越来越小。特殊构造的数据到最后需要对 O ( n ) O(n) O(n)个整块进行整块的操作,显然不是我们所希望看见的。
所以我们才会有必要来进行 m e r g e merge merge操作。
不过显然要保证所有块的大小都是严格的 n \sqrt n n代价是相当大的,我们选择保证所有块大小的规模都是 O ( n ) O(\sqrt n) O(n)的。
选择常数 α \alpha α,当两个相邻块的大小相加不超过 α n \alpha\sqrt n αn的时候,我们将两个块合并,当一个快的大小超过 α n \alpha \sqrt n αn的时候我们选择将它 s p l i t split split。
可以证明,这样的平均复杂度是 O ( n ) O(\sqrt n) O(n)的。
4. f i n d find find随机访问
我们从链表的表头开始寻找就行了,不过最好还是要保证这个玩意千万不要写错,不然会很麻烦
最坏 O ( n ) O(\sqrt n) O(n)
现在以本题为例子,来实现一下块状链表
这些操作转换成数据结构的语言是什么:
1.单点插入,insert(pos,val)
2.单点删除,erase(pos)
3.区间翻转,reverse(l,r)
4.区间循环位移,move(l,r,k)
5.区间加,add(l,r,val)
6.区间覆盖,cover(l,r,val)
7.询问区间和,query_sum(l,r)
8.询问区间最大最小值(之差),Min_Max(l,r)
9.区间询问前驱后继(最近的那个),nearest(l,r,val)
10.询问区间第K小,query_Kth(l,r,k)
11.区间询问排名,query_Rank(l,r,val)
当然,区间询问和询问区间两种表示方式是有区别的 ,不过没必要太在意这些
显然当我们看见第 11 11 11个操作:区间询问前驱后继,应该已经反应过来了吧。
为了处理整块的询问,排序势在必行。
好的,显然排序之后一切都变得简单很多了。
排序之后显然所有操作都可以方便的进行维护了。
接下来讨论具体怎么处理每个操作:
每个操作我会先给出我的实现中期望的复杂度,读者可以先自行思考后再看我的讲解,如果您有更好的实现方式欢迎告知博主。
不过把 s p l i t , m e r g e split,merge split,merge和维护排序数组分开的做法就不要来了,不好写。
1.单点插入:期望 O ( n ) O(\sqrt n) O(n),最坏 O ( n log n ) O(\sqrt n\log n) O(nlogn)
显然找到位置之后原数组循环位移一位就可以插入到原数组中了。
排序后的数组可以先lower_bound找到应该插入的地方,然后循环位移。
注意最后需要 m a i n t a i n maintain maintain一下。不然的话遇到全是 i n s e r t insert insert的数据复杂度就爆炸了。
复杂度就是块的大小 O ( n ) O(\sqrt n) O(n),最坏情况下需要分裂块,然后重构排序数组。
2.单点删除: O ( n ) O(\sqrt n) O(n)
显然和插入操作是一样的,原数组循环位移一位。排序后的数组用lower_bound找到对应的节点,也是循环位移。
最后可以不 m a i n t a i n maintain maintain,不会影响复杂度,不过实测还是 m a i n t a i n maintain maintain一下最快。
复杂度还是块的大小 O ( n ) O(\sqrt n) O(n),可能需要 m e r g e merge merge两个块,就飞到了 O ( n log n ) O(\sqrt n\log n) O(nlogn)。
3.区间翻转:最坏 O ( n log n ) O(\sqrt n\log n) O(nlogn),期望 O ( n log n ) O(\sqrt n\log n) O(nlogn)
很显然,将需要翻转的区间左右端点从对应的块里面 s p l i t split split出来,所有区间变换链表指针,特殊处理左右端点。
然后所有区间打翻转标记。最后 m a i n t a i n maintain maintain一下就行了(因为有两个 s p l i t split split出来的块)
不需要重构的情况只有当翻转的端点刚好落在块的端点的情况,概率很小,计算期望复杂度时不考虑。
所以复杂度只算 s p l i t split split和 m e r g e merge merge就行了 O ( n log n ) O(\sqrt n\log n) O(nlogn)
4.区间循环位移:最坏 O ( n log n ) O(\sqrt n\log n) O(nlogn)
直接把两个区间 s p l i t split split出来,一个是 [ l , r − k ] [l,r-k] [l,r−k],另一个是 [ r − k + 1 , r ] [r-k+1,r] [r−k+1,r]。
改变链表上的指针就行了。
复杂度仍然是 s p l i t split split和 m e r g e merge merge的复杂度。
5~6.区间加,区间覆盖:最坏 O ( n log n ) O(\sqrt n\log n) O(nlogn)
找得到对应的块,中间的打标记,两边的推完重构排序数组。
复杂度瓶颈在于排序: O ( n log n ) O(\sqrt n\log n) O(nlogn)
7~8.询问区间和,区间最大最小值:最坏 O ( n ) O(\sqrt n) O(n)
这个直接找到对应块,整块和散块都 O ( n ) O(\sqrt n) O(n)扫一遍就行了。
9,11.区间询问前驱后继,询问排名: O ( n log n ) O(\sqrt n\log n) O(nlogn)
散块暴力扫,整块在排序后的数组上二分就行了。
10.区间询问第K小:最坏 O ( n log n log ∣ A ∣ ) O(\sqrt n\log n\log |A|) O(nlognlog∣A∣)
其中 A A A是值域。
如果您用分块做过树套树的版题,您一定明白这是个什么东西。
由于我们维护的信息是散的,不可能直接找第 K K K小。
所以我们在值域上二分,统计当前答案的排名,然后。。。就没有然后了。。
C++STL实现需要注意的地方:
如果您完全用不来STL,不用看了,您看不懂的。
如果您STL比较熟,这里就是为您所写的。
首先我们知道有一种方便的可以维护动态长度的数组的STL叫vector,还有一种本身就是链表的STL叫list。
那么我们考虑用list和vector来实现块状链表。
首先定义结构体,把该存的都存上。
然后用一个list来维护信息。
这时候发现一个问题:由于迭代器本身是通过重载后的 + + ++ ++和 − − -- −−运算符相联系在一起的,怎么更改链表上的指针呢?
所以我们可能需要用到list的一个骚操作:splice
std::list::splice:
通过splice我们能够实现list内元素的 O ( 1 ) O(1) O(1)变动。
它的主要功能就是将一个list中连续的一段接在另一个list的某一个迭代器后面,同时在原来的list中删除这段。
由于只有指针的变动,没有任何内存的分配与删除,所以非常快。
不过要是在这之后对list调用size()函数,会很慢。。。
总之这份代码就是在这样的情况下写出来的。
非常窒息。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define re register
#define gc get_char
#define cs const
namespace IO{
namespace IOONLY{
cs int Rlen=1<<20|1;
char buf[Rlen],*p1,*p2;
}
inline char get_char(){
using namespace IOONLY;
return (p1==p2)&&(p2=(p1=buf)+fread(buf,1,Rlen,stdin),p1==p2)?EOF:*p1++;
}
inline int getint(){
re char c;
while(!isdigit(c=gc()));re int num=c^48;
while(isdigit(c=gc()))num=(num+(num<<2)<<1)+(c^48);
return num;
}
}
using namespace IO;
cs int N=100005,INF=0x7fffffff;
int n;
struct node{
vector<int> d,s;
bool rev;
int add,cover;
ll sum;
node():rev(0),add(0),cover(0),sum(0){}
inline void pushdown(){
if(rev)reverse(d.begin(),d.end()),rev=0;
if(add){
for(int re i=0;i<d.size();++i)
d[i]+=add,s[i]+=add;
sum+=(ll)add*d.size();
add=0;
}
if(cover){
fill(d.begin(),d.end(),cover);
fill(s.begin(),s.end(),cover);
sum=(ll)cover*d.size();
cover=0;
}
}
inline void resort(){
s=d;
sort(s.begin(),s.end());
}
inline ll Sum()cs{return cover?(ll)cover*d.size():(sum+(ll)add*d.size());}
inline int Max()cs{return cover?cover:s.back()+add;}
inline int Min()cs{return cover?cover:s[0]+add;}
};
list<node> d;
typedef list<node>::iterator point;
int Bsiz,Bcnt;
inline void init(cs int *a,int n,int m){
Bsiz=max((sqrt(n)+1)*4/3,1.0);
Bcnt=ceil((double)n/Bsiz);
d.resize(Bcnt);
point now=d.begin();
for(int re i=0,j=0;i<Bcnt;++i,++now){
j+=Bsiz;
now->d.assign(a+j-Bsiz,a+min(j,n));
for(vector<int>::iterator re it=now->d.begin();it!=now->d.end();++it)
now->sum+=*it;
now->resort();
}
}
inline void find(int &pos,point &now){
for(now=d.begin();now!=d.end()&&(int)(pos-now->d.size())>=0;pos-=now->d.size(),++now);
now->pushdown();
}
inline void split(point now,int pos){
if(pos==now->d.size())return ;
point nxt=now;
nxt=d.insert(++nxt,node());
now->pushdown();
nxt->d.assign(now->d.begin()+pos,now->d.end());
now->d.erase(now->d.begin()+pos,now->d.end());
for(vector<int>::iterator re it=nxt->d.begin();it!=nxt->d.end();++it)
nxt->sum+=*it;
now->sum-=nxt->sum;
now->resort();
nxt->resort();
}
inline void merge_sort(vector<int> &s,vector<int> &b){
vector<int> a=s;
s.resize(a.size()+b.size());
int sq=0,aq=0,bq=0;
while(aq<a.size()&&bq<b.size())s[sq++]=a[aq]<b[bq]?a[aq++]:b[bq++];
while(aq<a.size())s[sq++]=a[aq++];
while(bq<b.size())s[sq++]=b[bq++];
b.clear();
}
inline void merge(point now){
point nxt=now;++nxt;
now->pushdown();
nxt->pushdown();
now->d.insert(now->d.end(),nxt->d.begin(),nxt->d.end());
now->sum+=nxt->sum;
merge_sort(now->s,nxt->s);
d.erase(nxt);
}
inline void maintain(point now){
Bsiz=max((sqrt(n)+1)*4/3,1.0);
for(point re nxt;now!=d.end();++now)
if(now->d.size()>Bsiz*2)split(now,now->d.size()>>1);
else if(now->d.size()<Bsiz/2){
nxt=now,++nxt;
if(nxt==d.end())break;
else if(now->d.size()+nxt->d.size()<=Bsiz*2)merge(now);
}
}
inline point makenode(int pos){
point now;
find(pos,now);
if(pos>=0)split(now,pos+1),++now;
return now;
}
inline void insert(int pos,int val){
point now;--pos;++n;
find(pos,now);
now->d.insert(now->d.begin()+pos+1,val);
now->s.insert(lower_bound(now->s.begin(),now->s.end(),val),val);
now->sum+=val;
maintain(now);
}
inline void erase(int pos){
point now;--pos;--n;
find(pos,now);
now->sum-=now->d[pos];
now->s.erase(lower_bound(now->s.begin(),now->s.end(),now->d[pos]));
now->d.erase(now->d.begin()+pos);
maintain(now);
}
inline void reverse(int l,int r){
--l,--r;
list<node> tmp;
point lp=makenode(l-1),rp;
find(r,rp);
split(rp,r+1);
tmp.splice(tmp.begin(),d,lp,++rp);
for(point re now=tmp.begin();now!=tmp.end();++now)now->rev^=1;
tmp.reverse();
lp=rp;
if(lp!=d.begin())--lp;
d.splice(rp,tmp);
maintain(lp);
}
inline void move(int l,int r,int k){
--l,--r;
list<node> tmp;
point lp,rp,mp;
lp=makenode(l-1);
mp=makenode(r-k);
find(r,rp);
split(rp,r+1);
tmp.splice(tmp.begin(),d,mp,++rp);
d.splice(lp,tmp);
maintain(d.begin());
}
inline void add(int l,int r,int val){
--l,--r;
point lp,rp;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)lp->d[i]+=val;
lp->sum+=(r-l+1ll)*val;
lp->resort();
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)lp->d[i]+=val;
lp->sum+=(ll)(lp->d.size()-l)*val;
lp->resort();
++lp;
}
for(;lp!=rp;++lp)(lp->cover?lp->cover:lp->add)+=val;
if(r+1==rp->d.size())(rp->cover?rp->cover:rp->add)+=val;
else {
rp->pushdown();
for(int re i=0;i<=r;++i)rp->d[i]+=val;
rp->sum+=(ll)(r+1)*val;
rp->resort();
}
}
}
inline void cover(int l,int r,int val){
--l,--r;
point lp,rp;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)lp->sum+=val-lp->d[i],lp->d[i]=val;
lp->resort();
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)lp->sum+=val-lp->d[i],lp->d[i]=val;
lp->resort();
++lp;
}
for(;lp!=rp;++lp){
lp->add=0;
lp->cover=val;
}
if(r+1==rp->d.size())rp->add=0,rp->cover=val;
else {
rp->pushdown();
for(int re i=0;i<=r;++i)rp->sum+=val-rp->d[i],rp->d[i]=val;
rp->resort();
}
}
}
inline ll query_sum(int l,int r){
--l,--r;
point lp,rp;
re ll res=0;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)res+=lp->d[i];
return res;
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)res+=lp->d[i];
++lp;
}
for(;lp!=rp;++lp)res+=lp->Sum();
if(r+1==rp->d.size())res+=rp->Sum();
else {
rp->pushdown();
for(int re i=0;i<=r;++i)res+=rp->d[i];
}
return res;
}
}
inline int Min_Max(int l,int r){
--l,--r;
point lp,rp;
int mn=INF,mx=-INF;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)mn=min(mn,lp->d[i]),mx=max(mx,lp->d[i]);
return mx-mn;
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)mn=min(mn,lp->d[i]),mx=max(mx,lp->d[i]);
++lp;
}
for(;lp!=rp;++lp)mn=min(mn,lp->Min()),mx=max(mx,lp->Max());
if(r+1==rp->d.size())mn=min(mn,rp->Min()),mx=max(mx,rp->Max());
else {
rp->pushdown();
for(int re i=0;i<=r;++i)mn=min(mn,rp->d[i]),mx=max(mx,rp->d[i]);
}
return mx-mn;
}
}
inline int nearest(int l,int r,int val){
--l,--r;
re int res=INF;
point lp,rp;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)res=min(res,abs(lp->d[i]-val));
return res;
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)res=min(res,abs(lp->d[i]-val));
++lp;
}
for(int re pos,tmp;lp!=rp;++lp)
if(lp->cover)res=min(res,abs(lp->cover-val));
else {
tmp=val-lp->add;
pos=lower_bound(lp->s.begin(),lp->s.end(),tmp)-lp->s.begin();
if(pos!=lp->d.size())res=min(res,lp->s[pos]-tmp);
--pos;
if(~pos)res=min(res,tmp-lp->s[pos]);
}
rp->pushdown();
for(int re i=0;i<=r;++i)res=min(res,abs(rp->d[i]-val));
return res;
}
}
inline int Cnt(point lp,int l,point rp,int r,int val){
re int cnt=0;
if(lp==rp)for(int re i=l;i<=r;++i)cnt+=lp->d[i]<=val;
else {
if(l>0){
for(int re i=l;i<lp->d.size();++i)cnt+=lp->d[i]<=val;
++lp;
}
for(;lp!=rp;++lp)
if(lp->cover)cnt+=lp->cover<=val?lp->d.size():0;
else cnt+=upper_bound(lp->s.begin(),lp->s.end(),val-lp->add)-lp->s.begin();
rp->pushdown();
for(int re i=0;i<=r;++i)cnt+=(rp->d[i]<=val);
}
return cnt;
}
inline int query_Kth(int l,int r,int k){
--l,--r;
point lp,rp,np;
find(l,lp);find(r,rp);np=lp;
int L=INF,R=-INF;
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)L=min(L,lp->d[i]),R=max(R,lp->d[i]);
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)L=min(L,lp->d[i]),R=max(R,lp->d[i]);
++lp;
}
for(;lp!=rp;++lp)L=min(L,lp->Min()),R=max(R,lp->Max());
rp->pushdown();
for(int re i=0;i<=r;++i)L=min(L,rp->d[i]),R=max(R,rp->d[i]);
}
lp=np;
while(L<R){
int mid=(L+R)>>1;
if(Cnt(lp,l,rp,r,mid)>=k)R=mid;
else L=mid+1;
}
return L;
}
inline int query_Rank(int l,int r,int val){
--l,--r;
re int cnt=0;
point lp,rp;
find(l,lp);find(r,rp);
if(lp==rp){
lp->pushdown();
for(int re i=l;i<=r;++i)cnt+=(lp->d[i]<val);
return cnt;
}
else {
if(l>0){
lp->pushdown();
for(int re i=l;i<lp->d.size();++i)cnt+=(lp->d[i]<val);
++lp;
}
for(;lp!=rp;++lp)
if(lp->cover)cnt+=lp->cover<val?lp->d.size():0;
else cnt+=lower_bound(lp->s.begin(),lp->s.end(),val-lp->add)-lp->s.begin();
rp->pushdown();
for(int re i=0;i<=r;++i)cnt+=(rp->d[i]<val);
return cnt;
}
}
int a[N],m;
signed main(){
ios::sync_with_stdio(false);
n=getint();
for(int re i=0;i<n;++i)a[i]=getint();
m=getint();
init(a,n,m);
while(m--){
int x,y,val;
switch(getint()){
case 1:x=getint();val=getint();insert(x,val);break;
case 2:erase(getint());break;
case 3:x=getint(),y=getint();reverse(x,y);break;
case 4:x=getint(),y=getint(),val=getint();move(x,y,val);break;
case 5:x=getint(),y=getint(),val=getint();add(x,y,val);break;
case 6:x=getint(),y=getint(),val=getint();cover(x,y,val);break;
case 7:x=getint(),y=getint();cout<<query_sum(x,y)<<'\n';break;
case 8:x=getint(),y=getint();cout<<Min_Max(x,y)<<'\n';break;
case 9:x=getint(),y=getint(),val=getint();cout<<nearest(x,y,val)<<'\n';break;
case 10:x=getint(),y=getint(),val=getint();cout<<query_Kth(x,y,val)<<'\n';break;
case 11:x=getint(),y=getint(),val=getint();cout<<query_Rank(x,y,val)<<'\n';break;
}
}
return 0;
}