D. Pashmak and Parmida's problem - 题解
比赛与标签
比赛: Codeforces Round 261 (Div. 2) 标签: data structures, divide and conquer, sortings 难度: *1800
题目大意喵~
主人你好呀~!这道题是说,我们有一个由 n 个整数组成的序列 a。我们先来定义一个函数 f(l, r, x),它表示在序列的 l 到 r 这个区间里,数字 x 出现了多少次,喵~
我们的任务呢,就是要找出所有满足下面条件的数对 (i, j) 的数量:
1 ≤ i < j ≤ nf(1, i, a[i]) > f(j, n, a[j])
简单来说,就是要找有多少个 (i, j) 对,使得 a[i] 在序列开头到位置 i 的出现次数,严格大于 a[j] 在位置 j 到序列结尾的出现次数。是不是很有趣的说?
解题思路喵!
拿到题目,最直接的想法就是暴力枚举所有的 (i, j) 对,然后对每一个数对都去计算 f(1, i, a[i]) 和 f(j, n, a[j])。但是 n 高达 10^6,O(n^2) 的复杂度肯定会让电脑累坏的,会超时的说!所以我们要想一个更聪明的办法,喵~
第一步:简化问题
我们仔细看看这个条件 f(1, i, a[i]) > f(j, n, a[j])。
f(1, i, a[i])是a[i]在前缀a[1...i]中的出现次数。f(j, n, a[j])是a[j]在后缀a[j...n]中的出现次数。
这两个值似乎可以预处理出来!我们可以创建两个数组:
pref_count[i]:存储f(1, i, a[i])的值。suff_count[j]:存储f(j, n, a[j])的值。
怎么计算它们呢?
- 计算
pref_count: 我们从左到右遍历原数组a,用一个哈希表(unordered_map)来记录每个数字出现的次数。当遍历到i时,我们先将a[i]的计数值加一,然后把这个新的计数值存入pref_count[i]。这样一遍 O(n) 的遍历就可以搞定啦! - 计算
suff_count: 同样地,我们从右到左遍历原数组a,用另一个(或者清空后的)哈希表记录数字出现次数。当遍历到j时,同样先将a[j]计数值加一,再存入suff_count[j]。这也是 O(n) 的。
做完这一步,我们的问题就变成了:找出满足 i < j 且 pref_count[i] > suff_count[j] 的数对 (i, j) 的数量。
第二步:高效计数
现在问题简化了,但如果我们还是用两层循环来找这样的数对,不还是 O(n^2) 吗?不行不行,必须祭出我们的神器——树状数组(BIT)!
这个问题可以看作一个二维偏序问题(i < j 是一个维度,pref_count[i] > suff_count[j] 是另一个维度)。处理这类问题的经典思路是:固定一个维度,用数据结构优化另一个维度的查询。
让我们来固定 i,然后去寻找满足条件的 j。如果我们从左到右遍历 i,对于每个 i,我们需要查询所有 j > i 中,有多少个 suff_count[j] 小于 pref_count[i]。这需要在数组的后缀上进行查询,每次查询的范围都不同,处理起来有点麻烦。
逆向思维时间!如果我们从右到左遍历 i 呢? 我们让 i 从 n-2 遍历到 0(代码实现中的0-based索引)。当我们处理 i 的时候,我们已经处理过了所有比 i 大的索引。 我们可以用一个树状数组,来维护我们已经 "路过" 的 j 的 suff_count[j] 值的频率分布。
具体的算法是这样的喵:
- 初始化一个空的树状数组
BIT和一个总数total_pairs = 0。树状数组的大小是n+1,因为suff_count的值最大是n。 - 我们从
i = n-2开始,一直循环到i = 0。 - 在处理当前的
i之前,我们先把i+1这个位置看作一个潜在的j。我们将它的后缀计数值suff_count[i+1]加入到我们的数据结构中。具体操作是BIT.add(suff_count[i+1], 1),表示值为suff_count[i+1]的后缀计数又多了一个。 - 现在,树状数组里存储了所有
j > i的suff_count[j]的频率信息。对于当前的i,我们需要找有多少个j满足suff_count[j] < pref_count[i]。这不就是查询树状数组中,值域在[1, pref_count[i] - 1]区间内的元素个数嘛! - 这个查询操作正是树状数组的拿手好戏!我们用
BIT.query(pref_count[i] - 1)就能得到这个数量。 - 把查询到的结果累加到
total_pairs中。 - 循环结束,
total_pairs就是我们想要的答案啦!
整个过程,预处理是 O(n),主循环 n 次,每次循环中树状数组的操作是 O(log n),所以总的时间复杂度是 O(n log n),完全可以接受!完美解决,喵~
代码实现喵
#include <iostream>
#include <vector>
#include <unordered_map>
// 树状数组我们用1-based索引,这样写起来最方便喵~
// 计数值的范围是 1 到 n,所以树状数组大小要 n+1
// MAXN 开到 10^6 + 5 更安全一些
const int MAXN = 1000005;
int bit[MAXN];
int n;
// 在树状数组的 idx 位置加上 delta
void add(int idx, int delta) {
for (; idx <= n; idx += idx & -idx) {
bit[idx] += delta;
}
}
// 查询树状数组中 [1, idx] 区间的和
long long query(int idx) {
long long sum = 0;
for (; idx > 0; idx -= idx & -idx) {
sum += bit[idx];
}
return sum;
}
int main() {
// 处理大数据量,加速输入输出是好习惯哦~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
// 代码里用的是0-based索引,题目描述的1-based i, j 对应代码里的 i-1, j-1
// 计算 pref_count
// pref_count[i] 对应题目中的 f(1, i+1, a_i)
// 也就是 a[i] 在前缀 a[0...i] 中出现的次数
std::vector<int> pref_count(n);
std::unordered_map<int, int> freq;
for (int i = 0; i < n; ++i) {
pref_count[i] = ++freq[a[i]];
}
// 计算 suff_count
// suff_count[j] 对应题目中的 f(j+1, n, a_j)
// 也就是 a[j] 在后缀 a[j...n-1] 中出现的次数
std::vector<int> suff_count(n);
freq.clear(); // 清空 map,重新使用
for (int i = n - 1; i >= 0; --i) {
suff_count[i] = ++freq[a[i]];
}
// 我们要统计满足 0 <= i < j < n 且 pref_count[i] > suff_count[j] 的数对 (i, j)
//
// 我们从右往左遍历 i (从 n-2 到 0)
// 对每个 i,我们统计有多少个 j > i 满足 suff_count[j] < pref_count[i]
// 用树状数组来维护 j > i 的 suff_count[j] 的频率分布
long long total_pairs = 0;
// 全局/静态数组 bit 默认初始化为0,所以不用手动清零~
// 循环 i 从 n-2 到 0 (对应1-based的 i 从 n-1 到 1)
// i = n-1 (0-based) 不需要考虑,因为没有 j > i
for (int i = n - 2; i >= 0; --i) {
// 当我们把目光从 i+1 移到 i 时,索引 j = i+1 就变成了一个可用的 j
// 我们可以把它对应的后缀计数值加入到数据结构中
add(suff_count[i + 1], 1);
// 现在,树状数组里包含了所有 j > i 的 suff_count[j] 的频率信息
// 我们查询有多少个 j 满足 suff_count[j] < pref_count[i]
// 这就是求值在 1 到 pref_count[i] - 1 范围内的频率总和
// 如果 pref_count[i] 是 1, 查询范围是空的,可以跳过
if (pref_count[i] > 1) {
total_pairs += query(pref_count[i] - 1);
}
}
std::cout << total_pairs << std::endl;
return 0;
}复杂度分析
- 时间复杂度: O(n log n) 的说。
- 预处理
pref_count和suff_count使用了unordered_map,平均每次操作是 O(1),所以这两步都是 O(n) 的。 - 主循环遍历
i从n-2到0,共n-1次。在循环内部,add和query操作都是树状数组的标准操作,每次耗时 O(log n)。 - 所以总的时间复杂度是 O(n) + O(n log n) = O(n log n)。
- 预处理
- 空间复杂度: O(n) 的说。
- 输入数组
a,以及我们创建的pref_count和suff_count数组都需要 O(n) 的空间。 - 树状数组
bit也需要 O(n) 的空间。 unordered_map在最坏情况下(所有元素都不同)也需要 O(n) 的空间。- 所以总的空间复杂度是 O(n)。
- 输入数组
知识点与总结喵~
这道题真是一次愉快的思维体操呢!我们来总结一下学到了什么吧:
- 问题转化: 把一个看起来复杂的条件
f(1, i, a[i]) > f(j, n, a[j]),通过预处理转化为更直观的pref_count[i] > suff_count[j],是解题的第一步,也是非常关键的一步! - 逆向思维: "从右往左" 的遍历顺序是解决这类计数问题的神来之笔。它巧妙地把对 "未来"(更大下标)的查询,转化为了对 "过去"(已处理过的更大下标)的维护,让问题变得简单。
- 树状数组 (BIT): 树状数组是我们的好朋友!它在处理动态单点更新和前缀和查询的问题上效率极高。这道题就是它大显身手的好地方,用来统计满足特定值域范围的元素个数,非常高效,喵~
- 二维偏序: 这个问题本质上是一个二维偏序计数问题。通过排序(或按序处理)一个维度,再用数据结构处理另一个维度,是解决这类问题的经典范式。
希望这篇题解能帮助到主人哦!继续努力,享受算法的乐趣吧!喵~ >w<