[整理](DS)杂题选讲

前言

这篇杂题选讲是准备在学校用的,所以这两天不会发出来。
整理的是最近做到的一些好玩的数据结构题(真的很杂!),有些题是 CF 题所以难度是恶评不要管。
会随做题进度更新,直到讲课之日为止。懒得更了,发出来吧。

CF522D Closest Equals

查询太乱了没有精神,我们套路地按右端点排序,这样就只需要处理每个点左侧的答案了。
而找到一个点左侧的最近相同点是简单的,所以预处理出来。
我们从左往右枚举,有最近相同点的就把距离改到线段树上,查询直接查询区间最小值即可。

const int N=500010;
int n,m,a[N],t[N],pos[N],lst[N],ans[N];
struct Query {
  int l,r,idx;
}q[N];
bool operator < (Query A,Query B){
  return A.r<B.r;
}
struct Node {
  int l,r,wei;
}tr[N<<2];
il void Pushup(int k){
  tr[k].wei=min(tr[ls].wei,tr[rs].wei);
}
void Build(int k,int l,int r){
  tr[k].l=l,tr[k].r=r,tr[k].wei=INF;
  if(l==r)return;
  Build(ls,l,nmid),Build(rs,nmid+1,r);
}
void Modify(int k,int pos,int num){
  if(Thispoint){
    tr[k].wei=num;return;
  }
  if(pos<=tmid)Modify(ls,pos,num);
  else Modify(rs,pos,num);
  Pushup(k);
}
int Query(int k,int l,int r){
  if(l<=tr[k].l&&tr[k].r<=r){
    return tr[k].wei;
  }
  int res=INF;
  if(l<=tmid)res=min(res,Query(ls,l,r));
  if(tmid<r)res=min(res,Query(rs,l,r));
  return res;
}
int main(){
  Read(n),Read(m);
  for(rg int i=1;i<=n;i++)Read(a[i]),t[i]=a[i];
  sort(t+1,t+1+n);int tot=unique(t+1,t+1+n)-t-1;
  for(rg int i=1;i<=n;i++){
    a[i]=lower_bound(t+1,t+1+tot,a[i])-t;
    lst[i]=pos[a[i]],pos[a[i]]=i;
  }
  for(rg int i=1;i<=m;i++){
    Read(q[i].l),Read(q[i].r),q[i].idx=i;
  }
  sort(q+1,q+1+m),Build(1,1,n);
  for(rg int i=1,now=1;i<=n;i++){
    if(lst[i])Modify(1,lst[i],i-lst[i]);
    while(q[now].r==i){
      ans[q[now].idx]=Query(1,q[now].l,i);
      if(ans[q[now].idx]==INF)ans[q[now].idx]=-1;
      now++;
    }
  }
  for(rg int i=1;i<=m;i++)cout<<ans[i]<<endl;
  KafuuChino HotoKokoa
}

CF911G Mass Change Queries

题目只要求最终序列,我们可以考虑有什么便于修改的方法。
首先可以注意到数字只有 \(100\)(然而我一开始做这题并没有注意到),然后就可以考虑分别维护每种数字。
对于每种数字,我们发现维护它在每个区间出现的次数可以方便地进行修改:修改时把线段树 \(x\)\([l,r]\) 拿出来放到线段树 \(y\) 上即可。
然后这题就是线段树分裂板子题了。

