朝花夕拾——二分

2023-12-21 08:40
文章标签 二分 朝花夕拾

本文主要是介绍朝花夕拾——二分,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

二分可以说是我编程最早接触的算法了,甚至不能说是一种具体的算法,而是一种解决问题的思路,当年打竞赛那会写得还比较多,大三以后就没怎么用过了。

二分是一个看上去很简单,但是实际一写很容易出错的算法,在我印象里是这样的,因为一些故意设计的题目很喜欢在它的边界判断给你挖坑,当年艾教说过,最好的办法是在 right - left <= 3 时停下来,然后用if自己判断,但我至今没遇到过这种二分题目。

今天刷leetcode每日一题时又遇到二分了,我直接加了一堆特判,结果发现自己想多了。

文章目录

  • 第一题 搜索插入位置
  • 第二题 完全二叉树节点的个数
  • 第三题 34. 在排序数组中查找元素的第一个和最后一个位置


第一题 搜索插入位置

先来看题目:35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。

示例 1:
输入: [1,3,5,6], 5
输出: 2示例 2:
输入: [1,3,5,6], 2
输出: 1示例 3:
输入: [1,3,5,6], 7
输出: 4示例 4:
输入: [1,3,5,6], 0
输出: 0

废话不多说:

class Solution {
public:int searchInsert(vector<int>& nums, int target) {int l = 0, r = nums.size() - 1;int mid;while(l <= r) {mid = l + ((r - l) >> 1);if (nums[mid] == target) return mid;else {if (nums[mid] < target) {l = mid + 1;} else {r = mid - 1;}}}return l;}
};

这里要重点提一嘴,

  • 为什么不用特判开头和结尾?
  • 为什么直接返回left 等价于 如果target不存在且最终应该插入的位置。

我们来看二分的一个重要步骤——获得mid的值:

mid = ((r - l) / 2) + l
mid = (l + r) / 2

以上两种写法等价,在ACM中有的题目数据很刁钻,r + l有可能大于整形的最大值。因此需要特殊处理,其实在日常中完全没必要,这算是ACMer的常规操作了。

在用二分法逼近target时,要么从右向左逼近,或从左向右逼近:

  • 情况一:target在数组中,若是这种情况不需要判断边界,直接返回值left,因为left, right, mid必定会收缩到同一值。
  • 情况二:若target不在数组中,不管是向哪边逼近,一定会收缩到 l + 1 = r 的情况
    1. target 在 l r之间,即nums[l] < target && nums[r] > target这种情况下求mid,肯定差是一个 1 / 2 等价于0,即mid = l,即一定会触发l = mid + 1,即一定是lr靠近,若数组是升序,现在的left一定是待插入的位置,若是降序呢?可以自己想一想。
    2. target 在 l r左边,即nums[l] > target && nums[r] > target这种情况下求mid,肯定差是一个 1 / 2 等价于0,即mid = r,即一定会触发r = mid - 1,即一定是rl靠近,若数组是升序,现在的left一定是待插入的位置,即0
    3. target 在 l r右边,即nums[l] < target && nums[r] < target这种情况下求mid,肯定差是一个 1 / 2 等价于0,即mid = l+1,即一定会触发l = mid + 1,即一定是lr靠近,若数组是升序,现在的left一定是待插入的位置,即nums.size()

这相当于直接回答了第二个问题,间接回答了第一个问题。

写完了以后感觉废话挺多的,溜了


第二题 完全二叉树节点的个数

题目地址:222. 完全二叉树的节点个数

在这里插入图片描述
点评:好题,考察了位运算二分,而且还是考虑二进制特点的变式二分,由于题目的特殊性,存在最终两个区间在特殊数据的情况下,lowhigh指针永远无法合并。需要对二分边界特殊处理。

看代码:

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:int getTreeHeight(TreeNode* root) {int height = 0;while(root != NULL) {root = root->left;++height;}return height;}int countNodes(TreeNode* root) {if (root == NULL) return 0;int height = getTreeHeight(root);int minSum = pow(2, height - 1);int maxSum = pow(2, height) - 1;int lo = minSum, hi = maxSum;while(lo < hi) {int mid = ((hi - lo + 1) >> 1) + lo;TreeNode* scan = root;int bit = 1 << (height - 2);int idx = height - 1;while(idx--) {if ((mid & bit) == 0) {scan = scan->left;} else {scan = scan->right;}bit >>= 1;}if (scan == NULL) {hi = mid - 1;} else {lo = mid;}}return lo;}
};

