对于二分答案来说, 他将整个答案划分为左右两个区间, 会有以下几个问题
-
问题一:答案
mid
落在哪个区间? 对此问题可以有两种区间的划分方式:
- ①
mid
落在左边,那么区间划分为[l, mid]
,[mid + 1, r]
- ②
mid
落在右边,那么区间划分为[l, mid - 1]
,[mid, r]
- ①
-
问题二:到底选择哪种划分方式?
严格来见,
check
函数其实有三种结果,- ①
res == mid
,说明当前mid
是答案 - ②
res < mid
,说明res
小了,答案在右边的区间 - ③
res > mid
,说明res
大了,答案在左边的区间
举个例子,假如一道题限制是不超过
100
,若当前mid = 98
,而且我们不知道区间中到底有没有99
和100
,那么虽然98 < 100
属于第二种情况,但它可能是最终答案(并且我们仍然要继续循环,寻找一个更优解);若当前
mid = 101
,其显然超过了限制,它一定不是答案,所以答案在其左边;在这个例子中,显然可以将res == mid
与res < mid
合并起来,那么check
函数的返回结果可以是res <= mid
也可以是res > mid
,就是将这两个结果分配给两个区间,分配的方式就是看这个=
号,等于号在哪边,哪边区间就包含mid
- ①
-
问题三:为什么会无限循环
见例题代码
例题: C - Transportation Expenses (atcoder.jp)
例题代码:
void solve()
{
int n;
long long m;
cin >> n >> m;
vector<long long> a(n);
for(int i = 0; i < n; i++) {
cin >> a[i];
}
auto check = [&](long long mid) -> bool {
long long s = 0;
for(int i = 0; i < n; i++) {
s += min(a[i], mid);
}
// 根据题意, 当 s == m 时, 说明当前补贴 mid 有可能是最终答案,
// 当 s < m 时, 说明 mid 小了, 答案在右边, 并且当前mid可能是最终答案
// 当 s > m 时, 说明 mid 大了, 答案在左边, 并且当前mid不可能是最终答案, 因为超过了限制
// 所以必须要将 s == mid 这种情况划给右边, 使区间划分为 [l, mid - 1] 与 [mid, r]
// 此时, 返回条件可以是 s >= m, 也可以是 s < m
return s >= m;
};
long long l = 0, r = 1e14;
while(l < r) {
long long mid = l + (r - l + 1 >> 1);
if(check(mid)) {
l = mid; // 选择了 s >= m, 那么check为true时, 答案应该在右边这个区间
// 但在计算mid时, 若没有 +1, 代表向下取整, 那么计算出来的mid就会落在左区间
// 此时再将mid赋值给l就会造成无限循环(因为l本身就在左区间, 你将mid赋值给它, 还在左区间)
}
else {
r = mid - 1;
}
}
if(r == 1e14) {
cout << "infinite\n";
}
else {
cout << l << "\n";
}
}