const int N=200010;
int n,m,a[N<<1],rt[N<<1],tot,cnt,ans[N];
struct Node {
  int l,r,wei;
}tr[N*100];
int rb[N*100],tp;
il int New(){
  if(tp)return rb[tp--];
  return ++tot;
}
il void Pushup(int k){
  tr[k].wei=tr[ls].wei+tr[rs].wei;
}
void Delete(int &k){
  ls=rs=tr[k].wei=0,rb[++tp]=k,k=0;
}
void Merge(int &k,int &u,int l=1,int r=n){
  if(!k||!u)return k+=u,(void)0;
  if(l==r){
    tr[k].wei+=tr[u].wei,Delete(u);
    return;
  }
  Merge(ls,tr[u].l,l,nmid),Merge(rs,tr[u].r,nmid+1,r);
  Delete(u),Pushup(k);
}
void Split(int &k,int &u,int L,int R,int l=1,int r=n){
  if(!k)return;
  if(L<=l&&r<=R){
    return u=k,k=0,(void)0;
  }
  if(!u)u=New();
  if(L<=nmid)Split(ls,tr[u].l,L,R,l,nmid);
  if(nmid<R)Split(rs,tr[u].r,L,R,nmid+1,r);
  Pushup(k),Pushup(u);
}
void Modify(int &k,int pos,int l=1,int r=n){
  if(!k)k=New();
  if(l==r){
    tr[k].wei=1;
    return;
  }
  if(pos<=nmid)Modify(ls,pos,l,nmid);
  else Modify(rs,pos,nmid+1,r);
  Pushup(k);
}
void Query(int k,int num,int l=1,int r=n){
  if(!k)return;
  if(l==r){
    ans[l]+=tr[k].wei*num;
    return;
  }
  Query(ls,num,l,nmid);
  Query(rs,num,nmid+1,r);
}
signed main(){
  Read(n),cnt=n;
  for(rg int i=1;i<=n;i++){
    Read(a[i]),Modify(rt[a[i]],i);
  }
  Read(m);
  for(rg int i=1,l,r,x,y;i<=m;i++){
    Read(l),Read(r),Read(x),Read(y);
    if(rt[x]&&x!=y){
      a[++cnt]=y;
      int t=0;Split(rt[x],t,l,r);
      Merge(rt[y],t);
    }
  }
  sort(a+1,a+1+cnt);
  int t=unique(a+1,a+1+cnt)-a-1;
  for(rg int i=1;i<=t;i++){
    Query(rt[a[i]],a[i]);
  }
  for(rg int i=1;i<=n;i++)cout<<ans[i]<<" ";
  KafuuChino HotoKokoa
}

洛谷P2839 [国家集训队]middle

先考虑简化的问题:静态中位数?二分即可。
如果变成动态?我们想要使答案更大,那么就需要在二分时让大于二分中点的数越多,稍微想一下就会发现要维护区间和、区间前/后缀和最大值。
但是怎么维护呢?当场建树显然是不行的。
提前把所有数的线段树处理好?空间要爆炸了。
等等,空间爆炸?发现当前数字变化时只有一个位置的大小关系发生了改变,很符合主席树保留信息的特点,于是这题就做完了。

