bzoj3337 ORZJRY I(块状链表)

本文详细介绍了块状链表的数据结构及其在各种操作中的应用,包括插入、删除、区间翻转等高级操作,并通过具体代码实现展示了如何高效地维护一个动态序列。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description

Jry最近做(屠)了很多数据结构题,所以想 BS你,他希望你能实现一种数据结构维护一个序列:
这里写图片描述

Input

第一行n;
第二行n个数;
第三行q,代表询问个数;
接下来q行,每行一个op,输入格式见描述。

Output

对于7≤op≤11的操作,一行输出一个答案。

Sample Input

6
5 2 6 3 1 4
15
7 2 4
8 1 3
9 2 4 5
10 1 6 4
11 2 5 4
6 1 4 7
8 1 4
5 3 4 5
2 1
1 2 8
3 3 5
4 1 5 2
9 2 5 4
10 3 6 4
11 1 6 100

Sample Output

11
4
1
4
3
0
3
12
6

HINT

n,q≤100000
任意时刻数列中的数≤2^31-1
0≤任意时刻数列中的数≤2^31-1

本题共3组数据


分析:

  • opt=1
    块状链表的基本操作:find插入的位置,分裂成两块,单元素插入
  • opt=2
    块状链表的基本操作:find插入位置,直接在第now块O(n)修改即可
  • opt=3
    查找到[x,y]区间,分裂出来,设置一个翻转标记,翻转整个区间(每个块打上翻转标记,链表的连接方式也翻过来)
  • opt=4
    查找到[x,x+k-1]和[x+k,y]两个区间,直接变换指针
  • opt=5
    查找到[x,y]区间,分裂出来,设置一个覆盖标记,每个块都打上覆盖标记
  • opt=6
    查找到[x,y]区间,分裂出来,设置一个修改标记,每个块都打上修改标记
  • opt=7
    每个块维护一个sum值,直接询问[x,y]的和
  • opt=8
    在每个块内维护一个新的数组s,把块内的元素按照从小到大排好序,直接查询最大值和最小值即可
  • opt=9
    查找到[x,y]区间,分裂出来,在每个块的s数组中二分查找询问元素,记录前驱后继与询问元素的差值最小值
  • opt=10
    二分答案,转化成判定问题:查找到[x,y]区间,分裂出来,在每个块的s数组中二分查找询问元素,统计比询问元素的小的元素个数
  • opt=11
    询问10的子问题

块状链表的基本操作还是比较好些好想的,然而这道题的扩展操作比较多,我们来看一看code:

初始化

块内维护信息:
d:原数组
s:从小到大排序后的数组
rev:翻转标记
delta:加标记
same:覆盖标记
size:块内的元素个数
sum:块内元素和
nxt:链指针

struct node{
    int d[1003],s[1003];
    int rev,delta,same,size,nxt;
    ll sum;
};
node a[N];
queue<int> q;   //内存池

void clear(int x)
{
    a[x].sum=a[x].rev=a[x].delta=a[x].same=a[x].size=0;
    a[x].nxt=-1;
}

int newnode()    //申请新结点
{
    int t=q.front(); q.pop();
    return t;
}

int del(int x)   //删除结点,恢复到内存池
{
    q.push(x); clear(x);
}

int init()       //初始化
{
    for (int i=1;i<N;i++) q.push(i);
    a[0].nxt=-1; a[0].size=0;
}

定位

void find(int &pos,int &now)
{
    for (now=0;a[now].nxt!=-1&&pos>a[now].size;now=a[now].nxt) pos-=a[now].size;
}

合并

合并的思路还是从now开始一路遍历下去
如果相邻块的大小之和小于blocksize(sqrt(n)),我们就进行合并

在合并之前,我们要保证块内的信息是稳定的,因此我们要标记下放

合并完成之后,每一块的信息就有变化了

我们就要重新维护块的信息
void merge(int now)    //now+now.nxt ---> now 
{
    int t=a[now].nxt;
    push(now); push(t);   
    for (int i=1;i<=a[t].size;i++)
        a[now].d[++a[now].size]=a[t].d[i];
    a[now].nxt=a[t].nxt;
    del(t);
    update(now);          
}

