加载中...

区间并查集优化频繁删除的二分查找trick

有时候,对于一个有序列表,我们要进行多次二分,且每一次二分后需要删除刚刚查到的元素,并对新的列表继续二分。显然这个过程需要 \(O(n^{2})\) 实现,因为每次二分后不得不暴力将需要删除的元素从原列表中移除。那么有没有什么办法,优化删除操作,让整个过程变快呢?

我们很容易想到直接对原序列中的已删除位置打标记,在二分时额外判断标记带来的影响即可。可是问题在于每次二分到一个已被删除的位置时,如何快速找到其附近新的未被删除的位置,如果能快速找到,那么我们就可以把整个过程看作是对所有未删除元素的二分。

一个简单高效的办法:对所有已删除元素建立区间并查集,使得每次二分到某个已删除位置时,直接 find 到其右侧最近的未删除位置。于是,在二分内套一个区间并查集查找即可。

实现注意事项(用开区间二分实现,即有效区间为 \((lo, hi)\)):

  • \(mid = \lfloor\frac{lo+hi}{2}\rfloor\)\(real\_mid\)\(mid\) 右侧第一个未被删除的位置
  • mid 二分到某个已删除位置时,\([mid, hi)\) 内的所有元素可能均已被删除(即 real_mid 不存在)。此时直接用 mid 更新 hi;
  • 其他情况均用 real_mid 更新 lo 与 hi,并且需要注意用一个额外变量 res 存储最终结果,其他形式均保持不变

模板:

int n = 6;
DSU dsu(n + 1);
vector<int> vec = {1, 2, 4, 6, 7, 9}; // 有序列表,大小为 n,0-index
vector<bool> deleted(n); // 删除标记数组
vector<int> target = {6, 1, 4, 9, 2, 7}; // 依次查找 target[i],每次查到后删除该元素

for(int i = 0; i < n; i ++){
    int lo = -1, hi = n; // 开区间形式二分
    int res = -1; // 最终在列表内的目标位置
    while(lo + 1 < hi){
        int mid = lo + hi >> 1;
        int real_mid = dsu.find(mid); // 找 mid 右侧第一个未被删除的位置
        if(real_mid >= hi){ // [mid, hi) 内的所有元素均被删除
            hi = mid;
            continue;
        }
        assert(!deleted[real_mid]);
        if(vec[real_mid] == target[i]){
            res = real_mid;
            break;
        }
        else if(vec[real_mid] < target[i]){ 
            lo = real_mid;
        }
        else{
            hi = real_mid;
        }
    }
    if(res != -1){ // 目标位置存在,删除该位置并修改并查集,以便于下一次二分
        cout << res << "\n";
        deleted[res] = true;
        dsu.merge(res + 1, res);
    }
    else{}
}
posted @ 2026-06-29 09:38  小橘奏  阅读(10)  评论(0)    收藏  举报