const int N=20010;
int n,Q,rt[N],tot,lst;
pii a[N];
struct Node {
  int l,r,wei,lm,rm;
}tr[N*30];
il void Pushup(int k){
  tr[k].wei=tr[ls].wei+tr[rs].wei;
  tr[k].lm=max(tr[ls].lm,tr[ls].wei+tr[rs].lm);
  tr[k].rm=max(tr[rs].rm,tr[rs].wei+tr[ls].rm);
}
void Build(int &k,int l,int r){
  if(!k)k=++tot;
  if(l==r){
    tr[k].lm=tr[k].rm=tr[k].wei=1;
    return;
  }
  Build(ls,l,nmid),Build(rs,nmid+1,r);
  Pushup(k);
}
void Insert(int &k,int u,int p,int num,int l,int r){
  k=++tot;
  tr[k]=tr[u];
  if(l==r){
    tr[k].lm=tr[k].rm=tr[k].wei=num;
    return;
  }
  if(p<=nmid)Insert(ls,tr[u].l,p,num,l,nmid);
  else Insert(rs,tr[u].r,p,num,nmid+1,r);
  Pushup(k);
}
int Query(int k,int L,int R,int l,int r){
  if(L<=l&&r<=R){
    return tr[k].wei;
  }
  int res=0;
  if(L<=nmid)res+=Query(ls,L,R,l,nmid);
  if(nmid<R)res+=Query(rs,L,R,nmid+1,r);
  return res;
}
int QueryL(int k,int L,int R,int l,int r){
  if(L<=l&&r<=R){
    return tr[k].lm;
  }
  if(L>nmid)return QueryL(rs,L,R,nmid+1,r);
  else if(R<=nmid)return QueryL(ls,L,R,l,nmid);
  else {
    int t=Query(ls,L,nmid,l,nmid)+
    QueryL(rs,nmid+1,R,nmid+1,r);
    return max(QueryL(ls,L,nmid,l,nmid),t);
  }
}
int QueryR(int k,int L,int R,int l,int r){
  if(L<=l&&r<=R){
    return tr[k].rm;
  }
  if(L>nmid)return QueryR(rs,L,R,nmid+1,r);
  else if(R<=nmid)return QueryR(ls,L,R,l,nmid);
  else {
    int t=Query(rs,nmid+1,R,nmid+1,r)+
    QueryR(ls,L,nmid,l,nmid);
    return max(QueryR(rs,nmid+1,R,nmid+1,r),t);
  }
}
bool Check(int m,int A,int B,int C,int D){
  int res=0;m--;
  if(B+1<=C-1)res+=Query(rt[m],B+1,C-1,1,n);
  res+=QueryR(rt[m],A,B,1,n)+QueryL(rt[m],C,D,1,n);
  return res>=0;
}
int main(){
  Read(n);
  for(rg int i=1;i<=n;i++){
    Read(a[i].fi),a[i].se=i;
  }
  sort(a+1,a+1+n),Read(Q);
  Build(rt[0],1,n);
  for(rg int i=1;i<=n;i++){
    Insert(rt[i],rt[i-1],a[i].se,-1,1,n);
  }
  for(rg int i=1,q[4];i<=Q;i++){
    for(rg int j=0;j<4;j++){
      Read(q[j]),q[j]=(q[j]+lst)%n+1;
    }
    sort(q,q+4);
    int l=1,r=n,res;
    while(l<=r){
      if(Check(nmid,q[0],q[1],q[2],q[3])){
        res=nmid,l=nmid+1;
      }else r=nmid-1;
    }
    cout<<a[res].fi<<endl,lst=a[res].fi;
  }
  KafuuChino HotoKokoa
}

洛谷P5283 [十二省联考2019]异或粽子

如果有不会 01Trie 的同学请自行学习。
如果有不会可持久化 01Trie 的同学请自行学习。
我们从一个问题引入。
先前缀和,然后区间异或转化为两个元素异或,查询其实就是给定一个值 \(v\),求区间 \([l,r]\) 中元素异或 \(v\) 的最大值。
右端点的维护很容易,左端点比较麻烦:需要记录子树中最大编号。这样,如果最大编号 \(<l\) 就不进入。
回到此题,考虑如何转化成上面的形式。
一个简单的做法是将 \(O(n^2)\) 个异或值全部加入大根堆里,而这显然是不行的。
有没有可能少加一些呢?
对于一个右端点 \(i\),我们固定它,再从 \([0,i-1]\) 里找左端点,求出 \(n\) 个最大值加入堆。
每次弹出后(假设弹出元素右端点为 \(ed\),左端点区间为 \([l,r]\),最大值位置\(pos\))将左端点的区间按照 \(pos\) 分裂,重新加入。
容易发现,这样的加入方式是不重不漏的。(懒得证也不会证)
代码里有很多细节,比如分裂时端点的处理、运算符优先级long long 等等。