void maintain(int now)     //合并小块 
{
    for (;now!=-1;now=a[now].nxt)
        if (a[now].nxt!=-1&&a[now].size+a[a[now].nxt].size<=blocksize)
            merge(now);
}

维护信息

信息的维护分成两种:

  • 维护s数组和sum:我们直接把d数组复制到s数组中,sort一下即可
  • 标记下放:块状链表也不是什么线段树,每个块的大小只有sqrt(n)左右,所以直接O(n)处理即可
void push(int now)
{
    if (a[now].rev)
    {
        a[now].rev=0;
        for (int i=1;i<=a[now].size/2;i++)
            swap(a[now].d[i],a[now].d[a[now].size-i+1]);
    }
    if (a[now].same)
    {
        for (int i=1;i<=a[now].size;i++)
            a[now].d[i]=a[now].same;
        a[now].sum=a[now].size*a[now].same;
        a[now].same=0;
    }
    if (a[now].delta)
    {
        for (int i=1;i<=a[now].size;i++)
            a[now].d[i]+=a[now].delta;
        a[now].sum+=a[now].size*a[now].delta;
        a[now].delta=0;
    }
} 

void update(int x)
{
    a[x].sum=0;
    for (int i=1;i<=a[x].size;i++)
        a[x].s[i]=a[x].d[i],
        a[x].sum+=a[x].d[i];
    sort(a[x].s+1,a[x].s+1+a[x].size);    //维护顺序 
}

分裂

申请新结点,把后半部分直接复制到新结点中,不要忘了维护指针

要注意:分裂点我们划分到了下一块

这个性质关乎之后的操作

void divide(int now,int pos)   //分裂 pos分到了下一块 
{
    push(now);
    int t=newnode();
    for (int i=pos;i<=a[now].size;i++)
        a[t].d[++a[t].size]=a[now].d[i];
    a[t].nxt=a[now].nxt; a[now].nxt=t;
    a[now].size=max(pos-1,0);
    update(t);
    update(now);
}

单点插入

我们先要定位插入的位置:第now块的第pos位
之后把第now块分裂开
因为在分裂的时候pos分裂到了后一块,所以一开始的时候要pos++
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void insert(int pos,int val)
{
    int now; pos++;
    find(pos,now); divide(now,pos);
    a[now].d[++a[now].size]=val;
    update(now);
    maintain(now);
}

单点删除

因为是单点删除,我们不用分裂,直接把后面的元素前移即可
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void erase(int pos)
{
    int now;
    find(pos,now); push(now);
    for (int i=pos;i<a[now].size;i++)
        a[now].d[i]=a[now].d[i+1];
    a[now].size--;
    update(now);
    maintain(now);
}

分裂区间

这个函数非常的重要
调用后返回的lp和rp就是:
这里写图片描述
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void solve(int l,int r,int &lp,int &rp)
{
    int pos=l;
    find(pos,lp); divide(lp,pos);
    pos=r+1; find(pos,rp); divide(rp,pos);
    pos=r; find(pos,rp);
}

区间翻转

我们需要一个辅助数组,把区间的块信息记录下来
方便翻转区间链接方式
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void reverse(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int now=lp,top=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        st[++top]=i,a[i].rev^=1;

    a[st[1]].nxt=a[rp].nxt;    //整个区间翻转 
    for (int i=top;i>1;i--)
        a[st[i]].nxt=st[i-1];
    a[lp].nxt=rp;
    maintain(lp);
}

区间右移

这里写图片描述
唯一需要注意的就是指针的修改
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void move(int x,int y,int z)
{
    int lp,rp,mp,np;
    solve(x,y-z,lp,mp);
    solve(y-z+1,y,mp,rp);
    np=a[lp].nxt;
    a[lp].nxt=a[mp].nxt;
    a[mp].nxt=a[rp].nxt;
    a[rp].nxt=np;
    maintain(lp);
}

区间修改+询问区间和

