跳转至

二分

泛化ψ(`∇´)ψ

作用是在单调的序列或者单调的函数当中进行查找。

也可以将最优化问题转化为可行性问题之后进行判定求解。

应用ψ(`∇´)ψ

整数域二分ψ(`∇´)ψ

问题:在单调递增的序列当中查找大于等于 \(x\) 的最小的一个数

开始令两个指针 \(l = 1, r = n\),表示我们当前能确定的答案所在的区间。

我们称 \([1, l)\) 为不可行区间,\([r, n]\) 为可行区间(闭开的原因之后会说),可行区间指的是满足问题约束的区间。

每次取 \(mid = \lfloor (l + r) / 2 \rfloor\)

如果 \(a_{mid} \ge x\),那么证明 \([mid, n]\) 就一定是可行区间,因为序列单调递增,证明 \(mid\) 之后的 \(a\) 都必然 \(\ge x\)

所以,我们可以缩小答案区间,令 \(r = mid\)

反之,证明 \([1, mid]\) 都是不可行的,所以答案区间的左端点肯定不是 \(mid\),所以令 \(l = mid + 1\)

\(l = r\),即答案区间缩小为一个点的时候,此时的 \(a_l\) 就是答案。

如果需要判断无解,开始时令 \(r = n + 1\) ,如果最后 \(l = r = n + 1\) ,那么无解。

这种写法下 \(mid\) 永远不会取到 \(r\),因为如果 \(mid\) 要取到 \(r\),就必须让 \(l\) 不断收缩过来,而当 \(l = r\) 时,循环已经结束了,所以 \(mid\) 不可能取到 \(r\)

原本的二分区间是 \([1, n]\),现在将其扩大一格变为 \([1, n + 1]\),那么这个越界的下标本身的意义就是“无解”,类似 stl 容器的 end()

1
2
3
4
5
6
7
int l = 1, r = n; // 判无解则令 r = n + 1,这种写法 mid 永远不会取到 r
while(l < r) {
    int mid = (l + r) >> 1;
    if(a[mid] >= x) r = mid; // mid 也可能是答案,也要取。
    else l = mid + 1;
} 
ans = a[l];

如果问小于等于,也就是可行区间在 \(mid\) 左侧,反过来即可:

只不过为了让越界值为 \(0\),需要让 \(mid = \lfloor (l + r + 1) / 2 \rfloor\)

理解方式同上。

1
2
3
4
5
6
7
int l = 1, r = n; // 判无解则令 l = 0,这种写法 mid 永远不会取到 l
while(l < r) {
    int mid =(l + r + 1) >> 1;
    if(a[mid] <= x) l = mid; // mid 也可能是答案,也要取。
    else r = mid - 1;
} 
ans = a[l];

一张图理解,下图绿色为可行区间:

bs-1.png

upd on 01.23.23:昨天在 uoj 群里回答了一位小朋友的问题。

也算是有些新的想法吧,就是其实二分的写法有很多种,区别一般在于对答案区间的收缩上。

有的写法是不管 mid 是否 valid 都要取 mid,有的写法是左闭右开,有的写法是左开右闭。

然后循环停止的限制也不一样,所以导致记录答案的方式不一样,这个要自己去区分,不能瞎写(

虽然我用的写法区分了两种方式,但是相对来说不那么容易理解出错?

怪不得 Programming Pearl 说只有 \(20\%\) 的程序员能写对二分((因为很多程序员都是缺乏思考的吧!

实数域二分ψ(`∇´)ψ

和整数域比较类似,不过还需要设置 \(eps\) 来避免精度误差。

因为是实数域上的二分了,用之前那种 +-1 的方式精度是完全不够的(

所以就不管是否合法都收缩到 mid,所以要记录答案而不是可以直接用下标输出。

1
2
3
4
5
6
double l = 0.0, r = (double)(1e9 + 7), ans;
while(l + eps < r){
    double mid = (l + r) / 2;
    if(valid(mid)) r = mid, ans = mid;
    else l = mid;
}

也可以计算之后设置次数二分。

二分答案ψ(`∇´)ψ

如果一个最优解问题的最优解是 \(x\) ,并且 \(x\) 的一边不合法,一边合法,则这个问题具有单调性。

可以在每次二分的时候将问题转化为是否存在一个方案,使得解为 \(mid\)

二分答案 \(x\) 并进行判定。

然后根据可行区间的方式选择二分方式即可。

此类问题的显著特点就是“最小值最大”,“最大值最小”。

不过也有例外,比如 AGC006D。

STL 二分ψ(`∇´)ψ

  • lower_bound :在有序序列查找大于等于 \(x\) 的第一个。
  • upper_bound :大于 \(x\)

如果类似 set ,自己带有 lower_bound

就一定选择容器自身的:s.lower_bound(x) 而不是 lower_bound(s.begin(),s.end(),x)

这样效率更高,因为 \(s\) 本身不是类似数组那样的随机访问(他没有支持随机访问的迭代器),他是一个树形结构,所以要用 STL 在树上给你实现的 lower_bound


最后更新: May 9, 2023