查询区间内等于x的数的个数(分块)

本文介绍了一种名为分块的算法技巧,通过将数组分成多个块并预先处理,以提高区间查询效率。文章详细解释了如何实现分块,并通过具体代码示例展示了如何使用分块解决特定问题。

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

问题:有一个有n个数的数组,q次查询,每次查询l, r ,x,查询区间[l,r]内等于x的数的个数

思路:分块。


就把这题当成是分块的入门题来讲解一下分块。分块其实就是一种比较优美的暴力(我觉得),一般的分块都是把长度为n的数组分成每一块为sqrt(n)个数的多个块。然后对于区间的操作就可以不再是一个一个数进行处理,而是可以一个块一个块的处理,每次查询最多会涉及到sqrt(n)个块,这样复杂度就降了下来。

(先进行几点说明,本文中的数组下标从1开始,块的编号也是从1开始,每个块的大小k = sqrt(n))

分块过后的对于区间[l,r]的操作就分为两种情况:

设cl为区间左端点l所在的块号,cr为区间右端点r所在的块号。那么怎么求一个下标所属的块号呢,很简单:(x-1)/k + 1。

第一种情况是cl == cr,也就是左右端点在同一个块内,这样直接从l循环到r处理操作就可以了,复杂度很显然最多是sqrt(n)

第二种情况我们可以统一的假定为 cl != cr,这样我们可以把区间分为三部分,一是l到cl*k这部分可以直接循环操作,后面就是第cl+1块到cr-1块这部分,最后就是(cr-1)*k+1到r这部分。


这一题要求的是一个区间内等于x的有多少个数,我们可以对每一个块进行排序,然后就可以用二分求出等于x的个数。


#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

const int maxn = 300010;

int a[maxn];
int b[maxn];

int n,k;
int li(int x) //求x的块号
{
    return (x-1)/k + 1;
}

int query(int l,int r,int x)
{
    int i;
    int cl = li(l);
    int cr = li(r);
    int cnt = 0;
    if(cl == cr)
    {
        for(i=l;i<=r;i++)
            if(a[i] == x)
                cnt++;
        return cnt;
    }
    for(i=l;i<=cl*k;i++)
    {
        if(a[i] == x)
            cnt++;
    }
    for(i=cl+1;i<cr;i++)
    {
        cnt += upper_bound(b+(i-1)*k+1,b+i*k+1,x) - lower_bound(b+(i-1)*k+1,b+i*k+1,x);
    }
    for(i=(cr-1)*k+1;i<=r;i++)
    {
        if(a[i] == x)
            cnt++;
    }

    return cnt;
}
int main(void)
{
    int i,j;
    scanf("%d",&n);
    for(i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[i] = a[i];
    }
    k = sqrt(n);
    for(i=1;i<=k+1;i++)
    {
        if((i-1)*k+1 > n)
            break;
        sort(b+(i-1)*k+1,b+min(i*k,n)+1);
    }
    int q;
    scanf("%d",&q);
    while(q--)
    {
        int l,r,x;
        scanf("%d%d%d",&l,&r,&x);
        int ans = query(l,r,x);
        printf("%d\n",ans);
    }

    return 0;
}

<think>在莫队算法中,我们维护的区间是前缀和组的区间,即从 `lp` 到 `rp` 的位置(注意这些位置对应的是原组的前缀和下标,即0~n)。我们要求的是满足 `pre[i] ^ pre[j] = k` 且 `i < j` 的配对量(其中 `i` 和 `j` 都在当前区间内)。 但是,在移动指针时,我们考虑的是当前区间内所有满足条件的配对。在移动左指针(`lp`)时,我们移除位置 `lp`(注意这个位置是前缀和组中的一个位置,对应原组的前缀和下标为 `lp`)。当我们移除 `lp` 时,我们需要减去所有以 `lp` 为左端(即 `pre[i]` 中的 `i` 为 `lp`)的配对。也就是说,我们需要减去满足 `pre[lp] ^ pre[j] = k` 的 `j` 的量(其中 `j` 在区间内且 `j>lp`)。这个数量就是 `cnt[pre[lp]^k]`。注意,在移除之前,`cnt[pre[lp]]` 还没有被移除,所以 `cnt[pre[lp]^k]` 表示的就是 `pre[lp]` 配对的 `pre[j]` 的量。 同理,当加入一个位置时,我们也是先计算加入后能新增多少配对(即当前区间内有多少个 `pre[j]` 满足 `pre[j] = pre[new] ^ k`),然后再将这个位置的值加入计。 因此,在修正代码中,我们调整了指针移动的顺序: 1. 当左指针收缩(`lp` 右移)时: - 先移除 `lp` 位置:将 `cnt[pre[lp]]` 减1(注意,此时 `lp` 还在区间内,所以先减1) - 然后减去配对:`s -= cnt[pre[lp]^k]` (注意,此时 `pre[lp]` 已经被移除,所以这里 `cnt[pre[lp]^k]` 表示的是其他位置 `pre[lp]` 配对的个数,但注意,在移除 `pre[lp]` 后,这些配对就不存在了,所以减去) - 但实际上,这个顺序应该是:先减去配对,再移除位置?因为如果先移除位置,那么 `cnt[pre[lp]^k]` 就不包括 `pre[lp]` 自己了?但是,我们移除的是位置 `lp`,而配对要求的是 `j>lp`,所以 `pre[lp]` 自己不会和自己配对(因为同一个位置不能作为两个端点?)。然而,这里有一个细节:配对要求两个不同的位置,所以不会出现自己和自己配对(除非k=0,但自己和自己配对也是不允许的,因为区间需要两个不同的端点)。 然而,更标准的做法是:在移除一个位置时,先将其从计中移除,然后更新配对量。但这样更新配对量时,需要减去的是移除前该位置能配对的个数(即移除前 `cnt[pre[lp]^k]` 的值)。但因为我们刚刚已经移除了自己,所以此时 `cnt[pre[lp]^k]` 已经少了这个位置(如果之前有的话)?不对,因为配对是和其他位置配对,移除自己后,其他位置还在,所以 `cnt[pre[lp]^k]` 还是其他位置中等于 `pre[lp]^k` 的量。但是,这个数量恰好就是我们需要减去的:因为移除 `lp` 后,所有以 `lp` 为左端(或右端,具体看定义)的配对都不存在了。 然而,在莫队算法中,我们通常这样处理: 设当前区间为 [L, R],我们维护的是区间内所有位置的前缀和值的计组 `cnt`,以及配对总 `s`。 当我们要将左指针L向右移动(即收缩左边界)时,我们移除位置L(注意这个位置是前缀和组中的位置,对应原组的前缀和下标L): - 首先,我们减少配对:这个位置L作为配对中的左端(即i=L)时,配对量为 `cnt[pre[L]^k]`(因为配对要求 `pre[i]^pre[j]=k`,即 `pre[j]=pre[i]^k`,所以j的量就是 `cnt[pre[L]^k]`)。 - 然后,我们将 `cnt[pre[L]]` 减1,因为位置L被移除。 但是,在修正代码中,我们却先移除再减配对?这样就会导致减去的配对量不正确(因为移除后,`cnt[pre[L]^k]` 已经少了一个可能的位置L(如果 `pre[L]^k` 等于 `pre[L]` 的话,即k=0,那么移除自己后,自己就不能和自己配对了)?所以实际上,我们应该先计算配对量,再移除。 因此,修正代码中左指针收缩的部分应该改为: ```cpp while (lp < l) { // 先减去以lp为左端(即i=lp)的配对量 s -= cnt[pre[lp] ^ k]; cnt[pre[lp]]--; lp++; } ``` 同理,左指针扩展(左移)时,应该先加入位置,再更新配对(因为新加入的位置可以和区间内已有的位置配对,包括自己?但是不允许自己和自己配对?注意,新加入的位置是 `lp-1`,而区间内已有的位置包括 `lp` 到 `rp`,所以新位置可以和这些位置配对,但是不能和自己(因为自己只有一个)?所以配对量是 `cnt[pre[lp-1]^k]`,但注意,这个数量不包括自己,因为自己还没有加入。所以加入前计算配对量是合理的。 但是,在加入后,自己也会被计入计,然后后续的位置就可以和自己配对。所以加入时,我们应该是先加入位置,再更新配对?不对,因为新加入的位置需要和区间内已有的位置配对,而自己还没有加入,所以已有的位置不包括自己。因此,加入时: - 先将新位置(`lp-1`)加入计:`cnt[pre[lp-1]]++` - 然后,新加入的位置作为左端(或右端)可以和区间内已有的位置(包括自己?)配对?注意,区间内已有的位置包括之前加入的所有位置,但不包括自己(因为自己刚刚加入,还没有被配对计算过)。实际上,配对计算时,我们考虑的是两个不同的位置。新加入的位置 `lp-1` 可以和区间内任何一个位置(包括自己?)配对?但是,同一个位置不能配对两次。所以,新加入的位置 `lp-1` 的加入,会带来多少新增配对?就是区间内(包括自己?)等于 `pre[lp-1]^k` 的量?但是,自己还没有加入时,区间等于 `pre[lp-1]^k` 的量就是 `cnt[pre[lp-1]^k]`(不包括自己),然后我们加入自己,然后这个数量就增加了?不对,应该是加入后,这个位置就可以和区间内其他等于 `pre[lp-1]^k` 的位置配对。所以,新增的配对量就是加入前区间内 `pre[lp-1]^k` 的量(即 `cnt[pre[lp-1]^k]`)。因此,我们应该在加入前计算这个数量,然后加入,再更新计。 然而,标准做法是:加入一个位置时,先更新配对量(加上加入前区间内 `pre[new]^k` 的量),然后再将这个位置加入计(这样后续加入的位置就可以和这个位置配对)。这样,新位置不会和自己配对(因为加入前自己不在计组中)。 所以,左指针扩展(左移)的正确顺序是: ```cpp while (lp > l) { lp--; s += cnt[pre[lp] ^ k]; // 加入前,区间内有多少个等于pre[lp]^k的位置,这些位置都可以和lp配对 cnt[pre[lp]]++; // 将新位置加入计 } ``` 同理,右指针扩展(右移)时: ```cpp while (rp < r) { rp++; s += cnt[pre[rp] ^ k]; // 加入前,区间内有多少个等于pre[rp]^k的位置 cnt[pre[rp]]++; // 加入新位置 } ``` 收缩右边界(右指针左移)时: ```cpp while (rp > r) { cnt[pre[rp]]--; // 先移除位置 s -= cnt[pre[rp] ^ k]; // 然后减去移除后该位置能配对的个数?不对,应该是移除前,该位置能配对的个数是移除前区间等于pre[rp]^k的个数(包括自己?) // 但注意,移除前,该位置还在计组中,所以配对量是移除前区间等于pre[rp]^k的个数(不包括自己?因为配对要求两个不同位置,所以不会和自己配对?) // 实际上,在移除前,该位置还在计组中,所以cnt[pre[rp]^k]包括其他位置,但不包括自己(因为自己不能和自己配对,所以不影响)。 // 但是,移除后,该位置被移除,那么所有以该位置为右端(或左端)的配对都将消失。这个消失的配对量就是移除前区间等于pre[rp]^k的量(不包括自己)。 // 所以,应该先减去配对量,再移除? rp--; } ``` 然而,这里有一个问题:在移除位置 `rp` 时,我们需要减去的是以 `rp` 为右端(或左端)的配对量。这个数等于移除前区间等于 `pre[rp]^k` 的位置的量(注意,这些位置在区间内,且位置小于 `rp`(或大于,取决于定义))。但是,在移除前,`cnt[pre[rp]^k]` 已经包含了这些位置。所以,如果我们先移除,那么 `cnt[pre[rp]^k]` 就会少一个(如果 `pre[rp]^k` 等于 `pre[rp]` 的话,那么移除自己后,自己就不在计组中了,所以 `cnt[pre[rp]^k]` 会减少,但配对要求的是其他位置,所以实际上不应该包括自己)。因此,先移除再减配对会导致减去的量不正确(因为移除后,计组已经不包含这个位置,所以 `cnt[pre[rp]^k]` 可能比移除前少(如果 `pre[rp]^k` 等于 `pre[rp]`,那么移除自己后,计组中自己的那一项被移除,所以 `cnt[pre[rp]]` 减少,而 `pre[rp]^k` 如果等于 `pre[rp]`,则 `cnt[pre[rp]^k]` 也减少))。所以,正确的做法是先减去配对量,再移除: ```cpp while (rp > r) { // 先减去以rp为右端(或左端)的配对量 s -= cnt[pre[rp] ^ k]; // 注意:这里可能会减去自己?不会,因为配对要求两个不同的位置,所以自己不会和自己配对。但是,如果k=0,那么pre[i]^pre[j]=0 => pre[i]=pre[j],所以位置rp可以和所有它值相同的位置(包括自己?)配对?但是,自己不能和自己配对(因为需要两个位置,同一个位置不能作为两个端点)。所以,在计时,位置rp加入后,它只能和它之前加入的位置(位置比它小的)配对。因此,移除时,我们减去的是其他位置它配对的个数。 // 然而,这里有一个陷阱:如果k=0,那么配对要求两个位置的值相同,但是位置rp自己不能和自己配对(因为只有一个位置rp),所以配对的量是其他位置的值等于pre[rp]的量。 // 但是,在计组cnt中,位置rp已经被计入(因为我们还没有移除),所以cnt[pre[rp]]包括位置rp自己吗?是的,包括。所以,当我们计算 `cnt[pre[rp]^k]` 时(即 `cnt[pre[rp]]`,因为k=0),这个计包括了位置rp自己。因此,我们减去的量中多了一个自己(即多减了1)?这显然不对。 // 因此,为了避免这种情况,我们需要确保在计算配对量时,不能包括自己。所以,在移除时,应该先移除自己,然后再减配对?也不行,因为移除自己后,计组不再包括自己,那么 `cnt[pre[rp]^k]` 就是其他位置的量,这样减去的量就是正确的。但是,在移除自己后,我们减去的配对量是其他位置位置rp的配对,这些配对在移除前是存在的,移除后不存在了,所以减去。所以顺序应该是: // cnt[pre[rp]]--; // 先移除自己 // s -= cnt[pre[rp]^k]; // 再减去其他位置位置rp的配对(注意,此时位置rp已经被移除,所以cnt[pre[rp]^k]中不包括rp自己,所以正确) // 但是,这样操作后,我们发现:在移除自己后,其他位置rp的配对量应该是移除前其他位置中等于pre[rp]^k的量,但移除后,这个数量并没有改变(因为移除的是rp,不是其他位置)。所以,这样操作是可行的。 // 然而,仔细考虑:在移除自己后,计组不再包含rp,那么 `cnt[pre[rp]^k]` 就是其他位置中等于 `pre[rp]^k` 的量。而配对要求的是:位置j(j≠rp)满足 `pre[j]=pre[rp]^k`,这些配对在移除rp后就不存在了(因为配对需要rp这个位置)。所以,我们需要减去的量就是移除rp后,`cnt[pre[rp]^k]` 的值(因为移除rp不影响其他位置的量,所以移除rp后 `cnt[pre[rp]^k]` 就是其他位置中等于 `pre[rp]^k` 的量)。 // 因此,正确的顺序是:先移除自己,再减去配对量。 cnt[pre[rp]]--; // 先移除位置rp s -= cnt[pre[rp] ^ k]; // 再减去配对(注意,此时位置rp已经被移除,所以cnt[pre[rp]^k]表示其他位置中等于该值的量) rp--; } ``` 但是,这我们之前左指针收缩的顺序不一致。左指针收缩时,我们也是先移除自己,再减去配对?不对,我们在左指针收缩时,先移除自己,再减去配对,但这样会导致减去的配对量不包含自己(因为自己已经被移除),所以是其他位置自己的配对?这符合要求。 然而,这里有一个对称性:左指针收缩(移除lp)和右指针收缩(移除rp)应该采用相同的逻辑?实际上,配对是无向的,即 `pre[i]^pre[j]=k` 中,i和j没有顺序(但要求i<j)。所以,在区间内,我们统计的是所有满足条件的无序对(i,j)(i<j)。当我们移除一个位置(无论是lp还是rp)时,我们需要减去所有包含该位置的配对。这个配对等于移除前区间等于 `pre[x]^k` 的位置的量(不包括自己,因为自己不能和自己配对)。而移除后,计组中不再包含该位置,所以 `cnt[pre[x]^k]` 就是其他位置中等于 `pre[x]^k` 的量,也就是我们要减去的量。 因此,无论是左指针收缩还是右指针收缩,都应该: - 先移除自己:`cnt[pre[x]]--` - 然后减去 `cnt[pre[x]^k]`(此时这个值就是其他位置中等于 `pre[x]^k` 的量) 所以,修正后的指针移动函应该是: ```cpp void solve(ll l, ll r, ll &ans) { // 移动左指针:收缩(lp右移) while (lp < l) { cnt[pre[lp]]--; s -= cnt[pre[lp] ^ k]; lp++; } // 移动左指针:扩展(lp左移) while (lp > l) { lp--; cnt[pre[lp]]++; s += cnt[pre[lp] ^ k]; } // 移动右指针:扩展(rp右移) while (rp < r) { rp++; cnt[pre[rp]]++; s += cnt[pre[rp] ^ k]; } // 移动右指针:收缩(rp左移) while (rp > r) { cnt[pre[rp]]--; s -= cnt[pre[rp] ^ k]; rp--; } ans = s; } ``` 但是,这个顺序在扩展时是先加入位置,再加配对?这样会导致新加入的位置和自己配对(如果k=0)?不会,因为加入后,计组包含了这个位置,然后计算配对量 `cnt[pre[x]^k]` 时,这个数量包括了这个位置吗?注意,我们加入后立即增加了计,然后计算配对。但是,配对计算的是区间内其他位置(因为自己刚刚加入,还没有被用于配对计算?)实际上,配对计算是:新加入的位置x,它可以和区间内所有在它之前加入的位置(包括位置x自己?)配对?但位置x自己不能和自己配对(因为需要两个不同的位置)。所以,在加入位置x后,计组包含了x,那么 `cnt[pre[x]^k]` 就包括了位置x自己(如果 `pre[x]^k` 等于 `pre[x]`,即k=0,那么 `cnt[pre[x]]` 就包括了x自己)。所以,我们计算s时,会多算一个自己和自己配对(但实际上这是不允许的)。 因此,为了避免这种情况,扩展时应该先计算配对(加入前),再加入位置。这样,加入前,计组还不包含自己,所以 `cnt[pre[x]^k]` 是其他位置的量,加入后,这个位置就可以被后续的位置配对(但不会重复计算自己)。 所以,正确的扩展顺序是: 左指针扩展(左移): - 先计算:`s += cnt[pre[lp-1]^k]` (加入前,区间内其他位置等于 `pre[lp-1]^k` 的量) - 然后加入:`cnt[pre[lp-1]]++` - 然后 `lp--` 右指针扩展(右移): - 先计算:`s += cnt[pre[rp+1]^k]` (加入前) - 然后加入:`cnt[pre[rp+1]]++` - 然后 `rp++` (注意,这里我们移动rp到rp+1) 因此,修正后的指针移动函为: ```cpp void solve(ll l, ll r, ll &ans) { // 移动左指针:收缩(lp右移) while (lp < l) { cnt[pre[lp]]--; s -= cnt[pre[lp] ^ k]; lp++; } // 移动左指针:扩展(lp左移) while (lp > l) { lp--; s += cnt[pre[lp] ^ k]; cnt[pre[lp]]++; } // 移动右指针:扩展(rp右移) while (rp < r) { rp++; s += cnt[pre[rp] ^ k]; cnt[pre[rp]]++; } // 移动右指针:收缩(rp左移) while (rp > r) { cnt[pre[rp]]--; s -= cnt[pre[rp] ^ k]; rp--; } ans = s; } ``` 注意:在收缩时,我们先移除再减配对(上面已经论证可行)。在扩展时,先加配对再加入。 为什么扩展时先加配对再加入?因为加入的位置还没有进入计组,所以此时 `cnt[pre[x]^k]` 表示的是已经存在于区间内的、等于 `pre[x]^k` 的位置的量,这些位置都可以和新位置x配对。然后,我们将x加入计组,这样后续的位置就可以和x配对(避免了一次加入后立即和自己配对的情况)。 现在,考虑k=0的情况:新加入的位置x,如果区间内已经有 `pre[x]^0 = pre[x]` 的位置,那么这些位置就会和x配对。注意,这些位置是之前加入的,不包括x自己,所以不会出现自己和自己配对。 因此,这个顺序是合理的。 但是,在收缩时,我们移除位置x,然后减去 `cnt[pre[x]^k]`,这个 `cnt[pre[x]^k]` 是移除x后计组的值,所以它表示的是其他位置中等于 `pre[x]^k` 的量。而我们需要减去的是移除x之前,x这些位置的配对。在移除x前,计组中包含x,所以 `cnt[pre[x]^k]` 的值可能包含了x的影响吗?不会,因为 `pre[x]^k` 不一定等于 `pre[x]`,所以计组 `cnt[pre[x]^k]` 这个桶里,x的位置不会影响(除非 `pre[x]^k` 等于 `pre[x]`,即k=0)。当k=0时,`pre[x]^k = pre[x]`,那么在移除x前,计组中 `pre[x]` 的计包括x,而移除后,计减1,所以 `cnt[pre[x]]` 减少了1。因此,在移除后,我们减去的 `cnt[pre[x]]` 比移除前少1(即少了自己)。而我们需要减去的配对量是移除前其他位置中等于 `pre[x]` 的量(即不包括x),所以移除后 `cnt[pre[x]]` 的值(等于移除前 `cnt[pre[x]]-1`)就是其他位置的量,这正是我们想要的。 所以,收缩的顺序(先移除,再减配对)是正确的。 综上所述,我们采用以下顺序: 扩展(左移lp): lp-- s += cnt[pre[lp] ^ k] cnt[pre[lp]]++ 扩展(右移rp): rp++ s += cnt[pre[rp] ^ k] cnt[pre[rp]]++ 收缩(右移lp): cnt[pre[lp]]-- s -= cnt[pre[lp] ^ k] lp++ 收缩(左移rp): cnt[pre[rp]]-- s -= cnt[pre[rp] ^ k] rp-- 注意:收缩时,先移除再减配对;扩展时,先加配对再加入。 另外,初始状态:我们设区间为 [0,0],即 lp=0, rp=0,且 cnt[pre[0]]=1。 最后,我们还需要注意:在扩展和收缩过程中,配对的量s可能会因为k=0而出现负?不会,因为配对量是累积的。 下面我们用题目中的例子验证: 输入:n=4, m=5, k=1, a=[1,2,3,1] 前缀和: pre[0]=0 pre[1]=1 pre[2]=1^2=3 pre[3]=3^3=0 pre[4]=0^1=1 查询(注意,我们已经将查询的 l 减1,所以查询区间为 [l, r](前缀和组的下标)): 查询1: [0,4] -> l=0, r=4 查询2: [0,3] -> l=0, r=3 查询3: [1,3] -> l=1, r=3 查询4: [1,4] -> l=1, r=4 查询5: [3,4] -> l=3, r=4 初始状态:lp=0, rp=0, cnt[pre[0]]=1, s=0 (因为区间内只有一个位置0,无法配对,所以s=0) 处理第一个查询 [0,4]: 需要扩展右指针到4: rp=0->1: rp++ -> rp=1 s += cnt[pre[1]^k] = cnt[1^1]=cnt[0]=1 -> s=1 cnt[pre[1]]++ -> cnt[1] = 1 (原来0:1, 1:1) rp=1->2: rp=2 s += cnt[pre[2]^k] = cnt[3^1]=cnt[2]=0 -> s=1 cnt[pre[2]]++ -> cnt[3]=1 rp=2->3: rp=3 s += cnt[pre[3]^k] = cnt[0^1]=cnt[1]=1 -> s=2 cnt[pre[3]]++ -> cnt[0]=2 rp=3->4: rp=4 s += cnt[pre[4]^k] = cnt[1^1]=cnt[0]=2 -> s=4 cnt[pre[4]]++ -> cnt[1]=2 所以第一个查询的答案是4。 第二个查询 [0,3]: 需要收缩右指针从4到3: rp=4->3: cnt[pre[4]]-- -> cnt[1] becomes 1 s -= cnt[pre[4]^k] = cnt[1^1]=cnt[0]=2 -> s=4-2=2 rp=3 所以答案是2。 第三个查询 [1,3]: 需要收缩左指针从0到1: lp=0->1: cnt[pre[0]]-- -> cnt[0]=1 s -= cnt[pre[0]^k] = cnt[0^1]=cnt[1]=1 -> s=2-1=1 lp=1 所以答案是1。 第四个查询 [1,4]: 需要扩展右指针从3到4: rp=3->4: rp=4 s += cnt[pre[4]^k] = cnt[1^1]=cnt[0]=1 -> s=1+1=2 (注意:此时区间为[1,4],计组:cnt[0]=1, cnt[1]=1, cnt[3]=1, 因为pre[4]=1,所以cnt[1]在加入前为1(因为之前移除了pre[0]和pre[4]?实际上,当前区间是[1,3],所以计组:pre[1]=1, pre[2]=3, pre[3]=0 -> cnt[1]=1, cnt[3]=1, cnt[0]=1。然后加入pre[4]=1,所以加入前,cnt[pre[4]^k]=cnt[0]=1,所以s+=1) cnt[pre[4]]++ -> cnt[1]=2 所以答案是2。 第五个查询 [3,4]: 需要收缩左指针从1到3: lp=1->2: cnt[pre[1]]-- -> cnt[1]=1 s -= cnt[pre[1]^k] = cnt[1^1]=cnt[0]=1 -> s=2-1=1 lp=2 lp=2->3: cnt[pre[2]]-- -> cnt[3]=0 s -= cnt[pre[2]^k] = cnt[3^1]=cnt[2]=0 -> s=1 lp=3 所以答案是1。 输出:4,2,1,2,1 符合预期。 因此,最终代码采用上述指针移动顺序。 注意:在修正代码中,我们还需要注意组大小。因为异或值最大可能达到 2^17-1(因为a[i]<=1e5,而1e5<2^17=131072),所以cnt组的大小应该至少为 2^17(131072)的两倍?为了保险,可以设置为N*2+10(因为N=200000,所以最大异或值不会超过200000?不对,异或值可能达到(1<<17)-1,即131071,所以cnt组大小至少为131072*2?实际上,题目中a[i]和k都在0~1e5之间,所以异或值最大约为130000多,我们可以设置cnt组大小为200000(即2e5)的两倍,或者直接设为300000。 由于题目中a[i]<=1e5,n<=1e5,所以前缀和组的值最大不超过2^17-1(131071),所以我们可以将cnt组大小设为132000(或200000)。 在修正代码中,我们定义 `const ll M = 2e5 + 1000;` 然后 `cnt` 组大小为 `M`。 修改后的完整代码: 注意:原代码中 `cnt` 组的大小为 `N+5`,而 `N=2e5`,所以 `cnt` 组大小约为200000,足够(因为异或值最大131071<200000)。 因此,我们只需要修改指针移动的顺序,并确保初始状态正确。 最终代码(基于原代码风格):</think>在异或序列问题中,使用莫队算法统计子区间时,需要正确理解前缀和配对的计逻辑。以下是关键点解析和修正后的代码: ### 关键问题分析 1. **配对计原理**: - 子区间 `[x,y]` 的异或和 = `pre[y] ^ pre[x-1]` - 统计 `pre[j] ^ pre[i] = k` 的配对量(`i < j`) - 移动指针时,需计算位置变化带来的配对增减 2. **指针移动逻辑**: - **收缩时**(移除位置): - 先移除当前位置的计 - 再减去该位置产生的配对(避免自配对) - **扩展时**(加入位置): - 先计算新增配对(避免自配对) - 再加入当前位置计 3. **初始状态**: - 包含 `pre[0]=0`,确保子区间 `[1,y]` 可被统计 - 查询区间映射为前缀和下标 `[l-1, r]` ### 修正后代码 ```cpp #include <bits/stdc++.h> #define ll long long using namespace std; const ll N = 2e5; struct node { ll l, r, f, ans; } b[N + 5]; ll n, m, k; ll blk[N + 5], lenb; ll a[N + 5], pre[N + 5]; ll cnt[N * 2 + 5]; // 扩大防止值域溢出 ll lp, rp, s; // 莫队排序优化(奇偶排序) bool cmp(node l1, node l2) { if (blk[l1.l] == blk[l2.l]) { if (blk[l1.l] & 1) return l1.r < l2.r; return l1.r > l2.r; } return blk[l1.l] < blk[l2.l]; } bool cmpf(node l1, node l2) { return l1.f < l2.f; } void solve(ll l, ll r, ll &ans) { // 左指针收缩(右移) while (lp < l) { cnt[pre[lp]]--; s -= cnt[pre[lp] ^ k]; lp++; } // 左指针扩展(左移) while (lp > l) { lp--; s += cnt[pre[lp] ^ k]; cnt[pre[lp]]++; } // 右指针扩展(右移) while (rp < r) { rp++; s += cnt[pre[rp] ^ k]; cnt[pre[rp]]++; } // 右指针收缩(左移) while (rp > r) { cnt[pre[rp]]--; s -= cnt[pre[rp] ^ k]; rp--; } ans = s; } int main() { scanf("%lld%lld%lld", &n, &m, &k); // 分块设置 lenb = max(1LL, (ll)sqrt(n)); for (ll i = 0; i <= n; i++) { blk[i] = i / lenb; } // 前缀和初始化 pre[0] = 0; for (ll i = 1; i <= n; i++) { scanf("%lld", &a[i]); pre[i] = pre[i - 1] ^ a[i]; } // 存储查询(调整左边界) for (ll i = 1; i <= m; i++) { scanf("%lld%lld", &b[i].l, &b[i].r); b[i].l--; // 映射到前缀和区间 [l-1, r] b[i].f = i; } // 莫队排序 sort(b + 1, b + m + 1, cmp); // 初始化区间 [0,0] lp = 0; rp = 0; s = 0; cnt[pre[0]] = 1; // 包含pre[0] // 处理查询 for (ll i = 1; i <= m; i++) { solve(b[i].l, b[i].r, b[i].ans); } // 按原顺序输出 sort(b + 1, b + m + 1, cmpf); for (ll i = 1; i <= m; i++) { printf("%lld\n", b[i].ans); } return 0; } ``` ### 修正点详解 1. **前缀和映射**: - 查询区间 `[l, r]` → 前缀和下标 `[l-1, r]` - 通过 `b[i].l--` 实现转换 2. **指针移动顺序**: ```cpp // 收缩(移除位置) cnt[pre[pos]]--; // 先移除 s -= cnt[pre[pos]^k]; // 后减配对 // 扩展(加入位置) s += cnt[pre[pos]^k]; // 先加配对 cnt[pre[pos]]++; // 后加入 ``` 3. **初始状态**: - 区间 `[0,0]` 包含 `pre[0]=0` - `cnt[pre[0]] = 1` 初始化 4. **值域扩展**: - `cnt` 组大小设为 `N*2+5`(异或值最大约200,000) 5. **奇偶排序优化**: ```cpp if (blk[l1.l] & 1) return l1.r < l2.r; else return l1.r > l2.r; ``` ### 示例验证 输入: ``` 4 5 1 1 2 3 1 1 4 1 3 2 3 2 4 4 4 ``` 输出: ``` 4 2 1 2 1 ``` 完全匹配题目要求。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值