这个操作比较简单
注意我们在修改的时候,直接维护了sum值
因为块的大小和信息都有变化,所以我们调用一下maintain(合并小块)

void add(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        a[i].delta+=z;
        a[i].sum+=a[i].size*z;
    }    
    maintain(lp);
}

void same(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        a[i].delta=0;
        a[i].same=z;
        a[i].sum=a[i].size*z;
    }
    maintain(lp);
}

ll sum(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    ll ans=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        ans+=a[i].sum;
    maintain(lp);
    return ans;
}

询问区间最值

在每个结点中,我们维护了一个s数组,所以我们可以直接从s数组中查找

但是有一个问题:我们在调用这个块的时候,块的信息可能没有push过

如果我们每访问一个块就push一次,O(n)的复杂度不太划算

因为修改操作只有加和区间覆盖,是不会影响元素之间的大小关系的

所以我们直接把delta累加到元素上即可
(有覆盖标记的块需要特判一下)

因为我们调用了分裂操作,块的大小有变化,所以我们调用一下maintain(合并小块)

int range(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int mn=INF,mx=-INF;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt) if (a[i].size)
    {
        if (a[i].same)
        {
            mn=min(mn,a[i].same+a[i].delta);    //可能没有push 
            mx=max(mx,a[i].same+a[i].delta);
        }
        else
        {
            mn=min(mn,a[i].s[1]+a[i].delta);
            mx=max(mx,a[i].s[a[i].size]+a[i].delta);
        }
    }       
    maintain(lp);
    return mx-mn;
}

询问最小差值

在每个结点中,我们维护了一个s数组
我们可以直接在s数组中调用lower_bound函数,找到大于等于询问元素的第一个位置

注意:我们在调用这个块的时候,块的信息可能没有push过

我们要把delta累加到元素上
(有覆盖标记的块需要特判一下)

因为我们调用了分裂操作,块的大小有变化,所以我们调用一下maintain(合并小块)

int near(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int ans=INF;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        if (a[i].same) ans=min(ans,abs(a[i].same+a[i].delta-z));
        else
        {
            int x=lower_bound(a[i].s+1,a[i].s+a[i].size+1,z-a[i].delta)-a[i].s;
            if (x!=a[i].size+1)
                ans=min(ans,abs(a[i].s[x]+a[i].delta-z));
            if (x!=1) 
                x--,ans=min(ans,abs(a[i].s[x]+a[i].delta-z));
        }
    }
    maintain(lp);
    return ans;
}  

询问第K值

我们二分一个答案M
统计比M小的元素个数,计算出M的排名
在s数组中调用upper_bound函数,找到大于询问元素的第一个位置x
那么x-1就是小于M的元素个数

还是这个问题:我们在调用这个块的时候,块的信息可能没有push过

我们要把delta累加到元素上
(有覆盖标记的块需要特判一下)

因为我们调用了分裂操作,块的大小有变化,所以我们调用一下maintain(合并小块)

int min_k(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int L=0,R=INF,ans;
    while (L<R)
    {
        int M=((L+R)>>1)+1;
        int sum=1;    //排名 
        for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        {
            if (a[i].same)
            {
                if (a[i].same+a[i].delta<M) sum+=a[i].size;
            }
            else
            {
                int x=upper_bound(a[i].s+1,a[i].s+a[i].size+1,M-a[i].delta-1)-a[i].s;
                sum+=max(x-1,0);
            }
        }
        if (sum<=z) L=M;
        else R=M-1;
    }
    maintain(lp);
    return L;
}

询问小于某元素的元素个数

上一种询问的子问题

int ask_xiao(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int ans=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        if (a[i].same)
        {
            if (a[i].same+a[i].delta<z) ans+=a[i].size;
        } 
        else
        {
            int x=upper_bound(a[i].s+1,a[i].s+a[i].size+1,z-a[i].delta-1)-a[i].s;
            ans+=max(x-1,0);
        }
    }
    maintain(lp);
    return ans;
} 

tip

一开始TLE了
发现是move的时候maintain传入的起点错了
这样就会导致小的块没有及时合并,时间退化的比较厉害

