「莫队」学习笔记

莫队用来离线解决区间询问问题。

一、不带修莫队

考虑利用分块的方法对所有询问的区间按照一定的顺序来回答,而不是完全按照输入顺序在线回答,从而使历史信息得到充分利用。这就是莫队相较于暴力要优的地方。

对$[1,n]$进行分块,每块大小为$B$。按照以下规则来给询问排序:对于两个区间,第一关键字是左端点所在的块。第二关键字是右端点所在的块。第三关键字是右端点的编号。其实在这里这第二关键字是没用的。然而之所以弄得复杂了点是为了更自然地衔接到下文要介绍的“带修莫队”中去。

当上一个回答的区间是$[l_1,r_1]$,此时要回答的区间是$[l_2,r_2]$时,暴力地移动左右端点,在移动的过程中进行更新或是统计。

下面来证明时间复杂度:

左端点依次分布在块中,但块内的左端点不单调。对于块内,每一次最多移动$B$个单位。而移动一次$O(1)$。总共移动$(m)$次。故复杂度为$O(mB)$

右端点相对于每一个块是单调的。因此处理一个块右端点最多移动$n$个单位。总共$\dfrac{n}{B}$个块。因此复杂度为$O(\dfrac{n^2}{B})$

因此复杂度为$O(mB+\dfrac{n^2}{B})$


复杂度相关

