两道题让你知道如何正确应用二分
文章适合已经大概了解或者学习了二分搜索的套路,但是在实际题目上却很难想到用二分的同学。
下面会从两道题教大家如何在一道题里用上快速知道用二分。重点就放在应用上啦。
- 力扣 -704. 二分查找(简单)
- 蓝桥杯 JavaB_2021省赛 --杨辉三角形(第八题)
力扣 -704. 二分查找(简单)
文章重点不讲解边界的判断,while是用<还是<=。这里对二分搜索还不了解的同学可以去看文章末尾推荐的算法书,里面讲解的很透彻。
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 注意
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
当然,题目一般不会以上面的形式出现。一般会让你求数字“第一次出现”
“第二次出现”
“最后一次出现”
。
求第一次出现:
int binary_search_left(int []arr ,int target ){
int left =0,right = arr.length-1;
while (left<=right){//注意我这里用的等于
int mid = left+(right-left)/2;
if(arr[mid]>=target){
right=mid-1;
}else {
left=mid+1;
}
}
//搜索哪个边界就检查哪个索引是否越界
if(left>=arr.length||arr[left]!=target)return -1;
return left;
}
最后一次出现:
int binary_search_right(int []arr ,int target ){
int left =0,right = arr.length-1;
while (left<=right){//注意我这里用的等于
int mid = left+(right-left)/2;
if(arr[mid]>target){
right=mid-1;
}else {
left=mid+1;
}
}
//搜索哪个边界就检查哪个索引是否越界
if(right<0||arr[right]!=target)return -1;
return right;
}
所以到这里可能你都明白,但是就是做题的时候不知道应该用到二分搜索的技巧。接下来,我们来刨析一下题目。
思路刨析
题目有几个要点:(数据重新假设一下)
num={1,2,3,3,5,7}
(递增)- 条件 num[i]==target
- 求 i
我们将上面的要点画成坐标图,如果说求左边界,也就是要得到索引i=2
这里 num[i]就相当于是i 的一个函数,我们把它写成 fun(i),这就是初中的一一映射,没什么高大上的。
上面说的题目要点就变成了:
- 函数—>
fun(i)
(递增/减) - 条件----> fun(i)==target
- 求值-----> i
这里我们的做题思想就出来了,题目给一个fun(i)
(可能是一个递增递减的数组、数学表达式等等),然后让你找到这个数在哪个位置。这个i
就是题目要你求的,每次做题就想想,关于i
的关系是不是递增、递减,并且需要遍历,找到一个值(条件)下的i值。
- 题目求的i
- i 的关系是否递增递减
- 是不是找到关系上的一个条件
所以遇到想不出来的时候,不如问问自己这三个问题,如果都满足的话,大概率就可以用二分搜索了。
实战:蓝桥杯 JavaB_2021省赛 --杨辉三角形(第八题)
题目考察一般也没有那么直接,所以需要一些其他知识的储备
-
知识储备:
杨辉三角形其实是组合问题值值映射表,为什么这么说?
规定: Cij 为 从i个里选j个的组合数目
上述公式是组合的一个公式,换成杨辉三角的描述就是,第n
行 第m
列 + 第n
行 第m-1
列 = 第n+1
行 第m
列 -
思考
其次,如果知道一个数在第几行 第几列 我们就能知道题目问的,这个数在是第几个数。
比较简单,大家可以想一想,我们设所在行为 i, 列为 m
所求 = (1 + i) * i/ 2 + m + 1
(1 + i) * i/ 2
===> 1+2+3+…+n 的等差数列求和
m+1
===>这个数在该行第几个
- 开始做题
如果这题能用二分做的话,我该怎么思考?
当然是按照那三个要素来问自己:
- 求 i
这里要求的是该数第几个出现,根据上面的分析,只要知道 行号 、列号,就能知道,所以问题转化 ,求 行号 n ,列号 m。 - 找函数 fun (i)
将上面的 i 替换成我们要求的,不久变成了 fun(n,m) ,想想这个函数是什么? 输入行号和列号,我们求出来的不就是该行该列所对应的值么?比如 fun (4,1)=1,fun(5,1)=2。 - 看fun函数是否递增递减
这题如果使用二分来做,稍微难想一些也就是在找递增的关系上。
单独拿出一列来看,不难发现每一列都是递增的,这里就符合我们想要找的条件。但是要注意的是,题目求第一次出现。我们发现,从左到右找的时候,黄色的部分都会在前一列出现,所以不能存在我们所要找的范围内。
先把框架写出来:
(N是我们要求的数字)
while (l <= r) {
mid = l + (r - l) / 2;
long midNum= fun(mid, m);
if (midNum== N) {
return ...;
} else if (num > N) {
r = mid - 1;
} else {
l = mid + 1;
}
}
这里fun函数需要两个参数,行数和列数,行数我们通过二分获得,那列数呢?
我们做过一列递增的情况,这种多列递增的情况怎么办?那当然也是遍历列数。那应该从左到右遍历还是从右到左?那还是再看看上面的图,第一个数出现的位置,哪一列在前,我们是不是就应该先遍历那一列。比如说数字 6 ,第一列第6行出现,第二列第四行出现,如果从左到右遍历,要找的数在第一列的数永远会出现,所以应该右往左遍历。也就是在我们二分里嵌套一个for循环,遍历每一列。
右边应该从哪一行开始遍历?
我们的数是不是都靠组合来完成,所以只要组合数的值略大于10`(9) ,如果算阶乘的话,多少的阶乘能大于 上面的数值?不想算的话可以直接想一下,1010就会有两个0 ,101112…*19最少也有 9 个 0 ,所以直接可以从19行开始,但事实上只需要16行就足够。
for (int m = 16; m >= 0; m--) {
long l = .., r =.., mid;
while (l <= r) {
mid = l + (r - l) / 2;
long midNum= fun(mid, m);
if (midNum== N) {
return (1 + mid ) * mid / 2 + m + 1;//前文说过啦
} else if (num > N) {
r = mid - 1;
} else {
l = mid + 1;
}
}
}
接着看看左和右区间,也就是一列的第一行和最后一行怎么找,第一行好说,找一下规律,每次第一个有用的数字是 在 2*m 行,那右边呢? 最大是不是我们要找的那个数字。
long l = 2 * i, r = Math.max(N, l), mid;
我们只需要写一下fun(int n ,int m)
函数,前面说了,这是一个组合数函数:
static long fun(long n, long m) {
long res = 1;
//利用了最朴素的公式n!/m!(n-m)!
for (long i = n, j = 1; j <= m; i--, j++) {
res = res * i / j;
if (res > n) return res;
}
return res;
}
到此,已经题也已经完成,完整代码如下。
static long fun(int N) {
for (int m = 16; m >= 0; m--) {
long l = 2 * m, r = Math.max(N, l), mid;
while (l <= r) {
mid = l + (r - l) / 2;
long midNum = fun(mid, m);
if (midNum == N) {
return (1 + mid) * mid / 2 + m + 1;
} else if (midNum > N) {
r = mid - 1;
} else {
l = mid + 1;
}
}
}
return 0;
}
static long fun(long n, long m) {
long res = 1;
for (long i = n, j = 1; j <= m; i--, j++) {
res = res * i / j;
if (res > n) return res;
}
return res;
}
回归本质的话,也就三句话,其他的都是一些附加的条件。
我把上面三条顺序改一改,会更加符合做题思路:
- 求值-----> i (题目要求的)
- 函数—>
fun(i)
(递增/减) (要求的是否存在一个递增递减的关系) - 条件----> fun(i)==target (找到的条件)
于是,当你能画出下面这副图的时候,大概率就能使用二分了!
参考:
《labuladong的算法秘籍V1.3》
https://blog.csdn.net/zjsru_Beginner/article/details/121875334
http://lx.lanqiao.cn/problem.page?gpid=T2912