0x44 分块

0x44 分块

前两节中,我们探讨了树状数组和线段树两种数据结构。树状数组基于二进制划分和倍增思想,线段树基于分治思想。它们之所以能够高效地在一个序列上执行指令并统计信息,就是因为它们把序列中的元素聚合成大大小小的“段”,花费额外的代价对这些“段”进行维护,从而使得每个区间的信息可以快速由几个已有的“段”结合而成。

当然树状数组与线段树也有其缺点。比如在维护较为复杂的信息(尤其是不满足区间可加、可减性的信息)时显得吃力,代码维护也不是那么简单、直观,需要深入理解并注意许多细节。在本节中,我们介绍分块算法。分块的基本思想是通过适当的划分,预处理一部分信息并保存下来,用空间换取时间,达到时空平衡。事实上,分块更接近于“朴素”,效率往往比不上树状数组和线段树,但是它更加通用、容易实现。我们通过几道例题详细讨论各种形式的分块算法及其应用。

给定长度为 N ( N ≤ 1 0 5 ) N(N\leq 10^5) N(N105)的数列 A A A,然后输入 Q ( Q ≤ 1 0 5 ) Q(Q\leq 10^5) Q(Q105)行操作指令。

第一类指令形如“ C   l   r   d C\ l\ r\ d C l r d”,表示把数列中第 l ∼ r l\sim r lr个数都加上 d d d

第二类指令形如“ Q   l   r Q\ l\ r Q l r”,表示询问数列中第 l ∼ r l\sim r lr个数的和。

我们已经用树状数组和线段树在 O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN)的时间内解决过该问题。现在我们用分块来再次求解。

把数列 A A A分成若干个长度不超过 ⌊ N ⌋ \lfloor \sqrt{N} \rfloor N 的段,其中第 i i i段左端点为 ( i − 1 ) ⌊ N ⌋ + 1 (i-1)\lfloor \sqrt{N} \rfloor+1 (i1)N +1,右端点为 m i n ( i ⌊ N ⌋ ) min(i\lfloor \sqrt{N} \rfloor) min(iN ⌋),如下图所示。

0x44 分块_第1张图片

另外,预处理出数组 s u m sum sum,其中 s u m [ i ] sum[i] sum[i]表示第 i i i段的区间和。设 a d d [ i ] add[i] add[i]表示第 i i i段的“增量标记”,起初 a d d [ i ] = 0 add[i]=0 add[i]=0

对于指令“ C   l   r   d C\ l\ r\ d C l r d”:

1.若 l l l r r r同时位于第 i i i段内,则直接把 A [ l ] , A [ l + 1 ] , . . . , A [ r ] A[l],A[l+1],...,A[r] A[l],A[l+1],...,A[r]都加 d d d,同时令 s u m [ i ] + = d ∗ ( r − l + 1 ) sum[i]+=d*(r-l+1) sum[i]+=d(rl+1)

2.否则,设 l l l处于第 p p p段, r r r处于第 q q q段。

(1)对于 i ∈ [ p + 1 , q − 1 ] i\in [p+1,q-1] i[p+1,q1],令 a d d [ i ] + = d add[i]+=d add[i]+=d

(2)对于开头、结尾不足一整段的两部分,按照与第1种情况相同的方法朴素地更新。

该指令的操作方法如下图所示。

0x44 分块_第2张图片

对于指令“ Q   l   r Q\ l\ r Q l r”:

1.若 l l l r r r同时处于第 i i i段内,则 ( A [ l ] + A [ l + 1 ] + . . . + A [ r ] ) + ( r − l + 1 ) ∗ a d d [ i ] (A[l]+A[l+1]+...+A[r])+(r-l+1)*add[i] (A[l]+A[l+1]+...+A[r])+(rl+1)add[i]就是答案。

2.否则,设 l l l处于第 p p p段, r r r处于第 q q q段,初始化 a n s = 0 ans=0 ans=0