const int N=500010;
int n,k,a[N],rt[N],ans;
struct Trie {
  int ch[N<<6][2],mx[N<<6],tot;
  void Insert(int &k,int u,int idx,int pos){
    if(!k)k=++tot;
    if(pos<0)return mx[k]=idx,(void)0;
    bool b=a[idx]&(1ll<<pos);
    if(u)rs=ch[u][!b];
    Insert(ls,ch[u][b],idx,pos-1);
    mx[k]=max(mx[ls],mx[rs]);
  }
  il int Query(int k,int num,int lim){
    for(rg int i=32;~i;i--){
      bool b=num&(1ll<<i);
      if(mx[rs]>=lim)k=rs;
      else k=ls;
    }
    return mx[k];
  }
}T;
struct Node {
  int ed,l,r,pos;
  Node(int _e=0,int _l=0,int _r=0,int _p=0){
    ed=_e,l=_l,r=_r,pos=_p;
  }
};
bool operator < (Node A,Node B){
  return (a[A.ed]^a[A.pos])<(a[B.ed]^a[B.pos]);
}
priority_queue<Node> q;
signed main(){
  Read(n),Read(k);
  T.mx[0]=-1,T.Insert(rt[0],0,0,32);
  for(rg int i=1;i<=n;i++){
    Read(a[i]),a[i]^=a[i-1];
    T.Insert(rt[i],rt[i-1],i,32);
    int p=T.Query(rt[i-1],a[i],0);
    q.push(Node(i,0,i-1,p));
  }
  while(k--){
    Node u=q.top();q.pop();
    ans+=a[u.ed]^a[u.pos];
    if(u.pos!=u.r){
      int p=T.Query(rt[u.r],a[u.ed],u.pos+1);
      q.push(Node(u.ed,u.pos+1,u.r,p));
    }
    if(u.pos!=u.l){
      int p=T.Query(rt[u.pos-1],a[u.ed],u.l);
      q.push(Node(u.ed,u.l,u.pos-1,p));
    }
  }
  cout<<ans<<endl;
  KafuuChino HotoKokoa
}

这里附上分裂时不特判的后果:

CF515E Drazil and Park

题目中的这个能量消耗很难受,尝试化一化。
断环为链、前缀和等基本技巧不讲了。
题目中给出了式子:\(2\times(h_x+h_y)+d_y-d_x\)
想要在每个点上单独维护一些东西,就考虑把一样的放一起:\(x\) 作为左端点时的贡献为 \(2\times h_x-d_x\),作为右端点时的贡献为 \(2\times h_x+d_x\),那么就可以在线段树上维护这两个东西。
具体来说,维护全局最大值、前缀中做右端点的最大值、后缀中做左端点的最大值就可以完美地更新、查询了(注意这里有点绕,仔细看)。
然后就是细节的处理,比如 \(d_i\) 对应了谁和谁之间的距离、查询时的区间等等。

const int N=200010;
int n,m,d[N],h[N];
struct Node {
  int l,r,lm,rm,mx;
}tr[N<<4];
il void Pushup(int k){
  tr[k].lm=max(tr[ls].lm,tr[rs].lm);
  tr[k].rm=max(tr[ls].rm,tr[rs].rm);
  int J=max(tr[ls].mx,tr[rs].mx);
  tr[k].mx=max(J,tr[ls].rm+tr[rs].lm);
}
void Build(int k,int l,int r){
  tr[k].l=l,tr[k].r=r;
  if(l==r){
    tr[k].lm=h[l]+d[l-1],tr[k].rm=h[r]-d[r-1];
    tr[k].mx=-INF;return;
  }
  Build(ls,l,nmid),Build(rs,nmid+1,r);
  Pushup(k);
}
Node Query(int k,int l,int r){
  if(l<=tr[k].l&&tr[k].r<=r){
    return tr[k];
  }
  Node re1,re2,res;
  re1.lm=re1.rm=re1.mx=re2.lm=re2.rm=re2.mx=-INF;
  if(l<=tmid)re1=Query(ls,l,r);
  if(tmid<r)re2=Query(rs,l,r);
  res.lm=max(re1.lm,re2.lm);
  res.rm=max(re1.rm,re2.rm);
  int J=max(re1.mx,re2.mx);
  res.mx=max(J,re1.rm+re2.lm);
  return res;
}
signed main(){
  Read(n),Read(m);
  for(rg int i=1;i<=n;i++)Read(d[i]),d[i+n]=d[i];
  for(rg int i=1;i<=n;i++)Read(h[i]),h[i+n]=h[i];
  for(rg int i=1;i<=n+n;i++)d[i]+=d[i-1],h[i]<<=1;
  Build(1,1,n+n);
  for(rg int i=1,l,r;i<=m;i++){
    Read(l),Read(r);
    Node res;
    if(l<=r)res=Query(1,r+1,l-1+n);
    else res=Query(1,r+1,l-1);
    cout<<res.mx<<endl;
  }
  KafuuChino HotoKokoa
}

