1 条题解
-
0
题目解析
问题重述
维护一个长度为 的数组 ,支持两种操作:
- 单点修改:将 改为
- 区间查询:统计区间 内值不同的有序对 的数量,其中
查询是在线的,因为每个查询需要上一个第二类查询的答案来解码。
核心思路
1. 问题转化
区间 内,总共有 个有序对,其中 。
设区间内每个值 的出现次数为 ,则值相同的对数为 。
因此,值不同的对数 = 总对数 - 值相同的对数:
其中 。
2. 维护信息
我们需要维护区间内每个值的出现次数,以便快速计算 。
当某个值的出现次数从 变为 时, 的变化量为:
$$\Delta = \binom{c'}{2} - \binom{c}{2} = \frac{c'(c'-1) - c(c-1)}{2} $$特别地:
- 增加一个元素:,
- 减少一个元素:,
3. 数据结构选择
- 单点修改 + 区间查询 → 使用树状数组或线段树
- 需要维护每个值的出现次数,并且支持快速修改和查询
- 由于 ,值域大小为 ,可以按值维护出现次数
方法一:CDQ 分治 + 离线(但本题在线,不适用)
方法二:树状数组套平衡树(复杂度过高)
方法三:莫队算法(本题 较大, 较大,莫队 可能超时,且需要在线)
方法四:分块 + 维护每个块内信息(可行,但需要处理修改)
方法五:使用带修莫队(支持修改的莫队,复杂度 ,本题 可能较大)
实际上,本题正解是使用分块 + 维护每个值在每个块内的出现次数,并用树状数组维护每个值的全局出现次数,但修改时需要更新所有块。
4. 更简洁的思路
注意到 ,所以:
$$\sum_v \binom{cnt_v}{2} = \frac{\sum_v cnt_v^2 - \sum_v cnt_v}{2} = \frac{\sum_v cnt_v^2 - len}{2} $$因此,答案可以改写为:
$$ans = \frac{len(len-1)}{2} - \frac{\sum_v cnt_v^2 - len}{2} = \frac{len^2 - \sum_v cnt_v^2}{2} $$即:
这个公式非常优美:
- 很容易计算
- 可以通过维护平方和来快速更新
当某个值的出现次数从 变为 时, 的变化量为 。
5. 使用 BIT 维护
我们可以对每个值 维护一个 BIT,表示该值在数组中的出现位置。但这样空间复杂度 不可行。
更好的方法:使用 Fenwick Tree 套哈希表,但实现复杂。
实际上,由于 ,我们可以使用分块:
- 将数组分成 块,每块大小约
- 维护每块内每个值的出现次数
- 维护全局每个值的出现次数
- 查询时,整块直接使用块的统计,散块暴力统计
- 修改时,更新所在块的统计和全局统计
6. 最终算法
数据结构:
block_size = sqrt(n)block_cnt[block_id][value]:每个块内每个值的出现次数(值域 ,但 块,每块 种值,总空间 ,约 ,可以接受)global_cnt[value]:全局每个值的出现次数sum_sq:全局
查询 :
- 计算
- 初始化
- 处理整块:对每个整块的每个值,
- 处理散块:暴力统计散块内每个值的出现次数,累加平方
- 答案 =
但每次查询都遍历所有值()会超时。需要优化:维护每块的平方和,查询时整块直接取块的平方和。
优化后:
- 每块维护块内 (记为
block_sum_sq[block_id]) - 查询时:
- 整块的贡献直接取
block_sum_sq - 散块暴力统计,计算贡献后加到
total_sum_sq - 注意散块中出现的值可能在整块中也出现过,需要合并统计
- 整块的贡献直接取
7. 合并统计的方法
对于区间 ,我们需要的是区间内每个值的出现次数的平方和,而不是块内独立统计。
因此,我们需要:
- 遍历所有整块,记录每个值在整块中出现的次数(可以用临时数组或哈希表,但值域 ,可以直接用数组)
- 遍历散块,累加出现次数
- 最后计算
由于 可能很大,但每次查询涉及的散块元素只有 ,整块数量也是 ,所以每次查询的复杂度约为 ,但值域 会很大(),遍历所有值不可取。
更好的方法:
- 整块的贡献直接用块内统计的平方和,但这样不同块之间的相同值会被重复计算平方,而不是先合并再平方
实际上,块内平方和不能直接相加,因为 。
所以我们需要在查询时动态合并统计。
由于 ,整块数量不超过 ,散块元素不超过 ,所以总涉及的不同值数量是 (因为在散块中出现的值最多 个,整块中出现的值可能很多,但我们可以只记录整块中那些也在散块中出现的值)。
解决方案:
- 用哈希表或数组记录区间内每个值的出现次数(数组需要 清零,不可行)
- 用
unordered_map记录出现过的值(均摊 )
由于每次查询涉及的整块和散块元素总数是 ,因此
unordered_map的大小也是 。算法步骤
- 预处理分块:
block_size = sqrt(n) - 初始化全局数组
a和每块的统计 - 对于每个查询:
- 解码(使用
last) - 如果是修改操作:
- 找到 所在的块
- 更新该块的统计和全局统计
- 如果是查询操作:
- 用
unordered_map统计区间 内每个值的出现次数 - 遍历整块(值出现次数直接取块统计)
- 遍历散块(逐个元素统计)
- 计算
- 答案 =
- 更新
last
- 用
- 解码(使用
时间复杂度
- 每次修改:(更新块统计和全局统计)
- 每次查询:(遍历块和散块)
- 总复杂度:,约 ,勉强可过
C++ 代码
#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; const int MAXV = 100005; int n, q; int a[MAXN]; int block_size, block_num; vector<int> block_id; vector<unordered_map<int, int>> block_cnt; vector<long long> block_sum_sq; void init() { block_size = sqrt(n); block_num = (n + block_size - 1) / block_size; block_id.resize(n); block_cnt.resize(block_num); block_sum_sq.resize(block_num, 0); for (int i = 0; i < n; i++) { block_id[i] = i / block_size; int val = a[i]; int b = block_id[i]; block_cnt[b][val]++; block_sum_sq[b] += 2LL * block_cnt[b][val] - 1; // 当 cnt 从 c-1 变为 c 时,c^2 - (c-1)^2 = 2c - 1 } } void update(int pos, int new_val) { int b = block_id[pos]; int old_val = a[pos]; // 更新旧值的统计 int old_cnt = block_cnt[b][old_val]; block_sum_sq[b] -= 2LL * old_cnt - 1; if (--block_cnt[b][old_val] == 0) { block_cnt[b].erase(old_val); } // 更新新值的统计 int new_cnt = block_cnt[b][new_val]; block_sum_sq[b] += 2LL * new_cnt + 1; block_cnt[b][new_val] = new_cnt + 1; a[pos] = new_val; } long long query(int l, int r) { l--; r--; int len = r - l + 1; unordered_map<int, int> freq; int bl = block_id[l]; int br = block_id[r]; if (bl == br) { // 同一块内,暴力统计 for (int i = l; i <= r; i++) { freq[a[i]]++; } } else { // 左侧散块 for (int i = l; i < (bl + 1) * block_size; i++) { freq[a[i]]++; } // 右侧散块 for (int i = br * block_size; i <= r; i++) { freq[a[i]]++; } // 中间整块 for (int b = bl + 1; b < br; b++) { for (auto& [val, cnt] : block_cnt[b]) { freq[val] += cnt; } } } long long sum_sq = 0; for (auto& [val, cnt] : freq) { sum_sq += 1LL * cnt * cnt; } return (1LL * len * len - sum_sq) / 2; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; } init(); cin >> q; long long last = 0; for (int i = 0; i < q; i++) { int type, x, y; cin >> type >> x >> y; if (type == 1) { int p = ((x + last) % n); int val = ((y + last) % n) + 1; update(p, val); } else { int l = ((x + last) % n) + 1; int r = ((y + last) % n) + 1; if (l > r) swap(l, r); long long ans = query(l, r); cout << ans << " \n"[i == q - 1]; last = ans; } } return 0; }关键点总结
- 公式转化:不同对数量 = 总对数 - 相同对数量 =
- 分块策略: 查询, 修改
- 在线解码:使用
last变量逐次解码查询参数 - 值域压缩:,可以用数组或哈希表统计
- 空间优化:每个块用
unordered_map存储非零值,避免 空间
注意事项
- 使用
long long存储平方和,避免溢出 - 修改时更新块内统计和平方和的公式:
- 查询时整块和散块的统计需要合并
- 输出格式:每个查询答案后跟一个空格,最后一个查询后换行
- 1
信息
- ID
- 7071
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 9
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者