引入
有一些问题要求我们回答若干询问。这些询问通常都是互不相干的。有时候二分能够以 \(O(nlog(n))\) 的复杂度单独解决一个询问,但是对于所有的询问都采用二分的话就是 \(O(qnlog(n))\) 的复杂度会超时(可以看 ABC394 G 体会一下)。于是就有了并行二分这一解决方法。
并行二分
我们观察到在对所有的询问都进行二分时,重复 check 了非常多次。假设每个询问所设定的二分区间是 \([L, R]\),那么每个询问我们都对 \(L + R \over 2\) 这个点 check 了。实际上我们只需要 check 这个中点一次。并行二分所做的就是简化掉这些重复的 check。具体的做法就是对所有的询问都初始化二分区间为 \([L, R]\),然后在接下来的二分中先根据每个询问的 \(mid\) 值将询问归类, \(mid\) 值相同的询问在一起。然后对于所有可能的 \(mid\) 值依据题意从小到大或者从大到小 check,看是否满足一开始归为这类的询问。
比如 ABC394 G 并形二分的过程就是:
// code ...std::vector A(q, 0), B = A, Y = A, C = A, D = A, Z = A, L(q, 1), R(q, M);std::vector mid(M + 1, std::vector<int>{});while (true) {// 将上次归的类清空// M 是二分区间的右区间for (int i = 1; i <= M; i++) {mid[i].clear();}bool ok = true; // 是否所有询问都找到了答案for (int i = 0; i < q; i++) {if (L[i] <= R[i]) { // 这里的条件就是二分结束的条件,个人喜欢带等号ok = false;// 归类mid[L[i] + R[i] >> 1].push_back(i);}}if (ok) { // 都找到答案了就 break 掉break;}for (int i = 0; i < h * w; i++) {fa[i] = i;}// 从大到小遍历for (int i = M, j = 0; i >= 1; i--) {if (mid[i].empty()) {continue;}// 加入符合条件的边while (j < edge.size() && edge[j][2] >= i) {merge(edge[j][0], edge[j][1]);j++;}for (auto &id : mid[i]) {// 如果在一个连通块里if (get(A[id] * w + B[id]) == get(C[id] * w + D[id])) {L[id] = i + 1;}// 否则else {R[id] = i - 1;}}}}// code ...
觉得没看懂也可以看看这篇:Parallel Binary Search [tutorial]