逆序k倍对·复盘
前言
本题同样由那位BUAA大佬分享。
不久前,我用树状数组解决了洛谷上的逆序对模板题 。因此,当我初见这道 逆序 k 倍对 时,第一反应便是:这一定是模板的变体,核心工具依然是那个熟悉的树状数组。然而,从模板代码到这道题的AC,并非简单的复制粘贴,而是一段充满思考的魔改之旅。本文旨在完整复盘,我是如何基于逆序对模板的思路,一步步识别问题差异、调整关键逻辑,最终将一份模板代码成功改造为本题的解答。
题目
题目描述
给定一个序列 a₁, a₂, …, aₙ 和一个正整数 k,如果 1 ≤ i < j ≤ n
且 aᵢ > k · aⱼ
,我们就将 (i, j)
称作一个逆序 k 倍对。请你计算序列中逆序 k 倍对的个数。
输入格式
第一行两个正整数 n, k (1 ≤ n ≤ 10⁵, 1 ≤ k ≤ 10)。
第二行 n 个正整数 a₁, a₂, …, aₙ (1 ≤ aᵢ < 2³¹)。
为了提高区分度,对于得分占比 10% 的测试点,我们保证 1 ≤ n ≤ 100。
输出格式
一行一个非负整数,表示序列中逆序 k 倍对的个数。
输入样例
1 | 5 2 |
输出样例
1 | 4 |
题目分析
首先肯定要从那道经典的逆序对模板题开始分析。它的问题是统计 i < j
且 a[i] > a[j]
的数对。我当时AC的思路是:
- 从左到右遍历数组,对于每个数
a[j]
。 - 利用树状数组,查询在它之前已经出现过的数中,有多少个大于
a[j]
。 - 将这些查询结果累加,就是答案。
- 查询结束后,再将
a[j]
本身的信息更新到树状数组中。
当时的AC代码如下:
点击展开/折叠 逆序对模板题AC代码
1 |
|
这个框架非常清晰。现在,面对k倍逆序对,唯一的不同点,就是判断条件从 a[i] > a[j]
变成了 a[i] > k * a[j]
。
这意味着,我的改造也必须围绕这个变化点展开。当我遍历到 a[j]
时,我需要查询的不再是比 a[j]
大的数,而是比 k * a[j]
大的数。
这个看似微小的变化,却直接影响了算法的基石——离散化。在原模板中,我们只需要对数组 a
中的值进行离散化。但现在,为了能查询 k * a[j]
的相关信息,我们必须将所有可能作为查询边界的 k * a[j]
也一并纳入离散化的范围。
这就是从模板到AC的关键一步:识别出查询目标的变化,并相应地扩大离散化的集合。
算法设计及实现
我的整个解题过程,可以看作是对逆序对模板代码的一次定向升级。
核心思路:模板的演进
继承模板框架:
- 整体的算法流程完全继承自逆序对模板:遍历数组 -> 查询贡献 -> 更新数据 -> 累加结果。
- 核心数据结构依然锁定为树状数组,因为它能高效地完成我们需要的单点更新和前缀和查询。
- 由于
a[i]
的值域依然很大,离散化这一前置步骤也必须保留。
定位改造核心:查询目标的变化
- 模板的查询逻辑是:
贡献 = 总数 - 小于等于a[j]的个数
。 - 本题的查询逻辑变为:
贡献 = 总数 - 小于等于k*a[j]的个数
。 - 这个
k*a[j]
就是本次改造需要处理的新元素。
- 模板的查询逻辑是:
升级离散化:
- 模板:离散化集合仅包含
{ a[1], a[2], ..., a[n] }
。 - 本题:离散化集合必须扩大,同时包含
{ a[1], ..., a[n] }
和{ k*a[1], ..., k*a[n] }
。因为树状数组的下标是基于离散化后的排名,我们既需要能更新a[j]
对应的排名,也需要能查询k*a[j]
对应的排名。将它们全部放入一个集合中进行离散化,才能建立统一的“度量衡”。
- 模板:离散化集合仅包含
微调主逻辑
- 在主循环中,对于当前元素
p
(即a[j]
):- 首先,找到
p
离散化后的排名idx
。 - 然后,找到
k*p
离散化后的排名idx2
。 - 计算贡献值:
ans += cnt - Sum(idx2, c)
。这里的cnt
是已处理元素个数,Sum(idx2, c)
则是利用树状数组查到的、前面出现过且值小于等于k*p
的元素个数。 - 最后,执行和模板完全一样的更新操作:
Update(idx, 1, size, c)
,将p
的出现次数记录到树状数组中。
- 首先,找到
- 在主循环中,对于当前元素
可行性分析
这次“魔改”并没有改变算法的根本结构。
时间复杂度:
- 离散化集合的大小最多变为
2n
。排序的复杂度依然是O(n log n)
。 - 主循环中,树状数组和
lower_bound
的操作复杂度仍为O(log n)
。 - 总时间复杂度保持在
O(n log n)
,完全可以通过。
- 离散化集合的大小最多变为
空间复杂度:
- 辅助数组
b
和树状数组c
的大小变为原来的两倍左右,空间复杂度依然是O(n)
。
- 辅助数组
结论:
通过精准地定位差异并对离散化这一关键步骤进行升级,我们成功地将逆序对模板适配到了新问题上,方案高效可行。
实现
最终的AC代码,清晰地保留了逆序对模板的骨架,但在离散化的部分,能明显看到为了适应新规则而做的扩展。
点击展开/折叠 最终AC代码
1 |
|
代码对比
整个改造过程实际只涉及两个关键环节。
1. 扩展离散化集合
模板代码:仅收集原数组中的值。
1 | // 离散化集合 b 只需包含 a 内的值 |
本题代码:同时收集原数值与其 k
倍值。
1 | // 离散化集合 b 必须同时包含 a 内的值及其 k 倍 |
原因:树状数组需要查询 k*p
的排名,因此必须在一开始就将所有可能的 k*p
值纳入离散化,以建立统一的排名体系。
2. 变更查询边界
模板代码:查询大于 p
的元素个数。
1 | for(int p : a){ |
本题代码:查询大于 k*p
的元素个数。
1 | for(ll p : a){ |
原因:题目的核心条件从 > p
变为 > k*p
,因此计算贡献时,传入 Sum
函数的参数,必须是 k*p
对应的排名,而非 p
的排名。
P.S. 例行在最后放上做题手稿

总结
- 模型识别与套用:将新问题关联到已知的算法模型(本题的逆序对模板),以此为基础快速构建解题框架。
- 分析核心差异:精准定位问题变体的关键不同点。本题的核心差异在于判断条件从
a[i] > a[j]
变更为a[i] > k * a[j]
。 - 模块化调整:根据核心差异,修改算法中受影响的模块。此题中,查询条件的变更,直接决定了离散化的数据集合必须相应地扩展。
- 理解功能而非形式:掌握算法中每个模块的功能目的是解决问题的关键。这使得我们能根据问题变化,对模板进行有效调整,而不是进行无效的生搬硬套。