洛谷P4108 [HEOI2015]公约数数列

看到了莫名其妙的操作和莫名其妙的范围,于是大力分块。
那么每个块内要维护什么信息呢?我们先来看一看运算的性质。
1.所有查询都是前缀;
2.前缀最大公约数递减且最多有 \(\log n\) 个值(证明显然)。
现在我们就有一点思路了,块内 \(\gcd\) 和块内 \(\operatorname{xor}\) 肯定是要处理出来的,再维护前缀 \(\gcd\) 和块内前缀 \(\operatorname{xor}\)
修改可以暴力重构块,查询从前往后按块查,分两种情况:
1.块内 \(\gcd\) 没有发生变化,块内查找合适的 \(\operatorname{xor}\)
2.发生变化,进块暴力查询,由于前面的性质 \(2\) 最多会查 \(\log n\) 次。
所以最终复杂度 \(O(n\sqrt{n}\log n)\)

const int N=100010;
int n,q,a[N],bel[N],l[N],r[N],B;
int gp[N],xp[N];
int Gcd(int a,int b){
  return !b?a:Gcd(b,a%b);
}
struct Node {
  int val,idx;
  Node(int _v=0,int _i=0){
    val=_v,idx=_i;
  }
}p[N];
bool operator < (Node x,Node y){
  if(x.val==y.val)return x.idx<y.idx;
  return x.val<y.val;
}
il void Calc(int b){
  gp[l[b]]=xp[l[b]]=a[l[b]];
  p[l[b]]=Node(xp[l[b]],l[b]);
  for(rg int i=l[b]+1;i<=r[b];i++){
    gp[i]=Gcd(gp[i-1],a[i]);
    xp[i]=xp[i-1]^a[i];
    p[i]=Node(xp[i],i);
  }
  sort(p+l[b],p+r[b]+1);
}
il int Query(int x){
  int res=-1,g=a[1],o=0;
  for(rg int i=1;i<=bel[n]&&res==-1;i++){
    if(Gcd(g,gp[r[i]])==g){
      if(x%g==0){
        int L=l[i],R=r[i],val=(x/g)^o,pos=L;
        while(L<=R){
          int m=L+R>>1;
          if(p[m].val>=val)pos=m,R=m-1;
          else L=m+1;
        }
        if(p[pos].val==val){
          res=p[pos].idx;break;//讲个笑话,这里写成 res=pos 能过样例
        }
      }
      o^=xp[r[i]];
    }else {
      for(rg int j=l[i];j<=r[i];j++){
        g=Gcd(g,a[j]),o^=a[j];
        if(g*o==x){
          res=j;break;
        }
      }
    }
  }
  return res;
}
signed main(){
  Read(n),B=sqrt(n);
  for(rg int i=1;i<=n;i++){
    Read(a[i]),bel[i]=(i-1)/B+1;
    if(!l[bel[i]])l[bel[i]]=i;r[bel[i]]=i;
  }
  for(rg int i=1;i<=bel[n];i++)Calc(i);
  Read(q);
  for(rg int i=1,x,y;i<=q;i++){
    char opt[10];scanf("%s",opt);
    if(opt[0]=='M'){
      Read(x),Read(y),a[++x]=y;
      Calc(bel[x]);
    }else {
      Read(x),y=Query(x);
      if(y==-1)puts("no");
      else cout<<y-1<<endl;
    }
  }
}
posted @ 2021-05-10 08:42  ajthreac  阅读(106)  评论(0编辑  收藏  举报