分治 —— 莫队算法 —— 带修莫队

【概述】

普通莫队由于强制离线是不能修改的,但对于强制在线的题,可以在普通莫队的基础上强行加上一维时间轴 time,表示这次操作的时间,即在每个询问前已经完成了多少次修改

简单来说,就是将询问 [l,r],变为 [l,r,time],那么指针也可在时间维度上移动,使得第一关键字是左端点所在的块,第二关键字是右端点所在的块,第三关键字是时间,即 [l,r,time] 多了一维可移动的方向:

  • [l-1,r,time]
  • [l+1,r,time]
  • [l,r-1,time]
  • [l,r+1,time]
  • [l,r,time-1]
  • [l,r,time+1]
int l=1,r=0,time=0;
for(int i=1;i<=m;++i){
    int ql=q[i].l,qr=q[i].r;//询问的左右端点
    int qtime=q[i].time;//询问的时间轴
    while(l>ql) add(--l);//[l-1,r,time]
    while(l<ql) del(l++);//[l+1,r,time]
    while(r<qr) add(++r);//[l,r+1,time]
    while(r>qr) del(r--);//[l,r-1,time]
    while(time<qtime) change(i,++time);//[l,r,time+1]
    while(time>qtime) change(i,time--);//[l,r,time-1]
    res[q[i].id]=ans;//获取答案
}

暴力查询时,如果当前修改数比询问的修改数少就把没修改的进行修改,反之回退,需要注意的是,修改分为两部分:

  • 若修改的位置在当前区间:更新答案
  • 无论修改的位置是否在当前区间:都要进行修改,以供 add 和 del 函数在以后更新答案
struct Change{
    int pos;//要修改的位置
    int col;//要改成的值
}c[N];
void add(int x){...}//统计新的,根据具体情况修改
void del(int x){...}//减去旧的,根据具体情况修改
void change(int x,int ti){//改变时间轴
    if(c[ti].pos>=q[x].l&&c[ti].pos<=q[x].r){
        del(a[c[ti].pos]);//删除指定位置上的值
        add(c[ti].col);//统计新的数
    }
    swap(c[ti].col,a[c[ti].pos]);//直接互换,下次执行时必定是回退这次操作
}
/*
    change函数会执行或回退修改ti,
    执行还是回退取决于是否执行过,
    x表明当前的询问是x,
    即若修改了区间[q[x].l,q[x].r],
    便要更新答案
*/

【排序】

由于第一关键字是左端点所在的块,第二关键字是右端点所在的块,第三关键字是时间,虽然多了一个维度,但转移仍是 O(1) 的复杂度,只需要在排序时考虑 time 的影响即可。

  • 当左右端点在同一个块中:以 time 为关键字排序
  • 当左端点在同一个块中,右端点不在:以右端点为关键字排序
  • 当左端点不在同一个块:以左端点为关键字排序
bool cmp(Node a,Node b){//排序
    if( (a.l/block)==(b.l/block) ){//当左端点位于同一个块时
        if( (a.r/block)==(b.r/block) )//当右端点位于同一个块时
            return a.time<b.time;
        else
            return a.r<b.r;
    }
    else//当左端点不位于同一个块时
        return a.l<b.l;
    //return (a.l/block)^(b.l/block) ? a.l<b.l : ( ((a.r/block)^(b.r/block))?a.r<b.r:a.time<b.time );
}

【分块与时间复杂度】

设 block 为分块大小,c 为修改个数,q 为询问个数,l、r 块表示以 l/block、r/block 分的块,每个 l 块包含 n/block 个 r 块

  • 对于时间指针 time,每个 r 块最多会移动 c 次,共有 (\frac{n}{block})^2 个 r 块,总移动次数为 \frac{c*n^2}{block^2}
  • 对于左端点指针 l:l 块内最多移动 block 次,换 l 块时最多移动 2*block 次,总移动次数为:q*block
  • 对于右端点指针 r:总移动次数为:q*block+\frac{n^2}{block}
    • r 块内最多移动 block 次,换 r 块时最多移动 2*block 次,所有 l 块内移动次数之和为 q*block
    • 换 l 块时最多移动 n 次,总的换 l 块时移动次数之和为 \frac{n^2}{block}