利用对勾函数(均值不等式)可以知道这个函数的最小值为$O(n\sqrt{m})$,$size$应当取$\sqrt{\dfrac{n^2}{m}}$。当$n=m$时,$B$取$\sqrt{n}$


 模板(洛谷P2709 小B的询问

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
inline int read(){
    int x(0),w(1); char c = getchar();
    while(c^'-' && (c<'0'||c>'9')) c = getchar();
    if(c == '-') w = -1, c = getchar();
    while(c>='0' && c<='9') x = (x<<3) + (x<<1) + c - '0', c = getchar();
    return x*w;
}
struct Query{
    int l,r,idx;
    ll ans;
}q[50010];
int n,m,K,L,R,sz;
ll ans;
int a[50010],cnt[50010];
inline bool cmp(const Query& A, const Query& B){
    if(A.l/sz != B.l/sz) return A.l/sz < B.l/sz;
    return A.r < B.r; 
}
inline bool cmp2(const Query& A, const Query& B){
    return A.idx < B.idx;
}
inline void inc(int P){
    ans += (ll)2*(ll)cnt[a[P]]+(ll)1;
    ++cnt[a[P]];
}
inline void dec(int P){
    ans -= (ll)2*(ll)cnt[a[P]]-(ll)1;
    --cnt[a[P]];
}
int main(){
    n = read(), m = read(), K = read();
    sz = 223;
    for(int i = 1; i <= n; ++i) a[i] = read();
    for(int i = 1; i <= m; ++i){
        q[i].idx = i;
        q[i].l = read(), q[i].r = read();
    }
    sort(q+1,q+m+1,cmp);
    L = R = 1;
    inc(1);
    for(int i = 1; i <= m; ++i){
        while(R < q[i].r) inc(++R);
        while(R > q[i].r) dec(R--);
        while(L < q[i].l) dec(L++);
        while(L > q[i].l) inc(--L);
        q[i].ans = ans;
    }
    sort(q+1,q+m+1,cmp2);
    for(int i = 1; i <= m; ++i){
        printf("%lld\n",q[i].ans);
    }
    return 0;
}

 二、带修莫队

我们对于每一个区间需要三维:左端点,右端点,时间。

按照以下规则来给询问排序:对于两个区间,第一关键字是左端点所在的块。第二关键字是右端点所在的块。第三关键字是时间。我们发现前两个关键字和不带修莫队的排序方法是一样的。

关键在于复杂度和块的最优大小证明。

我们考虑:左端点的移动次数是不变的,是$O(mB)$;右端点现在多出了块内的移动,一样是$O(mB)$,块间移动近似$O(\dfrac{n^2}{B})$,因此复杂度的和近似为$O(mB+\dfrac{n^2}{B})$。时间指针的复杂度是$O(c(\dfrac{n}{B})^2)$。由此,总复杂度为$O(c(\dfrac{n}{B})^2+mB+\dfrac{n^2}{B})$。

这个东西分析起来太麻烦啦!设$c=n=m$,复杂度为$y=O(\dfrac{n^3}{B^2}+nB+\dfrac{n^2}{B})$。我们要求$y$的最小值

$\dfrac{y}{n}=\dfrac{n^2}{B^2}+B+\dfrac{n}{B}$

令$t=\dfrac{n}{B}$,则可设$\dfrac{y}{n}=t^2+\dfrac{n}{t}+t$

由于当$t\rightarrow \infty$时,$t^2$为$t$的高阶,因此$\dfrac{y}{n} \approx t^2+\dfrac{n}{t}$

$t^2+\dfrac{n}{t}=t^2+\dfrac{n}{2t}+\dfrac{n}{2t} \geq 3\sqrt[3]{t^2*\dfrac{n}{2t}*\dfrac{n}{2t}}=3\sqrt[3]{\dfrac{n^2}{4}}$

因此$\dfrac{y}{n} \geq O(n^{\frac{2}{3}})$

因此$y_{min}=O(n^{\frac{5}{3}})$

而只有当$t^2=\dfrac{n}{2t}$时可以满足等号,因此$B$应当取$n^{\frac{2}{3}}$


模板(洛谷P1093 [国家集训队]数颜色 / 维护队列 )

在修改时有一个很妙的操作,那就是每一次修改交换修改的值和原值,因为下一次修改这个时一定是倒着修改了。

/*DennyQi 2019*/
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
inline int read(){
    int x(0),w(1); char c = getchar();
    while(c^'-' && (c<'0'||c>'9')) c = getchar();
    if(c == '-') w = -1, c = getchar();
    while(c>='0' && c<='9') x = (x<<3) + (x<<1) + c - '0', c = getchar();
    return x*w;
}
struct Query{ int l,r,t,idx,ans; }q[50010];
int n,m,T,x,y,Q,ans,L,R,cur_T,sz;
int a[50010],b[50010],md[50010],mdc[50010],cnt[1000010];
char s[10];
inline bool cmp(const Query& A, const Query& B){
    if(A.l/sz != B.l/sz) return A.l < B.l;
    if(A.r/sz != B.r/sz) return A.r < B.r;
    return A.t < B.t;
}
inline bool cmp2(const Query& A, const Query& B){ return A.idx < B.idx; }
inline void inc(int P){ if(cnt[a[P]]++ == 0) ++ans; }
inline void dec(int P){ if(--cnt[a[P]] == 0) --ans; }
inline void modify(int i, int _L, int _R){
    int P = md[i];
    if(_L <= P && P <= _R){
        if(--cnt[a[P]] == 0) --ans;
        swap(a[P],mdc[i]);
        if(cnt[a[P]]++ == 0) ++ans;
    } 
    else swap(a[P],mdc[i]);
}
int main(){
    scanf("%d%d",&n,&m);
    sz = 1357;
    for(int i = 1; i <= n; ++i) scanf("%d",a+i);
    for(int i = 1; i <= m; ++i){
        scanf("%s",s);
        if(s[0] == 'Q'){
            ++Q;
            scanf("%d%d",&q[Q].l,&q[Q].r);
            q[Q].idx = Q, q[Q].t = T;
        }
        else{
            ++T;
            scanf("%d%d",&md[T],&mdc[T]);
        }
    }
    sort(q+1,q+Q+1,cmp);
    inc(L=R=1);
    for(int i = 1; i <= Q; ++i){
        while(L < q[i].l) dec(L++);
        while(L > q[i].l) inc(--L);
        while(R < q[i].r) inc(++R);
        while(R > q[i].r) dec(R--);
        while(cur_T < q[i].t) modify(++cur_T,q[i].l,q[i].r);
        while(cur_T > q[i].t) modify(cur_T--,q[i].l,q[i].r);
        q[i].ans = ans;
    }
    sort(q+1,q+Q+1,cmp2);
    for(int i = 1; i <= Q; ++i){
        printf("%d\n",q[i].ans);
    }
    return 0;
}

 三、树上莫队

太难了,咕咕咕

 

posted @ 2019-05-22 10:55  DennyQi  阅读(209)  评论(0编辑  收藏  举报