在你真的理解二分的写法吗 - 二分写法详解这篇博客中,有几个朋友给二分求中点mid = (l + r) / 2
提出了一些疑问和改进。自己最近也对这个问题有过一些思考,因此在这里系统详细地聊聊自己的看法,看看程序中求区间终点到底应该怎么写才是完美的。
先公布程序中求区间中点的完美写法:mid = l + (r - l) / 2
。
在数学上,mid = l + (r - l) / 2
和mid = (l + r) / 2
是等价的,这个自己通分一下就行了。但是在程序中,往往需要求一个数组区间的中点,这也就意味着区间必须是一个整数,如果中点是x.5
这种情况,你是向下取整还是向上取整。这在编程中是一个很大的坑。
mid = (l + r) / 2
的劣势
首先看看mid = (l + r) / 2
这种求中点有什么劣势。
- 溢出
- 求上下界会不统一
溢出
溢出比较好理解,l + r
可能会溢出int
的最大范围,而l + (r - l) / 2
不会,这里用减法替代了加法,这种思想很多地方都有用到,比如求最小一个数,这个数的平方大于或等于给定的一个值n
,直观代码的写法如下:
int x;
for (x = 0; x * x < n; ++x) {}
循环跳出,x
就是答案,但是如果n = (1 << 31) - 1
呢?这个循环永远不会结束,可以试试看,原因就是x * x
会大过int
的最大值,从而导致溢出出现负数。正确的写法如下:
int x;
for (x = 0; x < n * 1.0 / x; ++x) {}
这就是利用除法代替乘法,规避了溢出的分险,从数学的角度来看,这两者等价,但是在编程的领域中差别很大。下面分享一个题,你就更能体会这种思想了。
这是2018湘潭邀请赛的F题,题目很好理解,给三元组排序,排序规则就是题中的不等式。这题一看不就很好写嘛,sort
一下,cmp
函数自己写一下,但是先看看数据范围,
。对于这种分式不等式,最好的做法就是把分式转化成乘法,也就是
,把数据范围代入进去,发现
,而<stdint.h>
中unsigned long long
的最大值:#define UINT64_MAX 0xffffffffffffffffULL /* 18446744073709551615ULL */
,显然直接做数据范围承受不了。
这个题正确的做法应该是把 转化成 ,这样子 的最大值为 ,这是可以承受的。
通过代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef tuple<uLL, uLL, uLL, int> Node;
int main()
{
for (size_t n; EOF != scanf("%u", &n); ) {
vector<Node> arr;
uLL x, y, z;
for (size_t i = 0; i < n; ++i)
scanf("%lld%lld%lld", &x, &y, &z),
arr.emplace_back(x, y, z, i);
sort(arr.begin(), arr.end(), [] (const Node &A, const Node &B)
{
uLL AA = get<2>(A) * (get<0>(B) + get<1>(B) + get<2>(B));
uLL BB = get<2>(B) * (get<0>(A) + get<1>(A) + get<2>(A));
return AA == BB ? get<3>(A) < get<3>(B) : AA > BB;
});
for (size_t i = 0; i < n; i++)
printf("%u%c", get<3>(arr[i]) + 1, i == n - 1 ? '\n' : ' ');
}
return 0;
}
避免数据溢出的技巧还有很多,这里就先介绍到这里。
求上下界不统一
这个很有趣,一般不怎么会注意求上下界不统一这个坑,可以看下这篇博客右移一位和除二的区别。举个例子,区间[2, 5]
的中点求下界是mid = (2 + 5) / 2 = 3
,这没问题;区间[-5, 2]
的中点求下界是mid = (-5 + 2) / 2 = -1
,这里就出现问题了,[-5, 2]
求下界是-2
。而(l + r) / 2
求出来是-1
,也就是说(l + r) / 2
想要正确求出区间中点的上下界就要针对(l + r)
的正负做不同的处理。而用mid = l + (r - l) / 2
就不会出现上述问题。
mid = l + (r - l) / 2
的优势
mid = (l + r) / 2
的劣势其实就是mid = l + (r - l) / 2
的优势,这里总结一下:
- 不会溢出
- 上下界求法统一
下界:mid = l + (r - l) / 2
上界:mid = l + (r - l + 1) / 2
为什么很多人还是会写mid = (l + r) / 2
这是个问题,因为我也这样写,我总结了三点原因,如下。
- 代码短,简单
- 溢出这个问题一般不会发生,因为如果左右端点的范围到了
,我都会选用
long long
,因此就不会发生溢出 - 由于二分是求一个数组或向量的区间中点,那么
l + r
肯定不会是负数,那么也不会出现上下界不统一的问题
这也是为什么mid = (l + r) / 2
能活这么久,且很多人没有发现mid = l + (r - l) / 2
好处的原因。
总结
一个很小的细节里面的思想很精髓,我们应该多去思考“为什么是这样,为什么不是那样,那样和这样的区别是什么,优劣势是什么”。
C++标准库中STL的lower_bound
和upper_bound
也是时候解开它们的神秘面纱了,近期会写,只能说标准库中到处都是精髓,且标准库中的上下界二分比你真的理解二分的写法吗 - 二分写法详解这篇博客中的二分更好理解。