#lydlx04x0904. 作诗
作诗
作诗
题目描述
达达是 T 国的公主,平时的一大爱好是作诗。
由于时间紧迫,达达作完诗之后还要虐 OI,于是达达找来一篇长度为 的文章,阅读 次,每次只阅读其中连续的一段 ,从这一段中选出一些汉字构成诗。
规则
- 达达规定最后选出的每个汉字都必须在 里出现了正偶数次
- 达达认为选出的汉字的种类数(两个一样的汉字称为同一种)越多越好
- 每次询问需要求出 中有多少个数出现正偶数次
强制在线
设上一个询问的答案为 (第一个询问时 ),令:
若 ,交换 和 ,则本次询问为 。
输入格式
输入第一行包含三个整数 、 以及 ,表示文章字数、汉字的种类数、要选择 次。
第二行有 个整数,每个数 在 间,代表一个编码为 的汉字。
接下来 行每行两个整数 和 ,需要按照上述规则解密得到真正的 。
输出格式
输出共 行,每行一个整数,第 个数表示达达第 次能选出的汉字的最多种类数。
数据范围
输入样例
5 3 5
1 2 2 3 1
0 4
1 2
2 2
2 3
3 5
输出样例
2
0
0
0
1
样例解释
初始数据
- :文章长度5
- :汉字种类1,2,3
- :5次询问
- 文章:
[1, 2, 2, 3, 1]
第一次询问:0 4
- (第一个询问)
- :整个文章区间
统计区间 中每个数字出现次数:
- 1出现2次(偶数次)✓
- 2出现2次(偶数次)✓
- 3出现1次(奇数次)✗
出现偶数次的数字种类数:2(数字1和2)
输出:2
更新
第二次询问:1 2
- :位置4和5
统计区间 :
- 位置4:3
- 位置5:1
- 数字1出现1次(奇数次)
- 数字3出现1次(奇数次)
没有出现偶数次的数字
输出:0
更新
第三次询问:2 2
- :位置3
统计区间 :
- 位置3:2
- 数字2出现1次(奇数次)
没有出现偶数次的数字
输出:0
更新
第四次询问:2 3
- :位置3和4
统计区间 :
- 位置3:2
- 位置4:3
- 数字2出现1次(奇数次)
- 数字3出现1次(奇数次)
没有出现偶数次的数字
输出:0
更新
第五次询问:3 5
- ,交换得
- :位置1到4
统计区间 :
- 位置1:1
- 位置2:2
- 位置3:2
- 位置4:3
- 数字1出现1次(奇数次)
- 数字2出现2次(偶数次)✓
- 数字3出现1次(奇数次)
出现偶数次的数字种类数:1(数字2)
输出:1
问题分析
问题重述
给定一个长度为 的序列, 次询问,每次询问区间 中出现正偶数次的数的种类数。
强制在线
每次询问的 需要根据上一次的答案解密得到,不能离线处理。
数据范围
,需要 或 的算法。
解决方案:分块
分块思想
将序列分成 个块,每块大小 。
预处理:
- :前 个块中,数字 出现的次数
- :从第 个块到第 个块的答案
预处理
设块大小为 ,块数为
1. 预处理 数组
表示前 个块中,数字 出现的次数
- 空间:,但 ,,空间约 ,可能过大
- 优化:只存储每个块内数字出现次数,查询时累加
2. 预处理 数组
表示从第 个块到第 个块的答案
- 对于每对块 ,计算其中的答案
- 空间:,可接受
查询处理
对于查询 :
- 设 在块 , 在块
- 如果 或相邻,直接暴力统计:
- 否则:
- 先用 得到中间完整块的答案
- 再处理左右不完整部分,更新答案
具体实现细节
预处理 数组
对于每个起始块 :
- 初始化一个数组 记录数字 的出现次数
- 初始化答案
- 从块 开始,依次加入后面的块:
- 对于每个加入的数字 ,
- 如果 变为2,(从奇数变偶数)
- 如果 变为3,(从偶数变奇数)
- 记录 ( 为当前块)
查询
设 在块 , 在块
-
如果 或 :
- 直接暴力统计区间内每个数字出现次数
- 计算出现偶数次的数字种类数
-
否则():
- 取中间完整块的答案
- 复制中间块的数字频率到临时数组
- 处理左边不完整部分 :
- 对于每个数字 ,在 中更新
- 根据变化更新
- 处理右边不完整部分 :
- 类似更新
时间复杂度
预处理
- 计算 数组:
- 对于每个起始块 ,需要遍历后面的所有元素
查询
- 理想情况:(如果不需要处理不完整块)
- 最坏情况:(需要处理不完整块)
总复杂度:
空间优化
数组优化
不直接存储 ,而是:
- 存储每个块内数字出现次数 (稀疏存储,只存储出现的数字)
- 或者使用 vector 存储每个数字出现的位置,查询时二分
使用位置向量
对于每个数字 ,存储它在序列中出现的所有位置 。 查询 时,对于每个需要考虑的数字,在 中二分查找在 内的出现次数。
但这样每次查询需要遍历所有可能数字,复杂度高。
实际实现
数据结构
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;
}
注意事项
- 空间优化: 数组可能很大,需要优化存储
- 边界处理:注意块边界
- 频率更新逻辑:当频率从奇数变偶数时增加答案,从偶数变奇数时减少答案
- 强制在线:每次查询后更新
时空限制
- 时间限制:2秒
- 空间限制:256MB
对于 , 算法可以在2秒内完成。