[Codeforces455D]Serega and Fun(分块+双向链表)

=== ===

这里放传送门

=== ===

题解

没错这不是块状链表而是分块+链表。。
记得去年省队集训的时候学长出过这题然后ATP和它的小伙伴们作为场外打酱油人员为了30分部分分狂写一波Splay。。。
然后当时学长讲的标算是块状链表。。。然后ATP后来又看到这题的时候第一反应也是块状链表。。那块链咋做?排序+二分? O(nnlogn) 的渐进复杂度的话这常数得多小才能过啊?。。那在每一块内维护数字出现次数?但是块链那个东西分来分去合来合去说不定块搞的乱七八糟然后M飞了咋办啊。。。而且还要带着那个信息维护常数肯定大如狗写出来肯定也是T。。。
那么我们现在需要的是一种 O(nn) 的做法,那就要支持 O(1) 插入和删除?如果不弃掉刚才分块维护每一块之内数字出现次数的那个思路的话,这个做法还得资瓷分块?

链表是个好东西。。。
如果分块以后对每一块维护表头表尾还有表的大小,插入删除首先没问题了,但是注意的问题是一个元素前面可能插入了很多元素,所以元素标号不一定是元素位置,所以读入x和y以后需要用一个过程查一下它实际上是什么位置的东西。这个过程实际上跟块链很相似。。但是因为块固定只有 O(n) 个所以时间复杂度是不成问题的。
修改的时候先定位,然后插入元素,然后更新表的大小和记录每个块内元素出现情况的数组。然后就是删除元素,如果这个元素原来是表头或者表尾的话就要维护一下,并且如果这个元素是现在这个表中唯一一个元素——因为有频繁的插入和删除操作所以这种情况是很普遍的——就要把这个表标记一下说明它是空表,查找的时候直接跳过这样的表就可以了。
因为在块链的时候有一个维持块平衡的操作所以不会出现很大或者很小的块,但这里省去了这个操作就可能会有操作使得块的大小失去平衡。所以解决的方法就是每过 O(n) 个插入操作以后就把整个数组重构成一个好看一点的表,就是先根据原来记录的pre和nxt指针还原当前数组以后再重新build。这样就能把时间复杂度控制在 O(nn) 的范围内。

代码

#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,a[100010],nxt[100010],pre[100010],T[260],Block,cnt,lastans,K[260][100010];
int size[260],rec,Limit,H[260],pos[100010];
void BuildBlocks(){
    int tail;
    cnt=0;
    for (int i=1;i<=n;i=tail+1){
        tail=min(i+Block-1,n);
        ++cnt;T[cnt]=tail;H[cnt]=i;
        for (int j=i;j<=tail;j++){
            pos[j]=cnt;K[cnt][a[j]]++;
            ++size[cnt];
        }
    }
}
void ClearBlocks(){
    int ptr=0;
    memset(pos,0,sizeof(pos));
    for (int i=1;i<=cnt;i++){
        for (int j=H[i];j!=-1;j=nxt[j]){
            pos[++ptr]=a[j];
            K[i][a[j]]--;//清空计数器数组
            if (j==T[i]) break;
        }
        H[i]=T[i]=size[i]=0;
    }
    memcpy(a,pos,sizeof(pos));
    for (int i=1;i<=n;i++){
        nxt[i]=i+1;pre[i]=i-1;
    }
}
int find(int x){
    int now=1;
    while (H[now]==-1) ++now;
    while (x-size[now]>0){
        x-=size[now];++now;
        while (H[now]==-1) ++now;
    }//根据读入的编号找到这个数实际上的编号
    now=H[now];--x;
    while (x!=0){now=nxt[now];--x;}
    return now;
}
void Move(int x,int y){
    int px,py;
    if (x==y) return;//注意此时及时return
    x=find(x);y=find(y);
    px=pos[x];py=pos[y];
    ++size[px];--size[py];
    ++K[px][a[y]];--K[py][a[y]];
    if (y==T[py])//判断是否需要修改头尾指针
      if (size[py]==0) T[py]=H[py]=-1;
      else T[py]=pre[y];
    else if (y==H[py])
      if (size[py]==0) T[py]=H[py]=-1;
      else H[py]=nxt[y];
    if (pre[y]!=0) nxt[pre[y]]=nxt[y];//注意修改之前要判断一下,不能修改0位置
    if (nxt[y]<=n) pre[nxt[y]]=pre[y];

    pre[y]=pre[x];
    if (pre[x]!=0) nxt[pre[x]]=y;
    pre[x]=y;nxt[y]=x;
    pos[y]=px;
    if (x==H[px]) H[px]=y;
}
int get(int x,int y,int k){
    int ans=(a[y]==k);
    for (;x!=y;x=nxt[x])
      ans+=(a[x]==k);
    return ans;
}
int Count(int x,int y,int k){
    int px,py,ans=0,ptr;
    x=find(x);y=find(y);
    px=pos[x];py=pos[y];
    if (px==py) return get(x,y,k);
    ans+=get(x,T[px],k);
    ans+=get(H[py],y,k);
    ptr=nxt[T[px]];
    while (ptr!=H[py]){
        px=pos[ptr];
        ans+=K[px][k];
        ptr=nxt[T[px]];
    }
    return ans;
}
int main()
{
    scanf("%d",&n);Block=400;
    Limit=ceil(sqrt(n));
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    for (int i=1;i<=n;i++){
        nxt[i]=i+1;pre[i]=i-1;
    }
    BuildBlocks();
    scanf("%d",&m);
    for (int i=1;i<=m;i++){
        int opt,l,r,k;
        scanf("%d%d%d",&opt,&l,&r);
        l=(l+lastans-1)%n+1;
        r=(r+lastans-1)%n+1;
        if (l>r) swap(l,r);
        if (opt==1){
            ++rec;Move(l,r);
            if (rec%Limit==0){
                ClearBlocks();
                BuildBlocks();
            }
        }else{
            scanf("%d",&k);
            k=(k+lastans-1)%n+1;
            lastans=Count(l,r,k);
            printf("%d\n",lastans);
        }
    }
    return 0;
}

偏偏在最后出现的补充说明

感觉有的时候块链常数太大,如果维护的操作不是很复杂的话还不如直接写分块+重构块。。。但是如果再引入一些什么翻转啊之类的恶心东西的话就另当别论了对吧。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值