洛谷题目页面传送门 & CodeForces题目页面传送门
有一个集合\(s\),你需要支持以下\(3\)种\(q\)次操作:
- \(\texttt{add}\ x\):令\(s=s\cup\{x\}\),保证在执行之前\(x\notin s\),\(x\in\left[1,10^9\right]\);
- \(\texttt{del}\ x\):令\(s=s-\{x\}\),保证在执行之前\(x\in s\),\(x\in\left[1,10^9\right]\);
- \(\texttt{sum}\):设将\(s\)中所有元素排序后得到1-indexed序列\(a\),查询\(\sum\limits_{i\in[1,|a|],i\bmod5=3}a_i\)。
\(q\in\left[1,10^5\right]\)。
这其实是一个比较简单的DS题,毕竟难度只有\(^*2200\)。只不过这题比较nb,一题四解。
解法\(1\):离散化+线段树
考虑建一棵值域线段树,设节点\(i\)表示区间\([l_i,r_i]\),那么节点\(i\)存储\([l_i,r_i]\)内\(\in s\)的数的数量\(cnt_i\),和\(\forall j\in[0,5)\),区间\([l_i,r_i]\)内所有\(\in s\)的元素排序后所有下标\(\bmod5=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}\right)\bmod5}\)即可。一次上传时间复杂度\(\mathrm O(1)\)。
由于值域大小为\(10^9\),我们可以将所有操作离线下来并离散化所有数值。
此时\(\texttt{add},\texttt{del}\)操作都是简单的单点修改,\(\mathrm O(\log q)\);\(\texttt{sum}\)操作是整体查询,直接调用\(sum_{root,3}\)即可,\(\mathrm O(1)\)。
总空间复杂度\(\mathrm O(q)\),时间复杂度\(\mathrm O(q\log 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()<<"\n";
}
return 0;
}
解法\(2\):动态开点线段树
线段树部分的思路与解法\(1\)完全一样,不过此方法利用动态开点来处理值域过大问题,实现了在线执行操作。
总空间复杂度\(\mathrm O(q\log q)\),时间复杂度\(\mathrm O(q\log 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()<<"\n";
}
return 0;
}
解法\(3\):分块
分块是无所不能的
其实上面那句话在某种程度上是正确的
考虑对\(s\)中所有元素排序后得到的1-indexed序列\(a\)分块。由于有插入、删除操作,所以这是一个动态调整块内元素的分块。
考虑维护set
\(s\)。块\(i\)内存\(a\)的某一个子段\(a'_i\),和\(\forall j\in[0,5)\),该1-indexed子段\(a'_i\)内所有下标\(\bmod5=j\)的数的和\(sum_{i,j}\)。
扫描:调整所有块内元素使得每块内元素数量不超过\(sz1\)。从左往右依次扫描每个块\(i\),如果\(|a'_i|>sz1\)(此时必然有\(|a'_i|=sz1+1\),因为每次\(\texttt{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}\right)\)。
对于\(\texttt{add}\)操作:通过\(s\)二分找到距离\(x\)最近的\(\in s\)的元素\(x'\),找到\(x'\)所在块并将\(x\)插入该块且维护有序性。暴力重算该块内的\(sum\)。更新\(s\)。扫描一遍。时间复杂度\(\mathrm O\!\left(\log q+sz1+\dfrac q{sz1}\right)\)。
对于\(\texttt{del}\)操作:找到\(x\)所在块并将\(x\)删除。暴力重算该块内的\(sum\)。更新\(s\)。扫描一遍。时间复杂度\(\mathrm O\!\left(sz1+\dfrac q{sz1}\right)\)。
对于\(\texttt{sum}\)操作:从左往右让每个块贡献答案即可,这样是\(\mathrm O\!\left(\dfrac q{sz1}\right)\)的。然鹅其实可以优化到\(\mathrm O(1)\),就是每次扫描时顺便算出答案并记录下来,这样就可以直接调用。
令\(sz1=\left\lfloor \sqrt q\right\rfloor\),这样总时间复杂度\(\mathrm O(q\sqrt q)\)。空间复杂度\(\mathrm O(q)\)。
然后开开森森的交上去,T了?是因为这巨大的复杂度乘上巨大的常数。于是开始卡常。
先将\(\texttt{add},\texttt{del}\)内的“暴力重算该块内的\(sum\)”用指针寻址优化一下(因为deque
的随机访问很慢)。此时\(\texttt{add},\texttt{del}\)前面一部分的常数与最后的扫描的常数形成了鲜明的对比,因为扫描还带一个\(5\)的常数。于是调参要从娃娃抓起,我们将\(sz1\)稍微调大一点,来实现常数上的尽量平衡。令\(sz1=3\left\lfloor \sqrt q\right\rfloor\)即可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()<<"\n";
}
return 0;
}
解法\(4\):平衡树
看到往集合里添加、删除,不难想到平衡树。这里使用fhq-Treap。
节点\(i\)存储此节点的权值\(v_i\),以\(i\)为根的子树的大小\(sz_i\),和\(\forall j\in[0,5)\),以\(i\)为根的子树内所有元素排序后所有下标\(\bmod5=j\)的数的和\(sum_{i,j}\)。那么上传时令\(sz_i=sz_{lson_i}+1+sz_{rson_i},sum_{i,j}=sum_{lson_i,j}+[j=sz_{lson_i}+1]v_i+sum_{rson_i,\left(j-cnt_{lson_i}-1\right)\bmod5}\)即可。一次上传时间复杂度\(\mathrm O(1)\)。
此时\(\texttt{add},\texttt{del}\)操作都是简单的单点插入/删除,\(\mathrm O(\log q)\);\(\texttt{sum}\)操作是整体查询,直接调用\(sum_{root,3}\)即可,\(\mathrm O(1)\)。
总空间复杂度\(\mathrm O(q)\),时间复杂度\(\mathrm O(q\log 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()<<"\n";
}
return 0;
}
\(4\)种解法的比较
离散化+线段树 | 动态开点线段树 | 分块 | 平衡树 | |
---|---|---|---|---|
操作相应时间 | 离线 | 在线 | 在线 | 在线 |
时间复杂度 | \(\mathrm O(q\log q)\) | \(\mathrm O(q\log q)\) | \(\mathrm O(q\sqrt q)\) | \(\mathrm O(q\log q)\) |
空间复杂度 | \(\mathrm O(q)\) | \(\mathrm O(q\log q)\) | \(\mathrm O(q)\) | \(\mathrm O(q)\) |
最慢的点运行时长 | \(996\mathrm{ms}\) | \(1340\mathrm{ms}\) | \(2494\mathrm{ms}\) | \(1746\mathrm{ms}\) |
看起来平衡树全方位吊打其他所有解法,但是它的常数还是比不过线段树。