(1)对于 i ∈ [ p + 1 , q − 1 ] i\in [p+1,q-1] i[p+1,q1],令 a n s + = s u m [ i ] + a d d [ i ] ∗ l e n [ i ] ans+=sum[i]+add[i]*len[i] ans+=sum[i]+add[i]len[i],其中 l e n [ i ] len[i] len[i]表示第 i i i段的长度。

(2)对于开头、结尾不足一整段的两部分,按照与第1种情况相同的方法朴素地更新。

这种分块算法对于整段的修改使用标记 a d d add add记录,对于不足整段的修改采用朴素算法。因为段数和段长都是 O ( N ) O(\sqrt{N}) O(N ),所以整个算法的时间复杂度为 O ( ( N + Q ) ∗ N ) O((N+Q)*\sqrt{N}) O((N+Q)N )。大部分常见的分块思想都可以用“大段维护、局部朴素”来形容。

long long a[100010],sum[100010],add[100010];
int L[100010],R[100010]; //每段左右端点
int pos[100010]; //每个位置属于哪一段
int n,m,t;

void change(int l,int r,long long d)
{
    int p=pos[l],q=pos[r];
    if(p==q)
    {
        for(int i=l;i<=r;++i)
            a[i]+=d;
       	sum[p]+=d*(r-l+1);
    }
    else
    {
        for(int i=p+1;i<=q-1;++i)
            add[i]+=d;
        for(int i=l;i<=R[p];++i)
            a[i]+=d;
        sum[p]+=d*(R[p]-l+1);
        for(int i=L[q];i<=r;++i)
            a[i]+=d;
        sum[q]+=d*(r-L[p]+1);
    }
}

long long ask(int l,int r)
{
    int p=pos[l],q=pos[r];
    long long ans=0;
    if(p==q)
    {
        for(int i=l;i<=r;++i)
            ans+=a[i];
        ans+=add[p]*(r-l+1);
    }
    else
    {
        for(int i=p+1;i<=q-1;++i)
            ans+=sum[i]+add[i]*(R[i]-L[i]+1);
        for(int i=l;i<=R[p];++i)
            ans+=a[i];
        ans+=add[p]*(R[p]-l+1);
        for(int i=L[q];i<=r;++i)
            ans+=a[i];
        ans+=add[q]*(r-L[q]+1);
    }
    return ans;
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;++i)
        scanf("%lld",&a[i]);
    t=sqrt(n);
    for(int i=1;i<=t;++i)
    {
        L[i]=(i-1)*sqrt(n)+1;
        R[i]=i*sqrt(n);
    }
    if(R[t]<n)
    {
        t++;
        L[t]=R[t-1]+1;
        R[t]=n;
    }
    //预处理
    for(int i=1;i<=t;++i)
    {
        for(int j=L[i];j<=R[i];++j)
        {
            pos[j]=i;
            sum[i]+=a[j];
        }
	}
    //指令
    while(m--)
    {
        char op[3];
        int l,r,d;
        scanf("%s%d%d",op,&l,&r);
        if(op[0]=='C')
        {
            scanf("%d",&d);
            change(l,r,d);
		}
        else
            printf("%lld\n",ask(l,r));
    }
    return 0;
}
方法 复杂度 时间 内存 代码 优劣
树状数组 O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN) 1.0s 3MB 850B 效率高、代码短、不易扩展、不太直观
线段树 O ( ( N + Q ) l o g N ) O((N+Q)logN) O((N+Q)logN) 1.5s 7MB 1700B 效率较高、扩展性好、代码较长、直观性一般
分块 O ( ( N + Q ) N ) O((N+Q)\sqrt{N}) O((N+Q)N ) 1.9s 1.5MB 1500B 通用、直观、效率偏低、码长一般
朴素 O ( ( N + Q ) ∗ N ) O((N+Q)*N) O((N+Q)N) TLE 1MB 500B

分块时一定要注意边界值,如若分成 T T T块,每块长度为 l e n len len,若 T ∗ l e n < N T*lenTlen<N,不妨多加一块 T ∗ l e n + 1 ∼ N T*len+1\sim N Tlen+1N。也可以用 L L L R R R数组来记录每块的左右边界,用 p o s pos pos数组记录每个数在哪个块,这样方便于清晰地处理问题。(具体代码如上)

