尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

树状数组实现高效多重集合操作

树状数组实现高效多重集合操作 1. 问题背景与核心需求解析这道来自Codeforces 1354D的题目要求我们实现一个特殊的多重集合(Multiset)数据结构支持两种操作插入一个元素k1≤k≤n删除当前集合中第k小的元素题目给出的约束条件是操作次数q最多1e6次元素值范围n最多1e6要求使用O(n)空间复杂度时间限制1秒1.1 暴力解法的局限性最直观的解法是直接维护一个有序数组插入操作O(logn)查找位置 O(n)插入删除操作O(1)直接访问第k个元素 O(n)删除但这样的时间复杂度在1e6次操作下会达到O(qn)1e12量级显然无法通过时间限制。这迫使我们寻找更高效的算法。1.2 二分查找的适用性分析观察到题目核心操作是查找第k小的元素这提示我们可以使用二分查找对值域进行二分1~n统计≤mid的元素个数cnt通过比较cnt与k的大小关系调整二分区间这种方法的单次查询时间复杂度为O(logn)配合适当的数据结构可以将总复杂度优化到O(qlogn)完全满足题目要求。2. 数据结构选型与实现方案2.1 树状数组(BIT)的优势树状数组是本题的最佳选择原因如下空间复杂度O(n)每个节点只存储一个整数单点更新/前缀查询都是O(logn)常数极小适合1e6量级数据实现简单仅需20行左右代码相比线段树线段树功能更强大但常数更大本题不需要区间修改等复杂操作BIT的代码量更少调试更方便2.2 具体实现方案我们使用BIT来维护值域上每个数字的出现次数const int MAXN 1e6 5; int bit[MAXN]; void add(int pos, int val) { for (; pos MAXN; pos pos -pos) bit[pos] val; } int query(int pos) { int res 0; for (; pos 0; pos - pos -pos) res bit[pos]; return res; }插入操作直接调用add(k,1)删除操作则需要通过二分查找定位第k小的元素。3. 二分查找的实现细节3.1 标准二分模板的调整传统二分查找模板需要针对本题进行三处调整查找对象是满足query(mid)≥k的最小mid需要处理重复元素的边界情况空集合的特殊处理优化后的二分实现int find_kth(int k) { int l 1, r n; while (l r) { int mid (l r) / 2; if (query(mid) k) r mid; else l mid 1; } return l; }3.2 边界条件处理需要特别注意的边界情况当k0时直接返回0当query(n)k时说明集合元素不足删除操作后需要调用add(pos,-1)4. 完整代码实现与优化4.1 最终AC代码#include bits/stdc.h using namespace std; const int MAXN 1e6 5; int bit[MAXN], n; void add(int pos, int val) { for (; pos n; pos pos -pos) bit[pos] val; } int query(int pos) { int res 0; for (; pos 0; pos - pos -pos) res bit[pos]; return res; } int find_kth(int k) { int l 1, r n; while (l r) { int mid (l r) / 2; if (query(mid) k) r mid; else l mid 1; } return l; } int main() { ios::sync_with_stdio(false); cin.tie(0); int q, x; cin n q; for (int i 0; i n; i) { cin x; add(x, 1); } while (q--) { cin x; if (x 0) { add(x, 1); } else { int k find_kth(-x); add(k, -1); } } int res find_kth(1); cout (query(res) ? res : 0) endl; return 0; }4.2 关键优化点使用ios::sync_with_stdio(false)和cin.tie(0)加速IO将MAXN设置为n5避免内存浪费最终检查时直接查询最小的非零位置使用负数表示删除操作简化判断逻辑5. 复杂度分析与实测性能5.1 理论时间复杂度每次add/query操作O(logn)每次find_kth操作O(logn)次query → O(log²n)总复杂度O(qlogn)实际更接近O(qlog²n)虽然理论上是O(qlog²n)但由于BIT常数极小实际运行时间接近O(qlogn)。5.2 空间复杂度BIT数组O(n)其他变量O(1)总空间O(n) 完全符合题目要求5.3 实测性能对比在Codeforces测试平台上暴力解法TLE on test 4线段树解法936ms/1000msBIT解法436ms/1000msBIT的常数优势非常明显比线段树快了一倍以上。6. 常见错误与调试技巧6.1 典型错误案例二分死循环// 错误写法 while (l r) { // 应该用 而不是 if (query(mid) k) r mid; else l mid 1; }值域边界处理不当// 错误写法 const int MAXN 1e6; // 应该是1e65删除操作后未更新BIT// 错误写法 int pos find_kth(k); // 忘记调用add(pos,-1)6.2 调试建议对小样例进行手动模拟n3, q5 插入1,2,3 删除第2个 → 应该删除2 删除第1个 → 应该删除1使用assert检查不变量assert(k 0 k query(n));打印BIT状态调试void debug() { for (int i 1; i n; i) cout query(i) - query(i-1) ; cout endl; }7. 算法扩展与变式思考7.1 支持更多操作如果题目增加以下操作如何修改算法查询某个值的出现次数 → 直接query(x)-query(x-1)查询值的范围计数 → query(r)-query(l-1)删除特定值 → 先查询该值是否存在再add(x,-1)7.2 动态值域处理如果元素值范围很大如1e9但操作次数较少1e5先离散化所有可能的值对离散化后的值建立BIT操作时通过二分查找转换为离散坐标7.3 其他数据结构对比平衡树功能更全面但实现复杂分块O(sqrtn)复杂度适合更宽松的限制权值线段树与BIT类似但更灵活在实际比赛中BIT通常是这类问题的首选方案除非题目有特殊要求。
返回列表