洛谷题目页面传送门 & CodeForces题目页面传送门
有一个集合(s),你需要支持以下(3)种(q)次操作:
- ( exttt{add} x):令(s=scup{x}),保证在执行之前(x otin s),(xinleft[1,10^9 ight]);
- ( exttt{del} x):令(s=s-{x}),保证在执行之前(xin s),(xinleft[1,10^9 ight]);
- ( exttt{sum}):设将(s)中所有元素排序后得到1-indexed序列(a),查询(sumlimits_{iin[1,|a|],imod5=3}a_i)。
(qinleft[1,10^5 ight])。
这其实是一个比较简单的DS题,毕竟难度只有(^*2200)。只不过这题比较nb,一题四解。
解法(1):离散化+线段树
考虑建一棵值域线段树,设节点(i)表示区间([l_i,r_i]),那么节点(i)存储([l_i,r_i])内(in s)的数的数量(cnt_i),和(forall jin[0,5)),区间([l_i,r_i])内所有(in s)的元素排序后所有下标(mod5=j)的数的和(sum_{i,j})。那么上传时令(cnt_i=cnt_{lson_i}+cnt_{rson_i},sum_{i,j}=sum_{lson_i,j}+sum_{rson_i,left(j-cnt_{lson_i} ight)mod5})即可。一次上传时间复杂度(mathrm O(1))。
由于值域大小为(10^9),我们可以将所有操作离线下来并离散化所有数值。
此时( exttt{add}, exttt{del})操作都是简单的单点修改,(mathrm O(log q));( exttt{sum})操作是整体查询,直接调用(sum_{root,3})即可,(mathrm O(1))。
总空间复杂度(mathrm O(q)),时间复杂度(mathrm O(qlog q))。
下面是AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
const int QU=100000;
int qu;//操作数
struct query{int tp,x;}qry[QU+1];//操作
vector<int> nums;//离散化数组
void discrete(){//离散化
sort(nums.begin(),nums.end());
nums.resize(unique(nums.begin(),nums.end())-nums.begin());
for(int i=1;i<=qu;i++)if(qry[i].tp!=2)qry[i].x=lower_bound(nums.begin(),nums.end(),qry[i].x)-nums.begin();
}
struct segtree{//线段树
struct node{int l,r,sum[5],cnt;}nd[QU<<2];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define sum(p) nd[p].sum
#define cnt(p) nd[p].cnt
void bld(int l=0,int r=(int)(nums.size())-1,int p=1){//建树
if(l>r)return;
l(p)=l;r(p)=r;sum(p)[0]=sum(p)[1]=sum(p)[2]=sum(p)[3]=sum(p)[4]=cnt(p)=0;
if(l==r)return;
int mid=l+r>>1;
bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
}
void init(){bld();}//线段树初始化
void sprup(int p){//上传
cnt(p)=cnt(p<<1)+cnt(p<<1|1);
for(int i=0;i<5;i++)sum(p)[i]=sum(p<<1)[i]+sum(p<<1|1)[(((i-cnt(p<<1))%5)+5)%5];
}
void add(int x,int p=1){//add操作
if(l(p)==r(p))return sum(p)[1]=nums[x],cnt(p)=1/*此数in s*/,void();
int mid=l(p)+r(p)>>1;
add(x,p<<1|(x>mid));
sprup(p);
}
void del(int x,int p=1){//del操作
if(l(p)==r(p))return sum(p)[1]=cnt(p)=0/*此数notin s*/,void();
int mid=l(p)+r(p)>>1;
del(x,p<<1|(x>mid));
sprup(p);
}
int _sum()/*sum操作*/{return nums.size()?sum(1)[3]:0;}
}segt;
signed main(){
cin>>qu;
for(int i=1;i<=qu;i++){//离线
string tp;
cin>>tp;
if(tp=="add")qry[i].tp=0,cin>>qry[i].x,nums.pb(qry[i].x);
else if(tp=="del")qry[i].tp=1,cin>>qry[i].x,nums.pb(qry[i].x);
else qry[i].tp=2;
}
discrete();//离散化
segt.init();//线段树初始化
for(int i=1;i<=qu;i++){
int tp=qry[i].tp,x=qry[i].x;
if(tp==0)segt.add(x);
else if(tp==1)segt.del(x);
else cout<<segt._sum()<<"
";
}
return 0;
}
解法(2):动态开点线段树
线段树部分的思路与解法(1)完全一样,不过此方法利用动态开点来处理值域过大问题,实现了在线执行操作。
总空间复杂度(mathrm O(qlog q)),时间复杂度(mathrm O(qlog q))。
下面是AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int QU=100000,A_I=1000000000,LOG_A_I=30;
int qu;//操作数
struct segtree{//动态开点线段树
int sz;//点数
struct node{int l,r,lson,rson,sum[5],cnt;}nd[QU*LOG_A_I+1];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define lson(p) nd[p].lson
#define rson(p) nd[p].rson
#define sum(p) nd[p].sum
#define cnt(p) nd[p].cnt
int nwnd(int l=1,int r=A_I){return nd[++sz]=node({l,r,0,0,{0,0,0,0,0},0}),sz;}//新建节点
void init(){nd[0]=node({0,0,0,0,{0,0,0,0,0},0});sz=0;nwnd();}//线段树初始化
void sprup(int p){//上传
cnt(p)=cnt(lson(p))+cnt(rson(p));
for(int i=0;i<5;i++)sum(p)[i]=sum(lson(p))[i]+sum(rson(p))[(((i-cnt(lson(p)))%5)+5)%5];
}
void add(int x,int p=1){//add操作
if(l(p)==r(p))return sum(p)[1]=x,cnt(p)=1/*此数in s*/,void();
int mid=l(p)+r(p)>>1;
if(x<=mid)add(x,lson(p)=lson(p)?lson(p):nwnd(l(p),mid));
else add(x,rson(p)=rson(p)?rson(p):nwnd(mid+1,r(p)));
sprup(p);
}
void del(int x,int p=1){//del操作
if(l(p)==r(p))return sum(p)[1]=cnt(p)=0/*此数notin s*/,void();
int mid=l(p)+r(p)>>1;
if(x<=mid)del(x,lson(p)=lson(p)?lson(p):nwnd(l(p),mid));
else del(x,rson(p)=rson(p)?rson(p):nwnd(mid+1,r(p)));
sprup(p);
}
int _sum()/*sum操作*/{return sum(1)[3];}
}segt;
signed main(){
cin>>qu;
segt.init();//线段树初始化
while(qu--){
string tp;int x;
cin>>tp;
if(tp=="add")cin>>x,segt.add(x);
else if(tp=="del")cin>>x,segt.del(x);
else cout<<segt._sum()<<"
";
}
return 0;
}
解法(3):分块
分块是无所不能的
其实上面那句话在某种程度上是正确的
考虑对(s)中所有元素排序后得到的1-indexed序列(a)分块。由于有插入、删除操作,所以这是一个动态调整块内元素的分块。
考虑维护set
(s)。块(i)内存(a)的某一个子段(a'_i),和(forall jin[0,5)),该1-indexed子段(a'_i)内所有下标(mod5=j)的数的和(sum_{i,j})。
-
扫描:调整所有块内元素使得每块内元素数量不超过(sz1)。从左往右依次扫描每个块(i),如果(|a'_i|>sz1)(此时必然有(|a'_i|=sz1+1),因为每次( exttt{add})操作之后都进行扫描),那么将(a'_{i,|a'_i|})放到(a'_{i+1})的前面,并(mathrm O(1))重算(sum_i,sum_{i+1})。由于要在两端操作,所以(a'_i)应该定义为
deque<int>
。时间复杂度(mathrm O!left(dfrac q{sz1} ight))。 -
对于( exttt{add})操作:通过(s)二分找到距离(x)最近的(in s)的元素(x'),找到(x')所在块并将(x)插入该块且维护有序性。暴力重算该块内的(sum)。更新(s)。扫描一遍。时间复杂度(mathrm O!left(log q+sz1+dfrac q{sz1} ight))。
-
对于( exttt{del})操作:找到(x)所在块并将(x)删除。暴力重算该块内的(sum)。更新(s)。扫描一遍。时间复杂度(mathrm O!left(sz1+dfrac q{sz1} ight))。
-
对于( exttt{sum})操作:从左往右让每个块贡献答案即可,这样是(mathrm O!left(dfrac q{sz1} ight))的。然鹅其实可以优化到(mathrm O(1)),就是每次扫描时顺便算出答案并记录下来,这样就可以直接调用。
令(sz1=leftlfloor sqrt q ight floor),这样总时间复杂度(mathrm O(qsqrt q))。空间复杂度(mathrm O(q))。
然后开开森森的交上去,T了?是因为这巨大的复杂度乘上巨大的常数。于是开始卡常。
先将( exttt{add}, exttt{del})内的“暴力重算该块内的(sum)”用指针寻址优化一下(因为deque
的随机访问很慢)。此时( exttt{add}, exttt{del})前面一部分的常数与最后的扫描的常数形成了鲜明的对比,因为扫描还带一个(5)的常数。于是调参要从娃娃抓起,我们将(sz1)稍微调大一点,来实现常数上的尽量平衡。令(sz1=3leftlfloor sqrt q
ight
floor)即可AC。
下面是AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pf push_front
#define ppb pop_back
#define pb push_back
const int QU=100000,DB_SZ=500;
int qu;
struct dvdblk{//分块
int sz1,ans/*任意时刻的答案*/;
struct block{
deque<int> a;int sum[5];
block(){sum[0]=sum[1]=sum[2]=sum[3]=sum[4]=0;}
}blk[DB_SZ];
#define a(p) blk[p].a
#define sum(p) blk[p].sum
set<int> all;
void init(){ans=0;sz1=3*sqrt(qu);}//分块初始化
void scn(){//扫描
int now=0;
ans=0;
for(int i=1;i<=(qu+sz1-1)/sz1;i++){
if(a(i).size()>sz1){
a(i+1).pf(a(i).back());//将此块的最后一个元素放到后面一块
int tmp[]={sum(i+1)[0],sum(i+1)[1],sum(i+1)[2],sum(i+1)[3],sum(i+1)[4]};
for(int j=0;j<5;j++)sum(i+1)[j]=tmp[(j-1+5)%5];
sum(i+1)[1]+=a(i+1)[0];//O(1)调整sum[i+1]
sum(i)[a(i).size()%5]-=a(i).back();//O(1)调整sum[i]
a(i).ppb();//将此块的最后一个元素放到后面一块
}
ans+=sum(i)[((3-now)%5+5)%5];//计算当前的答案
now+=a(i).size();
}
}
void add(int x){//add操作
if(all.empty())a(1).pb(x),sum(1)[1]=x;//特判s为空的情况
else{
set<int>::iterator fd=all.lower_bound(x);
if(fd==all.end())fd--;
int p;
for(int i=1;;i++)if(a(i)[0]<=*fd&&*fd<=a(i).back()){p=i;break;}//找到即将插入的块
a(p).insert(lower_bound(a(p).begin(),a(p).end(),x),x);//插入
sum(p)[0]=sum(p)[1]=sum(p)[2]=sum(p)[3]=sum(p)[4]=0;
int now=1;
deque<int>::iterator end=a(p).end();
for(deque<int>::iterator i=a(p).begin();i!=end;++i,++now,now==5&&(now=0))sum(p)[now]+=*i;//暴力重算sum[p](寻址优化后)
}
all.insert(x);//维护s
scn();//扫描
}
void del(int x){//del操作
int p;
for(int i=1;;i++)if(a(i)[0]<=x&&x<=a(i).back()){p=i;break;}//找到即将插入的块
a(p).erase(lower_bound(a(p).begin(),a(p).end(),x));//删除
sum(p)[0]=sum(p)[1]=sum(p)[2]=sum(p)[3]=sum(p)[4]=0;
int now=1;
deque<int>::iterator end=a(p).end();
for(deque<int>::iterator i=a(p).begin();i!=end;++i,++now,now==5&&(now=0))sum(p)[now]+=*i;//暴力重算sum[p](寻址优化后)
all.erase(x);//维护s
scn();//扫描
}
int _sum()/*sum操作*/{return ans;}
}db;
signed main(){
cin>>qu;
db.init();//分块初始化
for(int i=1;i<=qu;i++){
string tp;int x;
cin>>tp;
if(tp=="add")cin>>x,db.add(x);
else if(tp=="del")cin>>x,db.del(x);
else cout<<db._sum()<<"
";
}
return 0;
}
解法(4):平衡树
看到往集合里添加、删除,不难想到平衡树。这里使用fhq-Treap。
节点(i)存储此节点的权值(v_i),以(i)为根的子树的大小(sz_i),和(forall jin[0,5)),以(i)为根的子树内所有元素排序后所有下标(mod5=j)的数的和(sum_{i,j})。那么上传时令(sz_i=sz_{lson_i}+1+sz_{rson_i},sum_{i,j}=sum_{lson_i,j}+left[j=(sz_{lson_i}+1)mod5 ight]v_i+sum_{rson_i,left(j-cnt_{lson_i}-1 ight)mod5})即可。一次上传时间复杂度(mathrm O(1))。
此时( exttt{add}, exttt{del})操作都是简单的单点插入/删除,(mathrm O(log q));( exttt{sum})操作是整体查询,直接调用(sum_{root,3})即可,(mathrm O(1))。
总空间复杂度(mathrm O(q)),时间复杂度(mathrm O(qlog q))。
下面是AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
#define mp make_pair
#define X first
#define Y second
mt19937 rng(20060617/*信仰优化*/);
const int QU=100000;
int qu;//操作数
struct fhq_treap{//fhq-Treap
int sz/*点数*/,root/*根*/;
struct node{unsigned key;int lson,rson,sz,v,sum[5];}nd[QU+1];
#define key(p) nd[p].key
#define lson(p) nd[p].lson
#define rson(p) nd[p].rson
#define sz(p) nd[p].sz
#define v(p) nd[p].v
#define sum(p) nd[p].sum
void init(){nd[sz=root=0]=node({0,0,0,0,0,{0,0,0,0,0}});}//fhq-Treap初始化
void sprup(int p){//上传
sz(p)=sz(lson(p))+1+sz(rson(p));
for(int i=0;i<5;i++)sum(p)[i]=sum(lson(p))[i]+sum(rson(p))[(((i-sz(lson(p))-1)%5)+5)%5];
sum(p)[(sz(lson(p))+1)%5]+=v(p);
}
pair<int,int> split(int x,int p=-1){~p||(p=root);
if(!x)return mp(0,p);
pair<int,int> sp;
if(x<=sz(lson(p)))return sp=split(x,lson(p)),lson(p)=sp.Y,sprup(p),mp(sp.X,p);
return sp=split(x-sz(lson(p))-1,rson(p)),rson(p)=sp.X,sprup(p),mp(p,sp.Y);
}
int mrg(int p,int q){
if(!p||!q)return p|q;
if(key(p)<key(q))return rson(p)=mrg(rson(p),q),sprup(p),p;
return lson(q)=mrg(p,lson(q)),sprup(q),q;
}
int lss(int v,int p=-1)/*比v小的数的个数*/{~p||(p=root);
if(!p)return 0;
if(v(p)<v)return sz(lson(p))+1+lss(v,rson(p));
return lss(v,lson(p));
}
int nwnd(int v){return nd[++sz]=node({rng(),0,0,1,v,{0,v,0,0,0}}),sz;}//新建节点
void add(int v){//add操作
pair<int,int> sp=split(lss(v));
root=mrg(mrg(sp.X,nwnd(v)),sp.Y);
}
void del(int v){//del操作
pair<int,int> sp=split(lss(v)),sp0=split(1,sp.Y);
root=mrg(sp.X,sp0.Y);
}
int _sum()/*sum操作*/{return sum(root)[3];}
}trp;
signed main(){
cin>>qu;
trp.init();//fhq-Treap初始化
while(qu--){
string tp;int x;
cin>>tp;
if(tp=="add")cin>>x,trp.add(x);
else if(tp=="del")cin>>x,trp.del(x);
else cout<<trp._sum()<<"
";
}
return 0;
}
(4)种解法的比较
离散化+线段树 | 动态开点线段树 | 分块 | 平衡树 | |
---|---|---|---|---|
操作响应时间 | 离线 | 在线 | 在线 | 在线 |
时间复杂度 | (mathrm O(qlog q)) | (mathrm O(qlog q)) | (mathrm O(qsqrt q)) | (mathrm O(qlog q)) |
空间复杂度 | (mathrm O(q)) | (mathrm O(qlog q)) | (mathrm O(q)) | (mathrm O(q)) |
最慢的点运行时长 | (996mathrm{ms}) | (1340mathrm{ms}) | (2494mathrm{ms}) | (1746mathrm{ms}) |
看起来平衡树全方位吊打其他所有解法,但是它的常数还是比不过线段树。