二分的本质思想:
二分的本质思想就是将一个整体拆成两半,选取合适的一半之后再拆,再选,一直循环下去拿到最终想要的结果。
遇到 具有单调性质 的问题,我们优先考虑二分。
我们拿下面一道 LeetCode
第414场周赛 的第二题作为例子详细分析
LeetCode
3281 范围内整数的最大得分
给你一个整数数组start
和一个整数d
,代表n个区间[start[i], start[i] + d]
。
你需要选择n个整数,其中第i个整数必须属于第i个区间。所选整数的得分定义为所选整数两两之间的最小绝对差。
返回所选整数的最大可能得分。
示例 1:
输入: start = [6,0,3], d = 2
输出: 4
解释:
可以选择整数 8, 0 和 4 获得最大可能得分,得分为 min(|8 - 0|, |8 - 4|, |0 - 4|)
,等于 4。
示例 2:
输入: start = [2,6,13,13], d = 5
输出: 5
解释:
可以选择整数 2, 7, 13 和 18 获得最大可能得分,得分为 min(|2 - 7|, |2 - 13|, |2 - 18|, |7 - 13|, |7 - 18|, |13 - 18|)
,等于 5。
提示:
2 <= start.length <= 105
0 <= start[i] <= 109
0 <= d <= 109
问题分析:
题目要求我们从这n个区间中选择n个数,再求这n个数两两之间最小绝对差的最大值。为方便观察,我们对这n个数进行排序,其实也就是对这n个区间按照区间左端点进行排序,也就是对start[]
里的元素从小到大进行排序。
我们记排完序之后的start[]
数组为
s
t
a
r
t
[
a
0
,
a
1
,
.
.
.
,
a
n
−
1
]
start[a_0,a_1,...,a_{n-1}]
start[a0,a1,...,an−1]
记区间右端点
b
i
=
a
i
+
d
b_i = a_i + d
bi=ai+d,那么这n个区间为
[
a
0
,
b
0
]
,
.
.
.
.
,
[
a
n
−
1
,
b
n
−
1
]
[a_0,b_0],....,[a_{n-1},b_{n-1}]
[a0,b0],....,[an−1,bn−1]
记从
[
a
i
,
b
i
]
[a_i,b_i]
[ai,bi]取出的数为
r
i
r_i
ri,最小绝对差为
s
c
o
r
e
score
score,我们知道
r
i
+
1
>
=
r
i
+
s
c
o
r
e
r_{i+1} >= r_i + score
ri+1>=ri+score
先枚举几个合适的
s
c
o
r
e
score
score值看看情况,我们发现,如果
s
c
o
r
e
=
0
score = 0
score=0,那么这n个数是很容易选出来的,如果
s
c
o
r
e
=
1
score = 1
score=1,也是比较容易的,但当
s
c
o
r
e
=
2
score = 2
score=2的时候,我们就要考虑一下存不存在两个甚至两个以上的[k,k+1]区间,比如:
第三个区间是 [ 3 , 4 ] [3,4] [3,4],第四个区间也是 [ 3 , 4 ] [3,4] [3,4],那么 r 3 r_3 r3和 r 4 r_4 r4的绝对值之差肯定比2小,此时这个 s c o r e score score就取不到2。
当 s c o r e = 3 , 4 , . . . score = 3,4,... score=3,4,...时,难度会越来越大。
s c o r e score score作为自变量,能否选出这n个数作为因变量,我们可以做出如下图像:
可以看出,因变量关于 s c o r e score score是具有单调性的,我们要的是值为 T r u e True True对应的 s c o r e score score的最大值。这可以利用二分来做,如果整数 m m m可以充当 s c o r e score score,那么我们就考虑 s c o r e score scored的右半部分,如果 m m m不可以充当 s c o r e score score,那么我们就考虑左半部分。
现在我们回到如何检测整数 m m m是否可以充当 s c o r e score score的问题上,假设 m m m可以充当 s c o r e score score,那么问题就转化成:
给定 m m m,能否从每个区间中各取一个数,使得任意两数之差的最小值至少为 m m m。
贪心地想,第一个数越小,第二个数就越能在区间里,所以 r 0 r_0 r0应当为 a 0 a_0 a0,如果 r 1 r_1 r1的最小取值,即 r 0 + m r_0 + m r0+m,超过了其区间右端点 b 1 b_1 b1,说明这个 m m m大了,我们需要减小 m m m得到右边界right。
又因为 r 1 r_1 r1是在区间 [ a 1 , a 1 + d ] [a_1,a_1+d] [a1,a1+d]里的,所以 r 1 > = a 1 r_1 >= a_1 r1>=a1。
现在我们得到关于
r
1
r_1
r1的两个范围:
r
1
>
=
a
1
r
0
+
m
<
=
r
1
<
=
a
1
+
d
r_1 >= a_1 \\ r_0+m <=r_1<= a_1 + d
r1>=a1r0+m<=r1<=a1+d
按照刚刚的贪心思路,我们假设
r
0
,
r
1
,
.
.
.
,
r
i
r_0,r_1,...,r_i
r0,r1,...,ri这些数的任意两数之差至少为m,现在我要找符合条件的
r
i
+
1
r_{i+1}
ri+1,那就要使得
r
i
r_i
ri尽可能的小嘛,这样我们才更容易找到
r
i
+
1
r_{i+1}
ri+1。
所以
m
m
m要想成为合格的
s
c
o
r
e
score
score,就要满足以下条件:
r
0
=
a
0
r
i
=
m
a
x
(
r
i
−
1
+
d
,
a
i
)
对任意的
i
=
1
,
2
,
.
.
.
,
n
−
1
r
i
<
=
a
i
+
d
r_0 = a_0 \\ r_i = max(r_{i-1}+d,a_i) ~~~~ 对任意的i = 1,2,...,n-1\\ r_i <= a_i + d
r0=a0ri=max(ri−1+d,ai) 对任意的i=1,2,...,n−1ri<=ai+d
解决代码:
到现在,我们给出检测 m m m是否能成为 s c o r e score score的代码:
int check(int m, vector<int> new_Start, int N, int d) {
long cur = new_Start[0]; // r0 = new_Start[0]
for (int i = 1; i < N; i++) {
int prex = cur;
cur = max((long)new_Start[i], cur + m);
if (cur > new_Start[i] + d)
return 2; // m大了,减小右边界
}
// 如果所有区间上,m都满足条件,那我们就增大左边界,挑选尽可能大的m
return 1; // 增大左边界
}
之后是主框架二分找最大 s c o r e score score代码:
int maxPossibleScore(vector<int>& start, int d) {
int N = start.size();
sort(start.begin(), start.end());
// 主框架是在二分
int left = 0;
int right = (start[N - 1] + d - start[0]) / (N - 1) + 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (check(mid, start, N, d) == 2)
right = mid;
else
left = mid;
}
return left;
}