二分查找套路
好久没水文了,今天来水一篇。。。
最近练成了二分查找的套路,来吹一波
套路总纲
- 知道此题适不适合二分
1)目标在一个连续有界区间内(即使是0~MAX,这个基本都能达到);
2)单调问题:问题会告诉你需要满足某个条件,比如最多多少天完成(设为d),而让你求最少的目标(设为x),那么d的取值f(x)必须是一个单调函数,才能使用二分去除一半的解空间; - 找到此单调函数f(x), 我一般使用count(nums, m)来表示这个函数;
- 二分模板:二分支二分,一定不满足条件的一边缩小(比如f(x)>d);满足的可以设为新的临界值(lo/hi=mid);
- 退出循环后 验证lo的合法性
道理很绕,变成题目就好理解了
题目
lc69 sqrt()
求解sqrt(n),要求下取整。
按照套路求解:
- 排除0和1的话,可以确定解在(1, n/2)之间;f(x)=x^2, 为单调递增函数(x>1)
- f(x)=x^2
剩下的看代码
class Solution {
public int mySqrt(int x) {
/**
0. 【0,x】的连续整数
1. 单调性:f(x)=sqrt(x), 单调递增;
2. =mid的是:left
*/
// 特殊值判断
if (x == 0) {
return 0;
}
if (x == 1) {
return 1;
}
int lo = 0, hi = x / 2;
while (lo < hi) {
int mid = (hi - lo + 1) / 2 + lo;
if (mid > x / mid) {
hi = mid - 1;
} else {
lo = mid;
}
}
return lo;
}
}
这题要注意,求解f(x)应该转为除法,否则越界,别问我怎么知道的。。。。
lc1011
这是一道标准的二分套路题,也是一道入门题,大家可以用来练手,熟悉套路
- 可以猜到:如果一天只运送一个货物,那么最起码要max(nums)那么大的运载能力,否则不可能完成;如果想一天完成,那么需要运载能力是sum(nums), 因此目标x有界,x∈[max(nums), sum(nums)];尽管不知道f(x)如何求解,但可以肯定:x若增大,需要的天数f(x)必然降低,反之升高,满足单调递减性,适合使用二分;
- 求解f(x)很简单,只需要在cur+nums[i]>m时另起一天来送货即可;
- 代码模板,不细说,看代码
class Solution {
public int shipWithinDays(int[] weights, int days) {
int lo = 0, hi = 0;
for (int n:weights) { //lo为最大重量,因为必须要保证能够载得动;hi为累计,可以一天装完
lo = Math.max(lo, n);
hi += n;
}
//x:最低装载能力,f(x)需要运输天数;单调递减函数
while (lo < hi) {
int m = (hi - lo) / 2 + lo;
int cnt = count(weights, m);
if (cnt > days) {
//装载能力不足
lo = m + 1;
} else {
//可以适当降低装载量达到最优
hi = m;
}
}
return lo;
}
private int count(int[] nums, int m) {
int cnt = 1, //至少要装一天
cur = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] + cur > m) {
cnt++;
cur = 0;
}
cur += nums[i];
}
return cnt;
}
}
lc410 410. 分割数组的最大值
虽然标为困难,但是和上面一题如出一辙
难点在于理解题意,可真狗绕口的。。。
这么简单写完一道困难题,想必你也很有成就感吧~
- 连续有界性:最好的情况下,也是将所有的数组元素一个下标一份,此时最大值就是max(nums),因此下界为max(nums), 最差情况下,只化为一份,最大值为sum(max),满足;可以料到,若增大数组最大值,那么数组切分的数量必然随之降低,反之也成立;
- f(x), 求的是切分数量;
- 若f(x)>题目给的条件,必然要舍弃该区间;否则应当在此范围内继续优化,直到lo==hi
class Solution {
public int splitArray(int[] nums, int m) {
/** 2.二分
二分的场景:在有范围的整数中查找一个数;
二分查找的依据:问题具有单调性
即:若要查找元素x,定义的查找依据是f(x),那么一定满足f(a)>f(x), a>x...
1、查找分割数组最大值的最小值,设为x;
2. 函数:f(x)为x可以确定的分割数量
任意位置a,若f(a)<f(x),证明分割数量太少,比最佳方案还少,必然导致a>x;
若f(a)=f(x),也无法确定a==x;(分割数正确,分割方式不唯一)
若f(a)=f(x)+1,且f(a-1)=f(x) (a-1是临界点)
则:x=a-1
f(x)单调递减:
f(x) < m, x > b;
== m, x∈[a, b]
> m, x < a
所求:b
*/
int max = 0, sum = 0;
for (int num: nums) {
max = Math.max(max, num);
sum += num;
}
//left分割为len段(f(left)=len),right化为1段(f(right)=1)
int left = max, right = sum;
while (left < right) {
//当前设定的a(片段最大值)
int mid = (right - left) / 2 + left;
int splits = split(nums, mid);//等价于f(x)
if (splits < m) {
//片段不足,分割度不够,证明最大值太小,缩小a
right = mid - 1;
} else {
left = mid;
}
}
return left;
}
//设定一个片段最大值为max,看能划分为几个片段
public int split(int[] nums, int max) {
int splits = 1, cur = 0;
for (int n: nums) {
if (cur + n > max) {
//不能加上这个数,此片段到此结束
splits++;
cur = 0;
}
cur += n;
}
return splits;
}
}
LCP 12. 小张刷题计划
这题是这几题中最难的一道,关于在f(x),即count()函数的写法,这里也可以猜到,二分问题最难的两点在于:
1)知道这题要用二分;
2)count()函数的编写。
不再题解,读者自己考虑
class Solution {
public int minTime(int[] time, int m) {
/**
寻找刷题最大耗时x,可以得到f(x):刷题天数,单调递减函数
*/
int lo = 0, //全都靠求助
hi = 0; //一天之内靠自己完成所有题
for (int t: time) {
hi += t;
}
while (lo < hi) {
int mid = (hi - lo) / 2 + lo;
int cnt = count(time, mid);
if (cnt > m) {
//刷題耗時不足
lo = mid + 1;
} else {
hi = mid;
}
}
System.out.println(count(time, 3));
return lo;
}
//t:当前规定每天最大耗时
public int count(int[] nums, int t) {
//为了最小化时间,记录每轮刷题最大耗时,利用求助减掉他
int max = 0, cnt = 1, cur = 0;
boolean used = false; //当天有咩有用掉求助
for (int i = 0; i < nums.length; i++) {
int n = nums[i];
if (n + cur > t) {
if (!used) {
//用掉求助
used = true;
max = Math.max(max, n);
cur -= max;
} else {
//只能第二天再刷
cnt++;
cur = 0;
used = false;
max = 0;
}
i--;
continue;
}
max = Math.max(max, n);
cur += n;
}
return cnt;
}
}
我这代码比较糙,大伙肯定有更好的写法。。。
总结
二分问题最难的两点在:
1)知道这题要用二分;
2)count()函数的编写。
第一点需要在反复刷题中提升反应度,比如我以前很难想到使用并查集去解题,在练习了一些之后,现在看到有些题目我就会觉得“这不是明摆着用xx嘛”,practise make prefect;
第二点就依赖算法能力了,但是基本上也是一个套路,可能多加一些条件而已。
附上其他的习题,自行练习:
lc875 :爱吃香蕉的珂珂;
lc1482 :制作 m 束花所需的最少天数
lc1552: 两球之间的磁力