区间并查集优化频繁删除的二分查找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{}
}

浙公网安备 33010602011771号