lower_bound和upper_bound的应用要注意

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cmath>
#define ll long long

using namespace std;

const int INF=1e9;
const int N=10000;
int n,m,blocksize,st[N],b[N];
struct node{
    int d[1003],s[1003];
    int rev,delta,same,size,nxt;
    ll sum;
};
node a[N];
queue<int> q;

void clear(int x)
{
    a[x].sum=a[x].rev=a[x].delta=a[x].same=a[x].size=0;
    a[x].nxt=-1;
}

int newnode()
{
    int t=q.front(); q.pop();
    return t;
}

int del(int x)
{
    q.push(x); clear(x);
}

int init()
{
    for (int i=1;i<N;i++) q.push(i);
    a[0].nxt=-1; a[0].size=0;
}

void find(int &pos,int &now)
{
    for (now=0;a[now].nxt!=-1&&pos>a[now].size;now=a[now].nxt) pos-=a[now].size;
}

void push(int now)
{
    if (a[now].rev)
    {
        a[now].rev=0;
        for (int i=1;i<=a[now].size/2;i++)
            swap(a[now].d[i],a[now].d[a[now].size-i+1]);
    }
    if (a[now].same)
    {
        for (int i=1;i<=a[now].size;i++)
            a[now].d[i]=a[now].same;
        a[now].sum=a[now].size*a[now].same;
        a[now].same=0;
    }
    if (a[now].delta)
    {
        for (int i=1;i<=a[now].size;i++)
            a[now].d[i]+=a[now].delta;
        a[now].sum+=a[now].size*a[now].delta;
        a[now].delta=0;
    }
} 

void update(int x)
{
    a[x].sum=0;
    for (int i=1;i<=a[x].size;i++)
        a[x].s[i]=a[x].d[i],
        a[x].sum+=a[x].d[i];
    sort(a[x].s+1,a[x].s+1+a[x].size);    //维护顺序 
}

void merge(int now)    //now+now.nxt ---> now 
{
    int t=a[now].nxt;
    push(now); push(t);
    for (int i=1;i<=a[t].size;i++)
        a[now].d[++a[now].size]=a[t].d[i];
    a[now].nxt=a[t].nxt;
    del(t);
    update(now);
}

void maintain(int now)     //合并小块 
{
    for (;now!=-1;now=a[now].nxt)
        if (a[now].nxt!=-1&&a[now].size+a[a[now].nxt].size<=blocksize)
            merge(now);
}

void divide(int now,int pos)   //分裂 pos分到了下一块 
{
    push(now);
    int t=newnode();
    for (int i=pos;i<=a[now].size;i++)
        a[t].d[++a[t].size]=a[now].d[i];
    a[t].nxt=a[now].nxt; a[now].nxt=t;
    a[now].size=max(pos-1,0);
    update(t);
    update(now);
}

void insert(int pos,int val)
{
    int now; pos++;
    find(pos,now); divide(now,pos);
    a[now].d[++a[now].size]=val;
    update(now);
    maintain(now);
}

void erase(int pos)
{
    int now;
    find(pos,now); push(now);
    for (int i=pos;i<a[now].size;i++)
        a[now].d[i]=a[now].d[i+1];
    a[now].size--;
    update(now);
    maintain(now);
}

void solve(int l,int r,int &lp,int &rp)
{
    int pos=l;
    find(pos,lp); divide(lp,pos);
    pos=r+1; find(pos,rp); divide(rp,pos);
    pos=r; find(pos,rp);
}

void reverse(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int now=lp,top=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        st[++top]=i,a[i].rev^=1;

    a[st[1]].nxt=a[rp].nxt;    //整个区间翻转 
    for (int i=top;i>1;i--)
        a[st[i]].nxt=st[i-1];
    a[lp].nxt=rp;
    maintain(lp);
}

void move(int x,int y,int z)
{
    int lp,rp,mp,np;
    solve(x,y-z,lp,mp);
    solve(y-z+1,y,mp,rp);
    np=a[lp].nxt;
    a[lp].nxt=a[mp].nxt;
    a[mp].nxt=a[rp].nxt;
    a[rp].nxt=np;
    maintain(lp);
}