故总移动次数为:\frac{c*n^2}{block^2}+\frac{n^2}{block}+q*block,因而总时间复杂度为 O(\frac{c*n^2}{block^2}+\frac{n^2}{block}+q*block)

由于一般题目不会分别告诉修改与询问的个数,统一用 m 表示,则有:O(\frac{m*n^2}{block^2}+\frac{n^2}{block}+m*block)

通过 Mathermatica 软件分析,要想让总时间复杂度最小,可以得到 block 的取值如下:

由于式子过于复杂,难以写出带修莫队的最佳分块数,因此一般视作 n=m,即有:O(\frac{n^3}{block^2}+\frac{n^2}{block}+n*block)

令总时间复杂度最小,可得到当 block=n^{\frac{2}{3}} 时最小,此时总时间复杂度为:O(n^\frac{5}{3})

综上,分块一般以 n^\frac{2}{3} 为一块,分成 n^\frac{1}{3} 个块,总时间复杂度为:O(n^\frac{5}{3})

int block=pow(n,0.66666);

 【模版】

struct Node{
    int l,r;//询问的左右端点
    int time;//时间维度
    int id;//询问的编号
}q[N];
struct Change{
    int pos;//要修改的位置
    int col;//要改成的值
}c[N];
int n,m,a[N];
int block;//分块
int numQ,numC;//查询、修改操作的次数
LL ans,cnt[N];
LL res[N];

bool cmp(Node a,Node b){//排序
    return (a.l/block)^(b.l/block) ? a.l<b.l : ((a.r/block)^(b.r/block)?a.r<b.r:a.time<b.time);
}

void add(int x){//统计新的,根据具体情况修改
    if(cnt[x]==0)
        ans++;
    cnt[x]++;

}
void del(int x){//减去旧的,根据具体情况修改
    cnt[x]--;
    if(cnt[x]==0)
        ans--;
}
void change(int x,int ti){//改变时间轴
    if(c[ti].pos>=q[x].l&&c[ti].pos<=q[x].r){
        del(a[c[ti].pos]);//删除指定位置上的值
        add(c[ti].col);//统计新的数
    }
    swap(c[ti].col,a[c[ti].pos]);//直接互换,下次执行时必定是回退这次操作
}
int main(){
    while(scanf("%d%d",&n,&m)!=EOF){
        ans=0;
        numQ=0;
        numC=0;
        memset(cnt,0,sizeof(cnt));
        block=pow(n,0.66666);//分块

        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);

        for(int i=1;i<=m;i++){
            char op[10];
            scanf("%s",op);
            if(op[0]=='Q'){//查询操作
                ++numQ;//查询操作次数+1
                scanf("%d%d",&q[numQ].l,&q[numQ].r);
                q[numQ].id=numQ;//序号
                q[numQ].time=numC;//时间轴
            }
            else{//修改操作
                ++numC;//修改操作次数+1
                scanf("%d%d",&c[numC].pos,&c[numC].col);
            }
        }
        sort(q+1,q+numQ+1,cmp);//对询问进行排序
        int l=1,r=0,time=0;//左右指针与时间轴
        for(int i=1;i<=numQ;i++){
            int ql=q[i].l,qr=q[i].r;//询问的左右端点
            int qtime=q[i].time;//询问的时间轴
            while(l>ql) add(a[--l]);//[l-1,r,time]
            while(l<ql) del(a[l++]);//[l+1,r,time]
            while(r<qr) add(a[++r]);//[l,r+1,time]
            while(r>qr) del(a[r--]);//[l,r-1,time]
            while(time<qtime) change(i,++time);//[l,r,time+1]
            while(time>qtime) change(i,time--);//[l,r,time-1]
            res[q[i].id]=ans;//获取答案
        }

        for(int i=1;i<=numQ;i++)
            printf("%lld\n",res[i]);
    }
    return 0;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值