注意主函数while循环下的第一行,在上一题中我们提到,对于mid的求法在一般情况下为:

mid = (l + r) / 2       // 最简单的情况
mid = ((r - l) / 2) + l // 考虑溢出的情况

中间插一句解题思路:
由于完全二叉树的左子树的深度遍历一定可以求出完全二叉树的深度,深度求出来就可以知道此二叉树的结点数的范围,比如高度为4的完全二叉树,结点数一定在[8, 15]之间,因此我们就可以在8到15之间二分。

其实我一开始没想出来要怎么二分,因为要遍历的节点全部在最下层,我想着“如果要二分节点,由于树形结构的原因,我不可能直接去索引,肯定要从树根向下索引,但是我怎么知道每次向下搜索时,要走左还是右呢?”

这里很牛逼的一个点就出来了,由于完全二叉树的结点的值是从0到n依次递增的,每一个值得二进制的0和1刚好对应在深度遍历时应该向左走还是向右走。很神奇,不懂得可以自己画一下。

但是由于此题要在完全二叉树的最下层进行二分,最下层的每一个数翻译成二进制的01都代表了在向下搜索时的走向。而且我们要找的是最大的那个数,因此,比如当我们找到k时,low不能等于k + 1,因为k有可能就是那个最大的数,因此low要等于k,这就引出了一个二分边界的变形。

再次回顾这篇文章的时候发现讲得不是很清晰,问题的关键在于有没有target,对于有的题目来说是有target的,如果有,就可以用if (arr[mid] == target) {}来判断一下。但是比如此题在读完题后发现是没有target的,因为,题目中是找完全二叉树最下层中最大的不为null的值,你也不知道这个数是谁,无法用上面的if语句判断。

1 2 3 4 5 n n n n n n n n n n n

比如上面这种情况,当你的midn的时候,一定可以用high = mid - 1,因为,根据题意,你要找的是一个整数,那么整数肯定在mid的左边。

但是,比如说你现在的mid指向的就是5,但是你不知道这个数是不是最大的,有可能是最大的,所以你用low = mid + 1的时候就要很小心,因为你可能指向的就是你要找的数,如果你执行了这句话,low指向n,high也指向n,你就永远也找不到你要找的值了。所以,只能执行low = mid来逼近结果,不能加1。

... 4 5 n ...

所以就可能出现这种情况,low指向4,high指向5,mid = low + high / 2指向4。当出现这种情况时就发生了死循环。就是因为low = mid,永远无法逼近结果。所以我们这里需要让mid向高处靠,而不是朝低处靠。因此就出现这样特殊的mid计算。

因此这里二分的边界是这样的:

mid = (l + r) / 2       		 // 最简单的情况
mid = ((r - l) / 2) + l 		 // 考虑整形溢出的情况
mid = ((hi - lo + 1) / 2) + lo;  // mid向右偏1/2

是不是很巧妙?

第三题 34. 在排序数组中查找元素的第一个和最后一个位置

题目:很简单,就是给你一个排好序的升序数组。让你在里面找一个target,这个target可能不止一个,也可能不存在,让你分别找出这个数组中最左侧和最右侧的target的位置。

思路:得益于上一题,我这个题闭着眼睛就做出来了,没看题解。我先直接二分,找到一个target的位置,只要找到就停下。那么这个target的位置记为kk有可能是最左侧的,也有可能是最右侧的,但大概率还是在中间的。我们对k左右侧分别进行两个二分,左侧的而分是对区间[0, k],右侧是[k, length - 1],注意这里为什么不是[0, k - 1]和[k + 1, length - 1]。正如我们之前所说的,这个k有可能本身在最左侧或最右侧。其实这里提这个不是很重要,主要在于下面对边界的处理,是不是和上一题很像?由于题目场景的原因,当向右扩展的时候low不能等于mid+1,只能等于mid,就是因为这个当前的位置,有可能已经是最右侧的target了。

