#lydlx04x0904. 作诗

作诗

作诗

题目描述

达达是 T 国的公主,平时的一大爱好是作诗。

由于时间紧迫,达达作完诗之后还要虐 OI,于是达达找来一篇长度为 NN 的文章,阅读 MM 次,每次只阅读其中连续的一段 [l,r][l,r],从这一段中选出一些汉字构成诗。

规则

  1. 达达规定最后选出的每个汉字都必须在 [l,r][l,r] 里出现了正偶数次
  2. 达达认为选出的汉字的种类数(两个一样的汉字称为同一种)越多越好
  3. 每次询问需要求出 [l,r][l,r] 中有多少个数出现正偶数次

强制在线

设上一个询问的答案为 ans\text{ans}(第一个询问时 ans=0\text{ans}=0),令:

L=(l+ans)modn+1L = (l + \text{ans}) \mod n + 1 R=(r+ans)modn+1R = (r + \text{ans}) \mod n + 1

L>RL > R,交换 LLRR,则本次询问为 [L,R][L,R]

输入格式

输入第一行包含三个整数 nncc 以及 mm,表示文章字数、汉字的种类数、要选择 mm 次。

第二行有 nn 个整数,每个数 A[i]A[i][1,c][1,c] 间,代表一个编码为 A[i]A[i] 的汉字。

接下来 mm 行每行两个整数 llrr,需要按照上述规则解密得到真正的 L,RL, R

输出格式

输出共 mm 行,每行一个整数,第 ii 个数表示达达第 ii 次能选出的汉字的最多种类数。

数据范围

1n,c,m1051 \le n, c, m \le 10^5

输入样例

5 3 5
1 2 2 3 1
0 4
1 2
2 2
2 3
3 5

输出样例

2
0
0
0
1

样例解释

初始数据

  • n=5n=5:文章长度5
  • c=3c=3:汉字种类1,2,3
  • m=5m=5:5次询问
  • 文章:[1, 2, 2, 3, 1]

第一次询问:0 4

  • ans=0\text{ans}=0(第一个询问)
  • L=(0+0)mod5+1=0+1=1L = (0+0) \mod 5 + 1 = 0 + 1 = 1
  • R=(4+0)mod5+1=4+1=5R = (4+0) \mod 5 + 1 = 4 + 1 = 5
  • [L,R]=[1,5][L,R] = [1,5]:整个文章区间

统计区间 [1,5][1,5] 中每个数字出现次数:

  • 1出现2次(偶数次)✓
  • 2出现2次(偶数次)✓
  • 3出现1次(奇数次)✗

出现偶数次的数字种类数:2(数字1和2) 输出:2

更新 ans=2\text{ans}=2

第二次询问:1 2

  • ans=2\text{ans}=2
  • L=(1+2)mod5+1=3+1=4L = (1+2) \mod 5 + 1 = 3 + 1 = 4
  • R=(2+2)mod5+1=4+1=5R = (2+2) \mod 5 + 1 = 4 + 1 = 5
  • [L,R]=[4,5][L,R] = [4,5]:位置4和5

统计区间 [4,5][4,5]

  • 位置4:3
  • 位置5:1
  • 数字1出现1次(奇数次)
  • 数字3出现1次(奇数次)

没有出现偶数次的数字 输出:0

更新 ans=0\text{ans}=0

第三次询问:2 2

  • ans=0\text{ans}=0
  • L=(2+0)mod5+1=2+1=3L = (2+0) \mod 5 + 1 = 2 + 1 = 3
  • R=(2+0)mod5+1=2+1=3R = (2+0) \mod 5 + 1 = 2 + 1 = 3
  • [L,R]=[3,3][L,R] = [3,3]:位置3

统计区间 [3,3][3,3]

  • 位置3:2
  • 数字2出现1次(奇数次)

没有出现偶数次的数字 输出:0

更新 ans=0\text{ans}=0

第四次询问:2 3

  • ans=0\text{ans}=0
  • L=(2+0)mod5+1=2+1=3L = (2+0) \mod 5 + 1 = 2 + 1 = 3
  • R=(3+0)mod5+1=3+1=4R = (3+0) \mod 5 + 1 = 3 + 1 = 4
  • [L,R]=[3,4][L,R] = [3,4]:位置3和4

统计区间 [3,4][3,4]

  • 位置3:2
  • 位置4:3
  • 数字2出现1次(奇数次)
  • 数字3出现1次(奇数次)

没有出现偶数次的数字 输出:0

更新 ans=0\text{ans}=0

第五次询问:3 5

  • ans=0\text{ans}=0
  • L=(3+0)mod5+1=3+1=4L = (3+0) \mod 5 + 1 = 3 + 1 = 4
  • R=(5+0)mod5+1=0+1=1R = (5+0) \mod 5 + 1 = 0 + 1 = 1
  • L=4>R=1L=4 > R=1,交换得 L=1,R=4L=1, R=4
  • [L,R]=[1,4][L,R] = [1,4]:位置1到4

统计区间 [1,4][1,4]

  • 位置1:1
  • 位置2:2
  • 位置3:2
  • 位置4:3
  • 数字1出现1次(奇数次)
  • 数字2出现2次(偶数次)✓
  • 数字3出现1次(奇数次)

出现偶数次的数字种类数:1(数字2) 输出:1

问题分析

问题重述

给定一个长度为 nn 的序列,mm 次询问,每次询问区间 [l,r][l,r]出现正偶数次的数的种类数

强制在线

每次询问的 l,rl,r 需要根据上一次的答案解密得到,不能离线处理。

数据范围

n,c,m105n, c, m \le 10^5,需要 O(nn)O(n\sqrt{n})O(nlogn)O(n\log n) 的算法。

解决方案:分块

分块思想

将序列分成 n\sqrt{n} 个块,每块大小 n\sqrt{n}

预处理:

  1. cnt[i][j]cnt[i][j]:前 ii 个块中,数字 jj 出现的次数
  2. ans[i][j]ans[i][j]:从第 ii 个块到第 jj 个块的答案

预处理

设块大小为 B=nB = \sqrt{n},块数为 T=n/BT = \lceil n/B \rceil

1. 预处理 cntcnt 数组

cnt[i][j]cnt[i][j] 表示前 ii 个块中,数字 jj 出现的次数

  • 空间:O(T×c)O(T \times c),但 c105c \le 10^5T316T \approx 316,空间约 3.16×1073.16 \times 10^7,可能过大
  • 优化:只存储每个块内数字出现次数,查询时累加

2. 预处理 ansans 数组

ans[i][j]ans[i][j] 表示从第 ii 个块到第 jj 个块的答案

  • 对于每对块 (i,j)(i,j),计算其中的答案
  • 空间:O(T2)105O(T^2) \approx 10^5,可接受

查询处理

对于查询 [L,R][L,R]

  1. LL 在块 ppRR 在块 qq
  2. 如果 p=qp=q 或相邻,直接暴力统计:O(B)O(B)
  3. 否则:
    • 先用 ans[p+1][q1]ans[p+1][q-1] 得到中间完整块的答案
    • 再处理左右不完整部分,更新答案

具体实现细节

预处理 ansans 数组

对于每个起始块 ii

  1. 初始化一个数组 freq[j]freq[j] 记录数字 jj 的出现次数
  2. 初始化答案 res=0res = 0
  3. 从块 ii 开始,依次加入后面的块:
    • 对于每个加入的数字 xxfreq[x]++freq[x]++
    • 如果 freq[x]freq[x] 变为2,res++res++(从奇数变偶数)
    • 如果 freq[x]freq[x] 变为3,resres--(从偶数变奇数)
  4. 记录 ans[i][k]=resans[i][k] = reskk 为当前块)

查询 [L,R][L,R]

LL 在块 ppRR 在块 qq

  1. 如果 p=qp=qp+1=qp+1=q

    • 直接暴力统计区间内每个数字出现次数
    • 计算出现偶数次的数字种类数
  2. 否则(p+1<qp+1 < q):

    • 取中间完整块的答案 res=ans[p+1][q1]res = ans[p+1][q-1]
    • 复制中间块的数字频率到临时数组 temptemp
    • 处理左边不完整部分 [L,p块结尾][L, p块结尾]
      • 对于每个数字 xx,在 temptemp 中更新
      • 根据变化更新 resres
    • 处理右边不完整部分 [q块开头,R][q块开头, R]
      • 类似更新

时间复杂度

预处理

  • 计算 ansans 数组:O(T2×B)=O(nn)O(T^2 \times B) = O(n\sqrt{n})
  • 对于每个起始块 ii,需要遍历后面的所有元素

查询

  • 理想情况:O(1)O(1)(如果不需要处理不完整块)
  • 最坏情况:O(B)=O(n)O(B) = O(\sqrt{n})(需要处理不完整块)

总复杂度:O(nn+mn)O(n\sqrt{n} + m\sqrt{n})

空间优化

cntcnt 数组优化

不直接存储 cnt[i][j]cnt[i][j],而是:

  1. 存储每个块内数字出现次数 block[i][j]block[i][j](稀疏存储,只存储出现的数字)
  2. 或者使用 vector 存储每个数字出现的位置,查询时二分

使用位置向量

对于每个数字 xx,存储它在序列中出现的所有位置 pos[x]pos[x]。 查询 [L,R][L,R] 时,对于每个需要考虑的数字,在 pos[x]pos[x] 中二分查找在 [L,R][L,R] 内的出现次数。

但这样每次查询需要遍历所有可能数字,复杂度高。

实际实现

数据结构

const int MAXN = 100010;
const int BLOCK = 320; // sqrt(100000) ≈ 316

int n, c, m;
int a[MAXN];
int block_size, block_cnt;
int belong[MAXN]; // 每个位置属于哪个块
int L[BLOCK], R[BLOCK]; // 每个块的左右边界

// ans[i][j]: 从块i到块j的答案
int ans[BLOCK][BLOCK];
// freq[i][j]: 前i个块中数字j的出现次数(或者使用其他方法)

预处理

void preprocess() {
    block_size = sqrt(n);
    block_cnt = (n + block_size - 1) / block_size;
    
    // 计算每个块的边界和每个位置所属块
    for (int i = 1; i <= block_cnt; i++) {
        L[i] = (i-1) * block_size + 1;
        R[i] = min(i * block_size, n);
        for (int j = L[i]; j <= R[i]; j++) {
            belong[j] = i;
        }
    }
    
    // 预处理ans数组
    for (int i = 1; i <= block_cnt; i++) {
        vector<int> freq(c+1, 0);
        int res = 0;
        for (int j = i; j <= block_cnt; j++) {
            // 加入第j块的所有元素
            for (int k = L[j]; k <= R[j]; k++) {
                freq[a[k]]++;
                if (freq[a[k]] % 2 == 0) {
                    res++; // 从奇数变偶数
                } else if (freq[a[k]] > 2) {
                    res--; // 从偶数变奇数(出现超过2次)
                }
            }
            ans[i][j] = res;
        }
    }
}

查询

int query(int l, int r) {
    int p = belong[l], q = belong[r];
    
    if (p == q || p + 1 == q) {
        // 直接暴力
        vector<int> freq(c+1, 0);
        int res = 0;
        for (int i = l; i <= r; i++) {
            freq[a[i]]++;
            if (freq[a[i]] % 2 == 0) {
                res++;
            } else if (freq[a[i]] > 2) {
                res--;
            }
        }
        return res;
    }
    
    // 中间有完整块
    int res = ans[p+1][q-1];
    vector<int> freq(c+1, 0);
    
    // 复制中间块的频率信息(需要额外存储,这里简化)
    // 实际需要存储每个块的频率信息,或重新统计
    
    // 处理左边不完整部分
    for (int i = l; i <= R[p]; i++) {
        freq[a[i]]++;
        // 更新res逻辑...
    }
    
    // 处理右边不完整部分
    for (int i = L[q]; i <= r; i++) {
        freq[a[i]]++;
        // 更新res逻辑...
    }
    
    return res;
}

强制在线处理

int last_ans = 0;
for (int i = 0; i < m; i++) {
    int l, r;
    cin >> l >> r;
    
    // 解密
    l = (l + last_ans) % n + 1;
    r = (r + last_ans) % n + 1;
    if (l > r) swap(l, r);
    
    last_ans = query(l, r);
    cout << last_ans << endl;
}

注意事项

  1. 空间优化cntcnt 数组可能很大,需要优化存储
  2. 边界处理:注意块边界
  3. 频率更新逻辑:当频率从奇数变偶数时增加答案,从偶数变奇数时减少答案
  4. 强制在线:每次查询后更新 last_anslast\_ans

时空限制

  • 时间限制:2秒
  • 空间限制:256MB

对于 n,m,c105n,m,c \le 10^5O(nn)O(n\sqrt{n}) 算法可以在2秒内完成。