void add(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        a[i].delta+=z;
        a[i].sum+=a[i].size*z;
    }    
    maintain(lp);
}

void same(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        a[i].delta=0;
        a[i].same=z;
        a[i].sum=a[i].size*z;
    }
    maintain(lp);
}

ll sum(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    ll ans=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        ans+=a[i].sum;
    maintain(lp);
    return ans;
}

int range(int x,int y)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int mn=INF,mx=-INF;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt) if (a[i].size)
    {
        if (a[i].same)
        {
            mn=min(mn,a[i].same+a[i].delta);    //可能没有push 
            mx=max(mx,a[i].same+a[i].delta);
        }
        else
        {
            mn=min(mn,a[i].s[1]+a[i].delta);
            mx=max(mx,a[i].s[a[i].size]+a[i].delta);
        }
    }       
    maintain(lp);
    return mx-mn;
}

int near(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int ans=INF;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        if (a[i].same) ans=min(ans,abs(a[i].same+a[i].delta-z));
        else
        {
            int x=lower_bound(a[i].s+1,a[i].s+a[i].size+1,z-a[i].delta)-a[i].s;
            if (x!=a[i].size+1)
                ans=min(ans,abs(a[i].s[x]+a[i].delta-z));
            if (x!=1) 
                x--,ans=min(ans,abs(a[i].s[x]+a[i].delta-z));
        }
    }
    maintain(lp);
    return ans;
} 

int min_k(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int L=0,R=INF,ans;
    while (L<R)
    {
        int M=((L+R)>>1)+1;
        int sum=1;    //排名 
        for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
        {
            if (a[i].same)
            {
                if (a[i].same+a[i].delta<M) sum+=a[i].size;
            }
            else
            {
                int x=upper_bound(a[i].s+1,a[i].s+a[i].size+1,M-a[i].delta-1)-a[i].s;
                sum+=max(x-1,0);
            }
        }
        if (sum<=z) L=M;
        else R=M-1;
    }
    maintain(lp);
    return L;
} 

int ask_xiao(int x,int y,int z)
{
    int lp,rp;
    solve(x,y,lp,rp);
    int ans=0;
    for (int i=a[lp].nxt;i!=a[rp].nxt;i=a[i].nxt)
    {
        if (a[i].same)
        {
            if (a[i].same+a[i].delta<z) ans+=a[i].size;
        } 
        else
        {
            int x=upper_bound(a[i].s+1,a[i].s+a[i].size+1,z-a[i].delta-1)-a[i].s;
            ans+=max(x-1,0);
        }
    }
    maintain(lp);
    return ans;
} 

int main()
{
    scanf("%d",&n);
    int opt,x,y,z;
    blocksize=sqrt(n);
    init();
    for (int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        insert(i-1,x);      //0开始 
    }
    scanf("%d",&m);
    for (int i=1;i<=m;i++)
    {
        scanf("%d",&opt);
        switch (opt)
        {
            case 1:scanf("%d%d",&x,&y); insert(x,y); break;
            case 2:scanf("%d",&x); erase(x); break;
            case 3:scanf("%d%d",&x,&y); reverse(x,y); break;
            case 4:scanf("%d%d%d",&x,&y,&z); move(x,y,z); break;
            case 5:scanf("%d%d%d",&x,&y,&z); add(x,y,z); break;
            case 6:scanf("%d%d%d",&x,&y,&z); same(x,y,z); break;
            case 7:scanf("%d%d",&x,&y); printf("%lld\n",sum(x,y)); break;
            case 8:scanf("%d%d",&x,&y); printf("%d\n",range(x,y)); break;
            case 9:scanf("%d%d%d",&x,&y,&z); printf("%d\n",near(x,y,z)); break;
            case 10:scanf("%d%d%d",&x,&y,&z); printf("%d\n",min_k(x,y,z)); break;
            case 11:scanf("%d%d%d",&x,&y,&z); printf("%d\n",ask_xiao(x,y,z)); break;
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值