1.询问区间众数

在乡下的小路旁种着许多蒲公英,而我们的问题正是与这些蒲公英有关。

为了简化起见,我们把所有的蒲公英看成一个长度为 n n n 的序列 { a 1 , a 2 . . a n } \{a_1,a_2..a_n\} {a1,a2..an},其中 a i a_i ai 为一个正整数,表示第 i i i 棵蒲公英的种类编号。

而每次询问一个区间 [ l , r ] [l, r] [l,r],你需要回答区间里出现次数最多的是哪种蒲公英,如果有若干种蒲公英出现次数相同,则输出种类编号最小的那个。

注意,你的算法必须是在线的。第一行有两个整数,分别表示蒲公英的数量 n n n 和询问次数 m m m

第二行有 n n n 个整数,第 i i i 个整数表示第 i i i 棵蒲公英的种类 a i a_i ai

接下来 m m m 行,每行两个整数 l 0 , r 0 l_0, r_0 l0,r0,表示一次询问。输入是加密的,解密方法如下:

令上次询问的结果为 x x x(如果这是第一次询问,则 x = 0 x = 0 x=0),设 l = ( ( l 0 + x − 1 )   m o d   n ) + 1 , r = ( ( r 0 + x − 1 )   m o d   n ) + 1 l=((l_0+x-1)\bmod n) + 1,r=((r_0+x-1) \bmod n) + 1 l=((l0+x1)modn)+1,r=((r0+x1)modn)+1。如果 l > r l > r l>r,则交换 l , r l, r l,r
最终的询问区间为计算后的 [ l , r ] [l, r] [l,r]

对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 40000 1\le n \le 40000 1n40000 1 ≤ m ≤ 50000 1\le m \le 50000 1m50000 1 ≤ a i ≤ 1 0 9 1\le a_i \le 10^9 1ai109 1 ≤ l 0 , r 0 ≤ n 1 \leq l_0, r_0 \leq n 1l0,r0n

本题是经典的在线求众数问题。因为众数不具有“区间可加性”(已知序列 a a a中区间 [ x , y ] [x,y] [x,y]的众数和区间 [ y + 1 , z ] [y+1,z] [y+1,z]的众数,不能直接得到区间 [ x , z ] [x,z] [x,z]的众数),所以有树状数组或线段树维护就十分困难。下面我们介绍两种常见的分块做法。

若把序列 a a a分成 T T T块,则每块的长度 L = N / T L=N/T L=N/T,我们稍后会讨论 T T T的取值。

对于每个询问 [ l , r ] [l,r] [l,r],设 l l l处于第 p p p块, r r r处于第 q q q块。我们把询问区间 [ l , r ] [l,r] [l,r]分成三部分:

1.开头不足一整段的 [ l , L ) [l,L) [l,L)

2.第 p + 1 ∼ q − 1 p+1\sim q-1 p+1q1块构成的区间 [ L , R ] [L,R] [L,R]

3.结尾不足一整段的 ( R , r ] (R,r] (R,r]

显然 a a a序列在区间 [ l , r ] [l,r] [l,r]中的众数只可能来自一下两种情况:
1.区间 [ L , R ] [L,R] [L,R]的众数。

2.出现在 [ l , L ) [l,L) [l,L) ( R , r ] (R,r] (R,r]之间的数。

方法一

预处理出所有以“段边界”为端点的区间 [ L , R ] [L,R] [L,R]中每个数出现的次数,以及区间众数。这样的区间总共有 O ( T 2 ) O(T^2) O(T2)个,并且保存每个数出现的次数需要长度为 O ( N ) O(N) O(N)的数组(记为 c n t L , R cnt_{L,R} cntL,R)。

对于每个询问中的 [ l , L ) [l,L) [l,L) ( R , r ] (R,r] (R,r],可以通过朴素扫描,在数组 c n t L , R cnt_{L,R} cntL,R的基础上累加次数,从而更新答案。回答询问后在进行一次朴素扫描,从而 c n t L , R cnt_{L,R} cntL,R中减少次数,把数组复原。

这个算法的时间为 O ( N T 2 + M N / T ) O(NT^2+MN/T ) O(NT2+MN/T),空间为 O ( N T 2 ) O(NT^2 ) O(NT2)。通过方程可解得 T = M / 2 3 T=\sqrt[3]{M/2} T=3M/2 。但我们一般让 T T T N N N有关,而不与 M M M有关(因为 T T T是让 N N N个数据分成 T T T块,如果令 T = M / 2 3 T=\sqrt[3]{M/2} T=3M/2 ,有可能出现 M M M过大,造成 T T T很大​,给的 N N N过小,不足以分成 T T T块)。不妨设 N , M N,M N,M为相同数量级,令 T = N 3 T=\sqrt[3]{N} T=3N ,此时算法复杂度在 O ( N 5 3 ) O(N^{ \frac{5}{3} }) O(N35)级别。

#include 
using namespace std;

int N, M, Q, T, len, l, r, ans; // T最大为30
int a[40005], b[40005], disc[40005];
int cnt[35][35][40005];
int f[35][35];
int L[35], R[35];
int pos[40005];
int c[40005];

int main()
{
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; ++i)
    {
        scanf("%d", &a[i]);
        b[i] = a[i];
    }
    T = max(1.0, pow(1.0 * N / 2, 1.0 / 3));
    len = N / T;
    for (int i = 1; i <= T; ++i)
    {
        L[i] = (i - 1) * len + 1;
        R[i] = i * len;
    }
    if (R[T] < N)
    {
        T++;
        L[T] = R[T - 1] + 1;
        R[T] = N;
    }
    for (int i = 1; i <= T; ++i)
        for (int j = L[i]; j <= R[i]; ++j)
            pos[j] = i;

    sort(b + 1, b + N + 1);
    for (int i = 1; i <= N; ++i)
        if (i == 1 || b[i] != b[i - 1])
            disc[++Q] = b[i];
    for (int i = 1; i <= N; ++i)
    {
        int id = lower_bound(disc + 1, disc + Q + 1, a[i]) - disc;
        b[i] = id;
    }
    for (int i = 1; i <= T; ++i)
    {
        for (int j = i; j <= T; ++j)
        {
            for (int k = L[i]; k <= R[j]; ++k)
            {
                cnt[i][j][b[k]]++;
                if (cnt[i][j][f[i][j]] < cnt[i][j][b[k]] || (cnt[i][j][f[i][j]] == cnt[i][j][b[k]] && f[i][j] > b[k]))
                    f[i][j] = b[k];
            }
        }
    }
    while (M--)
    {
        scanf("%d%d", &l, &r);
        l = ((l + ans - 1) % N) + 1;
        r = ((r + ans - 1) % N) + 1;
        if (l > r)
            swap(l, r);
        int p = pos[l], q = pos[r];
        ans = 0;
        if (p == q)
        {
            memset(c, 0, sizeof(c));
            for (int i = l; i <= r; ++i)
            {
                c[b[i]]++;
                if (c[ans] < c[b[i]] || (c[ans] == c[b[i]] && ans > b[i]))
                    ans = b[i];
            }
        }
        else
        {
            ans = f[p + 1][q - 1];
            for (int i = l; i <= R[p]; ++i)
            {
                cnt[p + 1][q - 1][b[i]]++;
                if (cnt[p + 1][q - 1][ans] < cnt[p + 1][q - 1][b[i]] || (cnt[p + 1][q - 1][ans] == cnt[p + 1][q - 1][b[i]] && ans > b[i]))
                    ans = b[i];
            }
            for (int i = L[q]; i <= r; ++i)
            {
                cnt[p + 1][q - 1][b[i]]++;
                if (cnt[p + 1][q - 1][ans] < cnt[p + 1][q - 1][b[i]] || (cnt[p + 1][q - 1][ans] == cnt[p + 1][q - 1][b[i]] && ans > b[i]))
                    ans = b[i];
            }
            for (int i = l; i <= R[p]; ++i)
                cnt[p + 1][q - 1][b[i]]--;
            for (int i = L[q]; i <= r; ++i)
                cnt[p + 1][q - 1][b[i]]--;
        }
        ans = disc[ans];
        printf("%d\n", ans);
    }

    return 0;
}

方法二

在预处理时,只保存所有以“段边界”为端点的区间 [ L , R ] [L,R] [L,R]的众数。另外,对每一个数值建立 S T L   v e c t o r STL\ vector STL vector,按顺序保存该数值在序列 a a a每次出现的位置。

对于每个询问,扫描 [ l , L ) [l,L) [l,L) ( R , r ] (R,r] (R,r]中的每个数 x x x,在对应的 v e c t o r vector vector中二分查找即可得到 x x x [ l , r ] [l,r] [l,r]中出现的次数,从而更新答案。

这个算法的时间为 O ( N T + M N / T ∗ l o g N ) O(NT+MN/T*logN) O(NT+MN/TlogN),空间为 O ( T 2 ) O(T^2) O(T2)。应取 T = M l o g N T=\sqrt{MlogN} T=MlogN ,取 T = N l o g N T=\sqrt{NlogN} T=NlogN ,此时整个算法的复杂度在 O ( N N l o g N ) O(N\sqrt{NlogN} ) O(NNlogN )级别。

#include 
using namespace std;

int N, M, Q, T, len, l, r, ans, cnt; // T最大为875
int a[40005], b[40005], disc[40005];
int f[900][900];
vector<int> v[40005];
int L[900], R[900];
int pos[40005];
int c[40005];

int find_up(int L, int num)
{
    int l = 0, r = v[num].size() - 1;
    int ans = -1;
    while (l <= r)
    {
        int mid = (l + r) / 2;
        if (v[num][mid] >= L)
        {
            ans = mid;
            r = mid - 1;
        }
        else
            l = mid + 1;
    }
    return ans;
}

int find_down(int R, int num)
{
    int l = 0, r = v[num].size() - 1;
    int ans = -2;
    while (l <= r)
    {
        int mid = (l + r) / 2;
        if (v[num][mid] <= R)
        {
            ans = mid;
            l = mid + 1;
        }
        else
            r = mid - 1;
    }
    return ans;
}

int main()
{
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; ++i)
    {
        scanf("%d", &a[i]);
        b[i] = a[i];
    }
    sort(b + 1, b + N + 1);
    for (int i = 1; i <= N; ++i)
        if (i == 1 || b[i] != b[i - 1])
            disc[++Q] = b[i];
    for (int i = 1; i <= N; ++i)
    {
        int id = lower_bound(disc + 1, disc + Q + 1, a[i]) - disc;
        b[i] = id;
    }

    T = max(1.0, sqrt(1.0 * M * log(N) / log(2)));
    len = N / T;
    for (int i = 1; i <= T; ++i)
    {
        L[i] = (i - 1) * len + 1;
        R[i] = i * len;
    }
    if (R[T] < N)
    {
        T++;
        L[T] = R[T - 1] + 1;
        R[T] = N;
    }
    for (int i = 1; i <= T; ++i)
        for (int j = L[i]; j <= R[i]; ++j)
            pos[j] = i;

    for (int i = 1; i <= N; ++i)
        v[b[i]].push_back(i);
    for (int i = 1; i <= T; ++i)
    {
        memset(c, 0, sizeof(c));
        ans = 0, cnt = 0;
        for (int j = L[i]; j <= N; ++j)
        {
            c[b[j]]++;
            if (cnt < c[b[j]] || (cnt == c[b[j]] && ans > b[j]))
            {
                ans = b[j];
                cnt = c[b[j]];
            }
            f[i][pos[j]] = ans;
        }
    }
    ans = 0;
    while (M--)
    {
        scanf("%d%d", &l, &r);
        l = ((l + ans - 1) % N) + 1;
        r = ((r + ans - 1) % N) + 1;
        if (l > r)
            swap(l, r);
        int p = pos[l];
        int q = pos[r];
        ans = 0;
        cnt = 0;
        if (p == q)
        {
            for (int i = l; i <= r; ++i)
            {
                int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
                if (cnt < tmp || (cnt == tmp && ans > b[i]))
                {
                    ans = b[i];
                    cnt = tmp;
                }
            }
        }
        else
        {
            if (p + 1 <= q - 1)
            {
                ans = f[p + 1][q - 1];
                cnt = find_down(r, ans) - find_up(l, ans) + 1;
            }
            for (int i = l; i <= R[p]; ++i)
            {
                int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
                if (cnt < tmp || (cnt == tmp && ans > b[i]))
                {
                    ans = b[i];
                    cnt = tmp;
                }
            }
            for (int i = L[q]; i <= r; ++i)
            {
                int tmp = find_down(r, b[i]) - find_up(l, b[i]) + 1;
                if (cnt < tmp || (cnt == tmp && ans > b[i]))
                {
                    ans = b[i];
                    cnt = tmp;
                }
            }
        }
        ans = disc[ans];
        printf("%d\n", ans);
    }
    return 0;
}

2.莫队算法

H 有一串由各种漂亮的贝壳组成的项链。H 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。H 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

n n n 个正整数 a i a_i ai,表示项链中第 i i i 个贝壳的种类。m个询问。

1 ≤ n , m , a i ≤ 1 0 6 1\le n,m,a_i \leq 10^6 1n,m,ai106 1 ≤ l ≤ r ≤ n 1\le l \le r \le n 1lrn

在本题中,我们将介绍分块算法的一种重要形式——对“询问”进行分块。这是一种离线做法,又称为莫队算法

题目到手,我们开始分析本题的算法。这题最简单做法无非暴力——用一个cnt数组记录每个数值出现的次数,再暴力枚举lr统计次数,最后再扫一遍cnt数组,统计cnt不为零的数值个数,输出答案即可。设最大数值为s,那么这样做的复杂度为 O ( m ( n + s ) ) O(m(n+s)) O(m(n+s))

然后优化一下,每次枚举到一个数值num,增加出现次数时判断一下 c n t n u m cnt_{num} cntnum是否为0,如果为0,则这个数值之前没有出现过,现在出现了,数值数当然要+1。反之在从区间中删除num后也判断一下 c n t n u m cnt_{num} cntnum是否为0,如果为0数值总数-1。这样我们优化掉了一个 O ( m s ) O(ms) O(ms)

然后再优化,我们弄两个指针 lr ,每次询问不直接枚举,而是移动lr 指针到询问的区间,直到 [ l , r ] [l,r] [l,r]与询问区间重合。在统计答案时,我们也只在两个指针处加减cnt

那么增添删除函数就可以写出来了:

inline void add(int p) // 添数,p为下标
{
    if (cnt[A[p]] == 0)
        cur++;
    cnt[A[p]]++;
}
inline void del(int p) // 删数
{
    cnt[A[p]]--;
    if (cnt[A[p]] == 0)
        cur--;
}

那么从一个区间到另一个区间,只需写:

while (l > Q[i].l)
    add(--l);
while (l < Q[i].l)
    del(l++);
while (r < Q[i].r)
    add(++r);
while (r > Q[i].r)
    del(r--);

注意++和–的位置。删数是先删后移,添数是先移后添。初始化时,要先令l=1,r=0。

现在我们可以从一个区间的答案转移到另一个区间了,但是,如果直接在线查询,很有可能在序列两头“左右横跳”,到头来还不如朴素算法。但是,我们可以把查询离线下来(记录下来),然后,排个序。

那么我们该如何排序呢?我们很容易想到以l为第一关键词,r为第二关键词排下序,这样 l l l 1 ∼ N 1\sim N 1N范围内不断递增,l得到控制,但r 1 ∼ N 1\sim N 1N范围不断来回横跳,例如前一个范围是 [ 1 , 2 ] [1,2] [1,2],下一个范围是 [ 2 , N ] [2,N] [2,N],然后下一个范围是 [ 3 , 5 ] [3,5] [3,5],这样遍历一个询问的最坏时间依旧是 O ( N ) O(N) O(N),效果并不好。

我们可以使用分块的思想,把这些询问按照左端点递增排序,然后分成 N \sqrt{N} N 块,每块内部再按右端点递增排序。这样左端点的变化范围是 N \sqrt{N} N ,而右端点是递增的,左右端点都得到了控制。如果我们以上一次询问的回答为基础,那么每次只需要花费 O ( N ) O(\sqrt{N}) O(N )的时间处理左端多出或减少的部分。而整块中右端点增长的范围之和为 O ( N ) O(N) O(N),所以在 O ( N N ) O(N\sqrt{N}) O(NN )的时间内即可求解本题。

实际排序的过程中,我们可以先完成分块,然后排序,左端点属于较小块的排在前面,左端点属于同一块的,右端点小的排在前面。

但在此之上,我们还可以进行常数优化:奇偶化排序。意为:如果pos[l] 是奇数,则将r顺序排序,否则将r逆序排序。这样可以在每个块间转移时,降低 r r r的移动次数。

#include 
using namespace std;

struct query{
    int l,r;
    int id;
}q[1000005];
int N,M,cur,T,len;
int a[1000005];
int cnt[1000005];
int ans[1000005];
int pos[1000005];

inline int read()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9')
    {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

inline void write(int x)
{
    if(x<0)
        putchar('-'),x=-x;
    if(x>9)
        write(x/10);
    putchar(x%10+'0');
}

void add(int p)
{
    if(cnt[a[p]]==0)
        ++cur;
    ++cnt[a[p]];
}

void del(int p)
{
    --cnt[a[p]];
    if(cnt[a[p]]==0)
        --cur;
}


int main()
{
    N=read();
    for(int i=1;i<=N;++i)
        a[i]=read();
    M=read();
    for(int i=1;i<=M;++i)
        q[i].id=i,q[i].l=read(),q[i].r=read();
    T=sqrt(M);
    len=max(1,N/T);
    for(int i=1;i<=T;++i)
        for(int j=(i-1)*len+1;j<=i*len;++j)
            pos[j]=i;
    if(T*len<N)
    {
        T++;
        for(int i=(T-1)*len+1;i<=N;++i)
            pos[i]=T;
    }
    sort(q+1,q+M+1,[](query a,query b){
        //return pos[a.l]==pos[b.l]?a.r
        return pos[a.l]==pos[b.l]?((pos[a.l]&1)?a.r<b.r:a.r>b.r):pos[a.l]<pos[b.l]; //奇偶化处理
    });
    int l=1,r=0;
    for(int i=1;i<=M;++i)
    {
        while(l<q[i].l) del(l++);
        while(l>q[i].l) add(--l);
        while(r<q[i].r) add(++r);
        while(r>q[i].r) del(r--);
        ans[q[i].id]=cur;
    }
    for(int i=1;i<=M;++i)
    {
        write(ans[i]);
        printf("\n");
    }
    return 0;
}

我们来分析一下时间复杂度。记块长为 N N N,询问数为 M M M,设块长为 S S S,则有 N S \frac{N}{S} SN个块。每个左端点每次最多移动 S S S的长度,一个块中右端点最多能移动 N N N的长度,时间复杂度为:
M ∗ S + N ∗ N S = M S + N 2 S M*S+N*\frac{N}{S}=MS+\frac{N^2}{S} MS+NSN=MS+SN2
易知当 S = N M S=\frac{N}{\sqrt{M}} S=M N时,即取 M \sqrt{M} M 块时,取最小值为 2 N M 2N\sqrt{M} 2NM ,所以实际时间复杂度为 O ( N M ) O(N\sqrt{M}) O(NM ),当 N N N M M M同数量级时,可以看做 O ( N N ) O(N\sqrt{N}) O(NN )。然而实际上我们并不会取 M \sqrt{M} M 块,而是取 N \sqrt{N} N 块,因为若 M M M远大于 N N N M \sqrt{M} M 大于 N N N则无法分出 M \sqrt{M} M 块。

你可能感兴趣的:(#,0x40,数据结构进阶,算法,c++)