mid = (l + r) / 2       		 // 最简单的情况
mid = ((r - l) / 2) + l 		 // 考虑整形溢出的情况
mid = ((hi - lo + 1) / 2) + lo; // 包裹下界的情况

有没有想到这里?是不是一模一样?但是为什么左侧不用?因为编程语言的特性,当整数截断的时候,自动向下取整,所以左侧就不用特别判断。但是这里如果我们不让他手动向上取整,就会在最后两个值中无限循环,因为
mid永远加不上去。

class Solution {
public:vector<int> searchRange(vector<int>& nums, int target) {int hi = nums.size() - 1;int lo = 0;bool exit = false;int idx_left = -1, idx_right = -1;while(!exit && lo <= hi) {int mid = (lo + hi) / 2;if (nums[mid] < target) {lo = mid + 1;} else if (nums[mid] > target) {hi = mid - 1;} else {int lo_left = 0;int hi_left = mid;while(lo_left < hi_left) {int mid_left = (lo_left + hi_left) / 2;if (nums[mid_left] != target) {lo_left = mid_left + 1;} else {// 没有 -1hi_left = mid_left;}}idx_left = lo_left;int lo_right = mid;int hi_right = nums.size() - 1;while(lo_right < hi_right) {// 特殊处理int mid_right = (hi_right - lo_right + 1) / 2 + lo_right;if (nums[mid_right] != target) {hi_right = mid_right - 1;} else {// 没有 +1lo_right = mid_right;}}idx_right = lo_right;exit = true;}}return vector<int> {idx_left, idx_right}; }
};

这篇关于朝花夕拾——二分的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/519313

相关文章

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

poj 2976 分数规划二分贪心(部分对总体的贡献度) poj 3111

poj 2976: 题意: 在n场考试中,每场考试共有b题,答对的题目有a题。 允许去掉k场考试,求能达到的最高正确率是多少。 解析: 假设已知准确率为x,则每场考试对于准确率的贡献值为: a - b * x,将贡献值大的排序排在前面舍弃掉后k个。 然后二分x就行了。 代码: #include <iostream>#include <cstdio>#incl

poj 3104 二分答案

题意: n件湿度为num的衣服,每秒钟自己可以蒸发掉1个湿度。 然而如果使用了暖炉,每秒可以烧掉k个湿度,但不计算蒸发了。 现在问这么多的衣服,怎么烧事件最短。 解析: 二分答案咯。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <c

poj 3258 二分最小值最大

题意: 有一些石头排成一条线,第一个和最后一个不能去掉。 其余的共可以去掉m块,要使去掉后石头间距的最小值最大。 解析: 二分石头,最小值最大。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <c

poj 2594 二分图最大独立集

题意: 求一张图的最大独立集,这题不同的地方在于,间接相邻的点也可以有一条边,所以用floyd来把间接相邻的边也连起来。 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <sta

poj 3692 二分图最大独立集

题意: 幼儿园里,有G个女生和B个男生。 他们中间有女生和女生认识,男生男生认识,也有男生和女生认识的。 现在要选出一些人,使得这里面的人都认识,问最多能选多少人。 解析: 反过来建边,将不认识的男生和女生相连,然后求一个二分图的最大独立集就行了。 下图很直观: 点击打开链接 原图: 现图: 、 代码: #pragma comment(

poj 2112 网络流+二分

题意: k台挤奶机,c头牛,每台挤奶机可以挤m头牛。 现在给出每只牛到挤奶机的距离矩阵,求最小化牛的最大路程。 解析: 最大值最小化,最小值最大化,用二分来做。 先求出两点之间的最短距离。 然后二分匹配牛到挤奶机的最大路程,匹配中的判断是在这个最大路程下,是否牛的数量达到c只。 如何求牛的数量呢,用网络流来做。 从源点到牛引一条容量为1的边,然后挤奶机到汇点引一条容量为m的边

二分最大匹配总结

HDU 2444  黑白染色 ,二分图判定 const int maxn = 208 ;vector<int> g[maxn] ;int n ;bool vis[maxn] ;int match[maxn] ;;int color[maxn] ;int setcolor(int u , int c){color[u] = c ;for(vector<int>::iter

POJ2413二分

注意二分, 上界。 import java.beans.beancontext.BeanContext;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrite