二分三分算法详解, 模板与临界条件分析
文章目录
- 0x00 序
- 0x01 整数域上的二分
- 0x0101 查找可行解的最左端点
- 0x0102 查找可行解的最右端点
- 0x02 实数域上的二分
- 0x03 三分
- 0x0310 实数域上三分
- 0x0302 整数域上三分
- 0x0303 三分套三分
0x00 序
二分, 就是在有序的序列中不断对半分,从而实现 l o g log log 级别的查询速度。
二分从简单的应用角度来说, 分为如下:
- 整数域二分
- 查找可行解区间的最左端点
- 查找可行解区间的最右端点
- 实数域二分
同样的, 三分就是每次选取三分之一段, 复杂度为 l o g 3 log_3 log3 级别. 主要用于寻找单峰函数极值. 分为凸函数和凹函数两类, 实数域和整数域上两类…
学习是开放的, 推荐一个我常常跟随学习的大佬的 博客, 可以看看. 对三分法讲的很细.
0x01 整数域上的二分
0x0101 查找可行解的最左端点
在一个有序序列中, 如 [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0,1,2,3,4,5,6,7,8,9] [0,1,2,3,4,5,6,7,8,9] 找到所有 5 ≤ x 5 \leq x 5≤x 的数中, 最左边的一个, 即找到例子中的 a [ 5 ] a[5] a[5].
我们定义两个变量 l = 0
和 r = 9
, 代表初始的这个数组范围, 在二分的过程中会不断缩小. 宗旨就是: 让
[
l
,
r
]
[l,r]
[l,r] 代表的区间尽可能的是我们想要的区间.
每次对半查询中, 定义一个 mid=l+r>>2
, 然后查看 a[mid]
与目标值
5
5
5 的关系。有以下几种情况:
a[mid]>=5
,check
返回true
:这时 m i d mid mid 是可取的, 本着让 [ l , r ] [l,r] [l,r] 代表的区间尽可能的是我们想要的区间的原则, 让 r = m i d r=mid r=mid, 保证 r r r 所在的位置一定是满足 5 ≤ a [ r ] 5 \leq a[r] 5≤a[r] 的.a[mid]<5
,check
返回false
:这时 m i d mid mid 是可取的, 让 l = m i d + 1 l=mid+1 l=mid+1, 因为此时 m i d mid mid 所在的位置一定是不满足 5 ≤ a [ r ] 5 \leq a[r] 5≤a[r] 的, 就要让 l l l 再往左去一个, 让它尽可能满足.
代码如下:
int bsearch_1(int l, int r)//求满足要求的最小值,求左端点
{
while (l < r)
{
int mid = l + (r - l) / 2; //int mid = l + r >> 1;有可能爆ll
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
0x0102 查找可行解的最右端点
在一个有序序列中, 如 [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0,1,2,3,4,5,6,7,8,9] [0,1,2,3,4,5,6,7,8,9] 找到所有 x < 5 x < 5 x<5 的数中, 最左边的一个, 即找到例子中的 a [ 4 ] a[4] a[4].
这里有点区别, 每次对半查询中, 定义 mid = (l + r + 1) >> 2
.有以下几种情况:
a[mid]>=5
,check
返回false
:这时 m i d mid mid 是不可取的, 让 r r r再往左去一个, 即 r = m i d − 1 r=mid-1 r=mid−1, 保证 r r r 所在的位置尽可能是满足 5 ≤ a [ r ] 5 \leq a[r] 5≤a[r] 的.a[mid]<5
,check
返回true
:这时 m i d mid mid 是可取的, 让 l = m i d l=mid l=mid.
具体为什么要在 定义 m i d mid mid 时加一呢?
讨论一个临界条件:
当
l
=
4
,
r
=
5
l=4, r=5
l=4,r=5, 如果不加一,
m
i
d
=
4
mid=4
mid=4, check
返回 true
-> l = mid = 4
, 发现会进入死循环. 让
m
i
d
mid
mid 往上偏一位, 就是为了避免这种死循环. 同时让两种情况的推出条件统一成了 while(l < r)
, 退出时
l
l
l 就是要找的答案.
代码如下:
int bsearch_2(int l, int r)//求满足要求的最大值,求右端点
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
0x02 实数域上的二分
实数域上的二分, 相对就比较简单, 如果选取上面的模板, 只需要改退出条件为 while(r - l < eps)
,
e
p
s
eps
eps 为定义的最小值, 一般为
1
e
−
7
1e-7
1e−7, 视题目误差要求而定. 对
m
i
d
mid
mid 的选择和
l
,
r
l,r
l,r 的赋值就更为宽松, 因为没有了临界条件的考虑.
const double eps =1e-7; //精度。
while(r - l > eps){
double mid = l + (r - l) / 2;
if (check(mid)) r = mid;
else l = mid;
}
还有一种更为通用的写法, 因为这种循环不超过几百次. 完全可以写一个循环, 直接跑上一千次. 完全在大多数的题目时间限制里.
int cnt = 1000;
while(cnt--){
double mid = l + (r - l) / 2;
if (check(mid)) r = mid;
else l = mid;
}
0x03 三分
0x0310 实数域上三分
以凸函数为例子, 凹函数改一下 check 就行.
对于一个单峰函数 (例如: y = − x 2 y=-x^2 y=−x2), 如果想要找到它的顶点 (当然, 已知在 x = 0 x=0 x=0), 可以先定义 l = − i n f , r = i n f l=-inf, r=inf l=−inf,r=inf, i n f inf inf 只要确保包含了答案就行.
每次取两个三等分点 m i d l midl midl 和 m i d r midr midr, 当函数值 f ( m i d l ) < f ( m i d r ) f(midl) < f(midr) f(midl)<f(midr) 的时候, 就可以判断极值在 [ l , m i d r ] [l,midr] [l,midr] 这个区间内, 画个图就能看出来了. 更具体的, 大家可以手玩一下 !
当然, 既然在实数域上了, 当然也是可以直接暴力循环一千次的. 代码就不贴了, 一定不是我懒着写.
想一想, 这份代码好像能通用地处理当区间 [ l , r ] [l,r] [l,r]为单调的, 即极值在两端. 主要是因为你不用真的找到两端, 只要找到两端加减 e p s eps eps 就行. 接下来的整数域就不行, 因为 e p s eps eps 的概念在整数域不存在, 你就要找到准确的那个数.
//以凸函数为例子, 凹函数改一下 check 中的 小于号 -> 大于号
while (r - l > eps) {
ld midl=(l*2+r)/3;
ld midr=(l+r*2)/3;//三等分法
if (f(midl) < f(midr))
l = midl;
else
r = midr;
}
// 这里 check 反过来写了, 注意一下, 这个写法反正我不用, 就贴一下代码, 注意问题注释里写了
while (r - l > eps) {
ld mid = l + (r - l) / 2;
ld midl = mid - eps;
ld midr = mid; //近似分割法, 会被卡精度
if (check(midl) > check(midr))
r = mid;//不能 r=midr, 当 r-l=2*eps, 会有 l=midl,r=midr 出现
else
l = mid;
}
0x0302 整数域上三分
直接贴代码, 思想是一样的, 主要是特殊处理两端. 注意 while
里面条件的变化.
int bin3(int l, int r)
{
if (l > r) return -1;
int res = max(f(l), f(r));
while (l <= r) {
int m = (l + r) >> 1, mm = (m + r) >> 1;
int fm = f(m), fmm = f(mm);
if (fm <= fmm) l = m + 1;
else r = mm - 1;
res = max(res, max(fm, fmm));
}
return res;
}
0x0303 三分套三分
主要处理这种问题(如图), 找它的极值点.
对于两个变量的凹 / 凸函数(一个圆锥形), 先固定
x
x
x 三分
y
y
y , 再三分
x
x
x. 因为对于其的任意切片, 一定是一个凹 / 凸函数, 且极值点和三维状态一样.
double run(double x) // 固定x,三分y
{
....
while (r - l > eps)
{
mid = (l + r) / 2;
if (f(x, mid - eps) > f(x, mid + eps))
l = mid;
else
r = mid;
}
return f(x, mid);
}
int main()
{
...
while (r - l > eps)
{
mid = (l + r) / 2; // 三分x
if (run(mid - eps) > run(mid + eps))
l = mid;
else
r = mid;
}
printf("%.6f\n", run(mid));
}