二分法 算法题笔记

 二分两种题型:

题型一:二分求下标(在数组中查找符合条件的元素的下标)

题型二:二分答案(在一个有范围的区间里搜索一个整数)

题型三:二分答案的升级版(每一次缩小区间的时候都需要遍历数组) 

参考3.1 二分查找习题分类 | 算法吧 (suanfa8.com)致谢大佬!!!

本文的东西全部参考大佬的思路和启发!!感谢!!!

结合大佬的文章资源,加上自己的思考和经验所写  再次感谢!

目录

题型一:二分求下标(在数组中查找符合条件的元素的下标)

1.  436. 寻找右区间

2.1300. 转变数组后最接近目标值的数组和

3.33. 搜索旋转排序数组

4. 81. 搜索旋转排序数组 II

 5.153. 寻找旋转排序数组中的最小值

 6.154. 寻找旋转排序数组中的最小值 II

7.【多看!这里有二分的基本步骤】852. 山脉数组的峰顶索引

题型二:二分答案(在一个有范围的区间里搜索一个整数)

1.69. x 的平方根  

2.287. 寻找重复数 

3.1283. 使结果不超过阈值的最小除数 

题型三:二分答案的升级版(每一次缩小区间的时候都需要遍历数组)

1.875. 爱吃香蕉的珂珂

2.410. 分割数组的最大值

 3.LCP 12. 小张刷题计划

4.1011. 在 D 天内送达包裹的能力 

5.[未解决]1482. 制作 m 束花所需的最少天数

6.[未解决]1552. 两球之间的磁力 

四.言归正传,回归蓝桥杯

1.试题 历届真题 分巧克力

2.「M 次方根」   实数型二分查找  **必看**


题型一:二分求下标(在数组中查找符合条件的元素的下标)

1.  436. 寻找右区间

题目中的关键字是找「大于等于」,最小的那个区间的下标,很显然需使用二分查找算法;

要使用二分查找,需要在有序的环境中进行,因此,需要对区间排序(可将这一步称之为预处理);

题目要求返回索引,但是排序以后,索引信息丢失。因此在预处理的时候,就得把「起点」和「下标」的关系存起来。

刚刚好题目中说道:「你可以假定这些区间都不具有相同的起始点」,用「哈希表」正合适;

排序的时候,只需要对起点进行排序即可;

在二分查找的时候,传入的是区间的右端点,查找的是大于等于区间的右端点的第 1 个值,因此它的反面就是:小于一定不是解。根据这个「逐渐缩小搜索区间」的策略,编写二分查找算法;

/*
我们看到大于等于,这种敏感的缩小范围的词语想到二分查找
我们需要先来梳理一下大致思路:
1.遍历二维数组当中每个一维数组的第二个元素,寻找在有序范围内第一个大于等于的数值所对应的范围下标
我们需要现将二维的第一列元素进行排序,但是又要记录下来他们的索引
又注意到每个区间段的第一个起始数各不相同————>
1.考虑到哈希表的应用[哈希表的key---value 一一对应],并且一个key要不重复如果重复则会覆盖
2.将二维数组的第一列、第二列抽取出来,在新建一个二维数组进行标记下标
 */
public class Main {
    public int[] findRightInterval(int[][] intervals) {
    	int len=intervals.length;
    	if(len==0||len==1) {
    		return new int[] {-1}; 
    	}
    	HashMap<Integer, Integer> map=new HashMap<>();
    	int[] start=new int[len];
    	int[] ans=new int[len];
    	for (int i = 0; i < len; i++) {
			map.put(intervals[i][0], i);
			start[i]=intervals[i][0];
		}
    	Arrays.sort(start);
    	for (int i = 0; i < len; i++) {
			int end=intervals[i][1];
			int index=Bin(start,end);
			if(index==-1) 
				ans[i]=-1;
			else
				ans[i]=map.get(start[index]);
		}
		return ans;

    }
    static int Bin(int[] num,int target) {
    	int len=num.length;
    	int left=0;int right=len-1;
    
    	while(left<right) {
    		int mid=left+((right-left)>>1);
    		if(num[mid]<target)
    			left=mid+1;
    		else
    			right=mid;
    	}
    	if(num[left]>=target)
    		return left;
    	else
    		return -1;
    }
	public static void main(String[] args) {
		Main test=new Main();
		int num[][]=new int[][] {{1,4}};
		int[] ans=test.findRightInterval(num);
		for (int i : ans) {
			System.out.print(i+" ");
		}
	}
}

2.1300. 转变数组后最接近目标值的数组和

 自己写的臭代码如下:

	public int findBestValue(int[] arr, int target) {
		Arrays.sort(arr);
		int len = arr.length;
		int sum = 0;
		for (int i = 0; i < arr.length; i++)
			sum += arr[i];
		if (sum == target)
			return arr[len - 1];
		int value = target / len;
		int ans;
		for (ans = value; ans <= target; ans++) {
			int[] s1 = new int[len];
			for (int i = 0; i < arr.length; i++)
				s1[i] = arr[i];

			int index = Binsum(s1, ans);
			if (index == -1) {
				ans--;
				break;
			}
			int a1 = Math.abs(sum(s1, index, ans) - target);
			int a2 = Math.abs(sum(s1, index, ans + 1) - target);
			if ((a2 > a1) || (a2 == a1)) {
				break;
			}
		}
		return ans;
	}

	private int sum(int[] num, int index, int value) {
		int len = num.length;
		for (int i = index; i < len; i++)
			num[i] = value;

		int sum = 0;
		for (int i = 0; i < len; i++)
			sum += num[i];
		return sum;
	}

	private int Binsum(int[] num, int value) {
		int len = num.length;
		int left = 0;
		int right = len - 1;
		if (num[len - 1] < value)
			return -1;

		while (left < right) {
			int mid = left + ((right - left) >> 1);
			if (num[mid] <= value)
				left = mid + 1;
			else
				right = mid;
		}
		return left;

	}

 犯几个致命错误:

1.忽略了java的引用变量地址传递机制,下面这个例子可以很好地说明

public static void main(String[] args) {
		Main test = new Main();
		int[] arr = new int[] { 2, 3, 5 };
//		System.out.println(test.findBestValue(arr, 11));
		int[] sum=arr;
		for (int i : arr) 
			System.out.print(i+" ");
		System.out.println();
		sum[0]=99999999;
		for (int i : arr) 
			System.out.print(i+" ");
		
	}

2.注意循环当中如果要采用了引用变量地址传递机制  要仔细思考是否需要新建对象!!!

3.记得进行特判!!

3.33. 搜索旋转排序数组

本题将介绍两种做法 我的做法和大佬的做法 

我的做法如下:初步看到此题根据单调性和要查找一个元素的下标我会敏感想到二分法,又结合到本文的第一题每个数组的各个元素都不相同我想到了先采用哈希表将旋转数组的数组元素值和对应的索引下标来进行一一对应存储,然后将nums数组进行排序,转换为有序数组,此时问题就转换为最简单最常规的二分查找数组元素的下标问题【二分+哈希】

提交给力扣,显然采用的是空间换时间的做法 

/*
整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转
使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target
如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

 */
public class Main {
	/*
	 * 大致思路:虽然它进行了旋转操作,但是它本来是一个有序数组,满足二分的算法的单调性
	 * 并且我们要记录旋转数组的索引下标,又看到数组的每个元素值都各不相同我们想到哈希表
	 */
	public int search(int[] nums, int target) {
		int len = nums.length;
		// 记录旋转数组的每个位置与索引的一一对应关系
		HashMap<Integer, Integer> map = new HashMap<>();
		for (int i = 0; i < len; i++)
			map.put(nums[i], i);
		Arrays.sort(nums);// 将数组进行排序,转化为最常规的问题
		// 二分
		int index = Bin(nums, target);
		if (index == -1) {
			System.out.println("********");
			return -1;
		}

		return map.get(nums[index]);

	}

	private int Bin(int[] nums, int target) {
		int len = nums.length;
		// 特判
		if (len == 0)
			return -1;

		int left = 0;
		int right = len - 1;
		while (left < right) {
			int mid = left + ((right - left + 1) >> 1);
			if (nums[mid] > target)
				right = mid - 1;
			else
				left = mid;
		}
		if (nums[left] == target)
			return left;
		return -1;
	}

	public static void main(String[] args) {
		Main test = new Main();
		int[] nums = new int[] { 4, 5, 6, 7, 0, 1, 2 };
		System.out.println(test.search(nums, 2));

	}
}

大佬的做法如下: 

 二分查找(Java) - 搜索旋转排序数组 - 力扣(LeetCode) (leetcode-cn.com)

文章好像有一个隐藏逻辑问题,想跟老师讨论一下,文章中说 “讨论 中间元素和右边界的关系 为例,其它情况类似。由于不存在重复元素,所以它们的关系不是大于就是小于”,这里“不是大于就是小于”的理解,它们的关系可能是“等于”,如这种情况,比如[3,1],target为1,一开始mid = 1,此时nums[mid] = nums[right]。对应else里,只能得到 [left, mid - 1] 有序,但不能得到 [left, mid] 有序,最后结果误打误撞也正确,只是逻辑会有问题。 不知道理解是否有漏洞,期待老师或大伙回复。

我的意思不是说代码逻辑有问题,是说文章和注释对这块逻辑的叙述,是不是有问题?

文章中说“讨论 中间元素和右边界的关系 为例,其它情况类似。由于不存在重复元素,所以它们的关系不是大于就是小于”,这里不是”除了大于就是小于“,也可能是”等于“;

以及注释里说”数组前半部分有序,即[left..mid] 有序,为了与上一个分支的逻辑一致,认为 [left..mid - 1] 有序“,注释这里写的是”为了与上一个分支的逻辑一致“,才只认为 [left..mid - 1] 有序,

但应该是:本来就只能得到 [left..mid - 1] 有序的结论,并不能确定 [left..mid] 是有序的,原因就是除了 nums[mid] < nums[right] 和 nums[mid] > nums[right] ,也可能 nums[mid] = nums[right] 。所以不是”为了一致“而认为的,是本身就只能得到 [left..mid - 1] 有序。

	/*
	我们多写几个旋转数组可以发现:
	1.无论如何mid一定会落在一个有序区间当中
	我们可以进行分情况讨论
	
	 */
	public int search(int[] nums, int target) {
		int len=nums.length;
		if(len==0) return -1;//特判
		int left=0;int right=len-1;
		while(left<right) {
			int mid=left+((right-left+1)>>1);
			//2.1 if(nums[mid]<nums[right]) 说明[mid,right]是有序的!
			//如果target存在 要么在[mid,right]之间,要么在[left,mid-1]之间
			if(nums[mid]<nums[right]) {
				if(nums[mid]<= target&& target<=nums[right])//target在[mid,right]之间
					left=mid;
				else
					right=mid-1;
			}//2.2 if(nums[mid]>=nums[right]) 只可以说明[left,mid-1]有序!!!
			如果target存在 要么在[left,mid-1]之间,要么在[mid,right]之间
			else {
				if(nums[left]<=target && target<=nums[mid-1])
					right=mid-1;
				else
					left=mid;
			}
		}
		if(nums[left]==target)
			return left;
		return -1;
		
		
	}

上面的这段讨论很好地解决了为什么下面要这么写  因为是当nums[mid]>nums[right]的时候,只可以保证[left,mid-1]是有序的!!!  举例[3,1]

if (nums[left] <= target && target <= nums[mid - 1]) {
                    // 如果 target 的值落在区间 [left..mid - 1] 里,设置 right = mid - 1;
                    right = mid - 1;
    }

体会:1.通过本题可以进一步体会二分法的使用,我们可以分析目标元素所落在的位置,来逐步缩小搜索的区间,最终会确定到一个值left==right 退出循环,我们只需对nums[left]与目标元素进行特判即可!!

2.只要我们使用到了left=mid  就一定要进行向上取整

3.大致伪代码如下:

while(left<right){

        编写缩小搜索区间的程序,常常从反面出发,将mid落在else的区间当中

}

后面退出循环再进行nums[left]与目标元素进行特判

if(nums[left]==target){

             ..........

}

4.在编写缩小搜索区间的if条件可能会存在多个!

TIPS:此题的关键在于以往的mid是在一个确定的有序区间内

而本题的mid所在的有序区间具有两种可能,所以我们要进行分类讨论

4. 81. 搜索旋转排序数组 II

要注意nums[mid]==nums[right]

先要判断nums[right]是不是target  如果不是则缩小区间,右边界往中间聚拢:right--

public boolean search(int[] nums, int target) {
		int len = nums.length;
		if (len == 0)
			return false;// 特判
		int left = 0;
		int right = len - 1;
		while (left < right) {
			int mid = left + ((right - left + 1) >> 1);
			if (nums[mid] < nums[right]) {
				if (nums[mid] <= target && target <= nums[right])// target在[mid,right]之间
					left = mid;
				else
					right = mid - 1;
			} else if (nums[mid] > nums[right]) {
				if (nums[left] <= target && target <= nums[mid])
					right = mid;
				else
					left = mid + 1;
			} else {
				if (nums[right] == target)
					return true;

				right = right - 1;
			}
		}
		if (nums[left] == target)
			return true;
		return false;

	}

 5.153. 寻找旋转排序数组中的最小值

 相信经过前面的练习你一定拿下

上代码

class Solution {
public int findMin(int[] nums) {
		int len=nums.length;
		if(len==0) return -1;
		int left=0;int right=len-1;
		while(left<right) {
			int mid=left+((right-left)>>1);
			if(nums[mid]<nums[right]) {
				right=mid;
			}else if(nums[mid]>nums[right]) {
				left=mid+1;
			}else {
				if(nums[mid]<nums[mid-1])
					left=mid;
				else
					right=mid-1;
			}
		}
		return nums[left];
	}
}

 6.154. 寻找旋转排序数组中的最小值 II

大佬代码如下

结论 3:当中间数与右边界表示的数相等的时候,看下面两个例子:

例 5:[0, 1, 1, 1, 1, 1, 1]

例 6:[1, 1, 1, 1, 0, 1, 1]

目标值可能在中间数的左边,也可能在中间数的右边,那么该怎么办呢?很简单,此时你看到的是右边界,就把只右边界排除掉就好了。正是因为右边界和中间数相等,你去掉了右边界,中间数还在,就让中间数在后面的循环中被发现吧。

因此,根据中间数和右边界的大小关系,可以使用二分法搜索到目标值。

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/solution/er-fen-fa-fen-zhi-fa-python-dai-ma-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {

public int findMin(int[] nums) {
		int len=nums.length;
		if(len==0) return -1;
		int left=0;int right=len-1;
		while(left<right) {
			int mid=left+((right-left)>>1);
			if(nums[mid]<nums[right]) {
				right=mid;
			}else if(nums[mid]>nums[right]) {
				left=mid+1;
			}else {
                //  assert nums[mid] == nums[right];
                right--;
               
			}
		}
		return nums[left];
	}

}

 我的代码如下采用向上取整:

好好体会向上取整和向下取整!!!!!!!!!!!!!

class Solution {

public int findMin(int[] nums) {
		int len=nums.length;
		if(len==0) return -1;
		int left=0;int right=len-1;
		while(left<right) {
			int mid=left+((right-left+1)>>1);
			if(nums[mid]<nums[right]) {
				right=mid;
			}else if(nums[mid]>nums[right]) {
				left=mid+1;
			}else {
                if(nums[mid-1]>nums[mid]){
                    left++;
                }else{
                    right--;
                }
                
				// if(nums[mid-1]<=nums[mid])
                //     right--;
                // else
			}
		}
		return nums[left];
	}

}

7.【多看!这里有二分的基本步骤】852. 山脉数组的峰顶索引

看到寻找数组的下标  又可以知道单调  有根据时间复杂度O(log(n))---愣着干什么!!二分!

我们再来回顾一下二分法的基本思路

1.特判 

        由题意可知,当存在三个元素的时候,峰顶元素的下标一定为1

2.确定我们要二分查找的范围

        峰顶元素的下标处于[0,len-1],只需要设置left=,right=len-1即可

3.编写缩小搜索区间的if语句

        说明我的心路历程:做完旋转数组之后,我们注意到旋转数组是存在两部分的有序部分构成,我首先采用的是将中间元素与最右边界元素来比较大小

        例1:1 2 3 4 5 6 2

        例2:   1 2 8 7 6 5 2

上面两个例子可以得出中间元素大于最右边界,但是无法判断峰顶到底位于哪一部分,我们又注意到其单调性是先升后减   可以进行nums[mid]与nums[mid+1]的判断

如果nums[mid] > nums[mid+1] 说明递减区间,峰顶元素位于[left,mid]之间(包含mid)-----right=mid

如果nums[mid] < nums[mid+1] 说明递增区间,峰顶元素位于[mid+1,right]之间(包含mid)---------left=mid+1;

反之同理不再赘述,后来我发现对于采用的是将中间元素与最右边界元素来比较大小 意义不大!可以省去修改,不再赘述。

4.对于向上取整和向下取整的选择

未使用left=mid 并且代码使用了nums[mid+1] 所以采用向下取整不会越界!

按照心路历程优化后的代码如下

   public int peakIndexInMountainArray(int[] arr) {
    	int len=arr.length;
    	//特判
    	if(len==3)
    		return 1;
    	//明确答案下标的范围:[0,len-1];
    	int left=0;int right=len-1;
    	while(left<right) {
    		int mid=left+((right-left)>>1);
    		
    			if(arr[mid]>arr[mid+1]) {
    			//说明[mid,right]单调递减 答案出现在[left,mid]之间  mid可能是答案
        			right=mid;
    			}else {
    				left=mid+1;
    			}
    		
    	}
		return left;

    }

题型二:二分答案(在一个有范围的区间里搜索一个整数)

1.69. x 的平方根  

 此题做完之后也是异常开心与大佬思路一致

采用以下步骤来进一步缩小搜索区间

1.一个数的平方根一定会小于等于它的1/2

2.我们观察数据范围,为了避免溢出采用除数

    public int mySqrt(int x) {
    	/*
    	 * 有个数的算术平方根肯定是小于等于这个数的1/2
    	 * 二分 我们要寻找第一个数平方后 小于等于 x的数
    	 */
    	//1.特判
    	if(x==0) return 0;
    	if(x==1) return 1;
    	//2.明确搜索范围[0,x];
    	int left=0;int right=x/2;
    	while(left<right) {
    		int mid=left+((right-left+1)>>1);
    		//3.编写缩小搜索区间的条件
    		if(mid>(x/mid)) {
    			right=mid-1;
    		}else {
    			left=mid;
    		}
    	}
		return left;
    }

2.287. 寻找重复数 

 一般我们来寻找重复数,是采用的是哈希表,此题严格明确来使用O(1)以及不修改数组

详见解析:使用二分法查找一个有范围的整数(结合抽屉原理) - 寻找重复数 - 力扣(LeetCode) (leetcode-cn.com)

public class Solution {

    public int findDuplicate(int[] nums) {
        int len = nums.length;
        int left = 1;
        int right = len - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            
            int cnt = 0;
            for (int num : nums) {
                if (num <= mid) {
                    cnt += 1;
                }
            }

            // 根据抽屉原理,小于等于 4 的个数如果严格大于 4 个,此时重复元素一定出现在 [1..4] 区间里
            if (cnt > mid) {
                // 重复元素位于区间 [left..mid]
                right = mid;
            } else {
                // if 分析正确了以后,else 搜索的区间就是 if 的反面区间 [mid + 1..right]
                left = mid + 1;
            }
        }
        return left;
    }
}

3.1283. 使结果不超过阈值的最小除数 

最开始拿到此题并没有对于二分来如何处理有思路,看到最大最小这种字眼也是采用二分! 

自己如下代码:因为明确知道数组的除数一定从数组的元素和除以阈值开始的

通过了68个样例,还有2个未通过 超时

public int smallestDivisor(int[] nums, int threshold) {
		   long sum=0;
		   for (int i : nums) {
			sum+=i;
		   }
		   long d=div( sum,threshold);
		   for (long j = d;true; j++) {
			   long ans=0;
			   for (int i = 0; i < nums.length; i++) {
				   ans+=div(nums[i], j);
			   }

			   if(ans<=threshold) {
				   return (int)j; 
			   }
		}
		   
	    }
	   public long div(long a,long j) {
		   if(a%j==0) return a/j;
		   else return (a/j)+1;
	   }

大佬思路:

1.明确题目要找的是什么-----除数

2.观察数据范围  除数肯定是在一个有序区间内  所以我们要确定left和right

left==数组的和/阈值, 因为阈值大于数组长度,若除数大于数组当中最大的数,除完之后数组的和是等于数组的长度的     所以right=数组中元素最大值。

public class Solution {

    public int smallestDivisor(int[] nums, int threshold) {
        // 先找数组中的最大值,用最大值作为除数,除完以后和最小
        int maxVal = 1;
        for (int num : nums) {
            maxVal = Math.max(maxVal, num);
        }

        // 注意:最小值是 1,因为 threshold 可以很大
        int left = 1;
        int right = maxVal;

        while (left < right) {
            int mid = (left + right) >>> 1;

            if (calculateSum(nums, mid) > threshold) {
                // sum 大于阈值一定不是解,说明除数选得太小了
                // 下一轮搜索区间是 [mid + 1, right]
                // (把下一轮搜索区间写出来,边界选择就不会错)
                left = mid + 1;
                // 边界是 left = mid + 1 ,中间数不用上取整
            } else {
                right = mid;
            }
        }
        return left;
    }

    /**
     * @param nums
     * @param divisor
     * @return 数组中各个元素与 divisor 相除的结果(向上取整)之和
     */
    private int calculateSum(int[] nums, int divisor) {
        int sum = 0;

        for (int num : nums) {
            sum += num / divisor;

            // 注意:不能整除的时候,需要向上取整
            if (num % divisor != 0) {
                sum++;
            }
        }
        return sum;
    }
}

题型三:二分答案的升级版(每一次缩小区间的时候都需要遍历数组)

 

我学习完成之后再来进行一些总结:

 此类题目都是细节怪!!!

此类题目还常常出现 最大 最小等极值字眼!

规定:我们要求的对象 A, 约束它的变量 B

1.先分析我们要求的数是不是满足正整数,连续。如果满足整数且连续 我们看看是不是可以分析题意找出这个要求的对象的所在的区间【分析是不是整数,且连续         找到所求对象的上下限】

2. 分析所要求的对象是不是与另一个变量相互制约,我们在while循环当中所编写的if()判断分支就是根据这约束所求对象的变量来进行缩小搜索范围的       【常常满足 A越大 B越小   A越小B越大】

3.分析出我们要求的对象a,与它的约束变量直接的函数关系式,通过函数关系式来将A转换为B***,将转换的结果在while循环当中所编写的if()判断分支来进行比较B***与B的大小。

1.875. 爱吃香蕉的珂珂

 分析:1.我们要求的是速度,速度是正整数,且连续    我们分析可以知道它的上限界线:【1,max(piles[i])】

2.我们发现速度与时间成约束关系

3.编写速度转换为时间的函数 hour(int[] piles,int k)

class Solution {
	 public int minEatingSpeed(int[] piles, int h) {
		int len=piles.length;
		//1.特判【不需要】
		//2.获得速度范围
		int max=0;
		for (int i : piles)
			max=Math.max(i, max);
		int left=1;int right=max;
		while(left<right) {
			int mid=left+((right-left)>>1);
			if(hour(piles, mid)>h) 
				left=mid+1;
			else
				right=mid;
		}
		return left;
		
	 }
	 public int hour(int[] piles,int k) {
		 int h=0;
		 for (int i : piles) {
			if(i%k==0) h+=i/k;
			else h+=((i/k)+1);
		}
		return h;
	 }
	 
}

2.410. 分割数组的最大值

1.首先分析题目让我们求解的是[子数组各自和的最大值] 
我们又发现[子数组各自和的最大值]具有一定的范围:[max(),sum(子数组各自和)]
2.我们再来寻找单调性发现子数组各自和的最大值与分隔数m成约束关系
子数组各自和的最大值越小,m越大   子数组各自和的最大值越大,m越小
3.并且这两个互相约束的条件总可以通过一个函数来实现

****************************************最大的xxx的最小值是如何选择的*********************************

​​​​​​​

 


    public int splitArray(int[] nums, int m) {
    	int len=nums.length;
    	//2.获取所求对象的范围
    	int max=0;int sum=0;
    	for (int i : nums) {
			sum+=i;
			max=Math.max(i, max);
		}
    	//1.特判
    	if(m==len)	return max;
    	//对二分范围赋值
    	int left=max;int right=sum;
    	System.out.println(max+"   "+sum); 
    	while(left<right) {
    		int mid=left+((right-left)>>1);
    		if(spilt(nums, mid)>m)
    			left=mid+1;
    		else
    			right=mid;
    	}
    	return left;
    }
    public int spilt(int[] nums,int max) {
    // 至少是一个分割
        int splits = 1;
        // 当前区间的和
        int curIntervalSum = 0;
        for (int num : nums) {
            // 尝试加上当前遍历的这个数,如果加上去超过了「子数组各自的和的最大值」,就不加这个数,另起炉灶
            if (curIntervalSum + num > maxIntervalSum) {
                curIntervalSum = 0;
                splits++;
            }
            curIntervalSum += num;
        }
        return splits;

    }

 3.LCP 12. 小张刷题计划

做了多次会发现此题目是基于第二题的  完全可以类比过去  切记注意细节

此类型函数的关系可以如下写法:

 public int spilt(int[] nums,int max) {
    // 至少是一个分割
        int splits = 1;
        // 当前区间的和
        int curIntervalSum = 0;
        for (int num : nums) {
            // 尝试加上当前遍历的这个数,如果加上去超过了「子数组各自的和的最大值」,就不加这个数,另起炉灶
            if (curIntervalSum + num > maxIntervalSum) {
                curIntervalSum = 0;
                splits++;
            }
            curIntervalSum += num;
        }
        return splits;

    }

本题代码如下 

/*
1.我们要求的是 m 天中做题时间最多的一天耗时为 T的最小值  我们观察到T为肯定为正整数,且连续
我们可以分析出来T的区间范围:[0,sum(time[i])-max]
2.制约关系:T越大天数越小   T越小天数越大
3.编写函数
*/
    public int minTime(int[] time, int m) {
    	int max=0;int sum=0;
    	for (int i : time) {
			max=Math.max(i, max);
			sum+=i;
    	}
    	//1.特判
    	if(m==1) return sum-max;
    	//2.确定界限
    	int left=0;int right=sum-max;
    
    	while(left<right) {
    		int mid=left+((right-left)>>1);
    		if(day(time, mid)>m)
    			left=mid+1;
    		else
    			right=mid;
    		
    		System.out.println(left+" * "+right);
    	}
		return left;
    }
    public int day(int[] time,int t) {
    	int d=1;
    	int max=0;int sum=0;
    	for (int i : time) {
    		max=Math.max(i, max);
			if((sum+i-max)>t) {
				d++;
				sum=0;
				max=i;
			}
			sum+=i;
		}
    	return d;
    }

4.1011. 在 D 天内送达包裹的能力 

思路与第二题一模一样 直接拿下:

class Solution {
/*
1.我们要求的是  船的最低运载能力 我们观察到T为肯定为正整数,且连续
我们可以分析出来T的区间范围:[max(weights[i]),sum]
2.制约关系:S越大天数越小   S越小天数越大  并且当天货运量必须小于等于S
3.编写函数
*/
    public int shipWithinDays(int[] weights, int days) {
    	int max=0,sum=0;
    	for (int i : weights) {
			max=Math.max(i, max);
			sum+=i;
		}
    	//范围界限赋值
    	int left=max;int right=sum;
    	while(left<right) {
    		int mid=left+((right-left)>>1);
    		if(day(weights, mid)>days)
    			left=mid+1;
    		else
    			right=mid;
    	}
    	
		return left;

    }
    public int day(int[] weights,int capacity) {
    	int sum=0;int d=1;
    	for (int i : weights) {
			if(sum+i>capacity) {
				d++;
				sum=0;
			}
			sum+=i;
		}
    	return d++;
    }
}

5.[未解决]1482. 制作 m 束花所需的最少天数

6.[未解决]1552. 两球之间的磁力 

四.言归正传,回归蓝桥杯

我学习二分算法是为了蓝桥杯

所以回到蓝桥杯:

1.试题 历届真题 分巧克力

1.拿到此题我们先来分析题目要求求什么-------巧克力的最大边长      很容易发现这个边长是正整数且连续,我们也很容易得到它的范围[1,max(所有巧克力的长,宽)]

满足二分答案的基本要求:单调,有序。

2.然后我们又发现其边长与人数是存在相互制约的关系:边长越大 可分的人数越少。边长越小 可分的人数越多。

3.编写边长----人数的函数

代码如下:

package main;

import java.io.*;
import java.math.*;
import java.util.*;

public class Main {
/*
1.我们要求的是 最大的边长是多少
我们观察到边长肯定为正整数,且连续
我们可以分析出来正常情况下 边长的区间范围:[1,max(qiaoke[i])]
2.制约关系:边长越大人数越小  边长越小人数越大
3.编写函数
*/
    public int maxLens(int[][] chocolate,int k) {
    	int max=chocolate[0][0];
    	for (int i = 0; i < chocolate.length; i++) {
			for (int j = 0; j < 2; j++) {
				max=Math.max(max, chocolate[i][j]);
			}
		}
    	//赋值上下界限
    	int left=1,right=max;
    	while(left<right) {
    		int mid=left+((right-left+1)>>1);
    		if(number(chocolate,mid)<k)
    			right=mid-1;
    		else
    			left=mid;
    	}
		return left;
    }
    public int number(int[][] chocolate,int len) {
    	int num=0;
    	for (int i = 0; i < chocolate.length; i++) {
			int down=chocolate[i][0]/len;
			int up=chocolate[i][1]/len;
			num=num+down*up;
		}
		return  num;
    }
	public static void main(String[] args) {
		Main t=new Main();
		Scanner sc=new Scanner(System.in);
		int n=sc.nextInt();int k=sc.nextInt();
		int[][] chocolate=new int[n][2];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < 2; j++) {
				chocolate[i][j]=sc.nextInt();
			}
		}
		System.out.println(t.maxLens(chocolate, k));
	
	}
}

2.「M 次方根」   实数型二分查找  **必看**

实数型 二分查找的模板:实数型缩小区间只需要都赋值mid即可!!!!!!

//令eps 为小于题目精度一个数即可。

//比如题目说保留4位小数,0.0001 这种的。那么eps 就可以设置为五位小数的任意一个数 0.00001- 0.00009 等等都可以。一般为了保证精度我们选取精度 /100 的那个小数,即设置  eps= 0.0001/100 =1e-6。

while (l + eps < r) {
  double mid = (l + r) / 2;

  if (pd(mid)) 
      r = mid; 
  else 
      l = mid;
}

// 实数域二分,规定循环次数法

//通过循环一定次数达到精度要求,这个一般log2N< 精度即可。N为循环次数,在不超过时间复杂度的情况下,可以选择给N乘一个系数使得精度更高。

for (int i = 0; i < 100; i++) {

  double mid = (l + r) / 2;
  if (pd(mid)) 
      r = mid; 
  else 
      l = mid;
}

记住!!常用:

 此题代码:

package main;

import java.io.*;
import java.math.*;
import java.util.*;

public class Main {
/*
典型的二分答案问题
1.我们要求一个数的N次根号,这个数连续  单调
*/
	public double mysqrt(double n,double m) {
		double l=0;double r=n;double eps=0.0000001/100;
		  while (l + eps < r) {
	          double mid = (l + r) / 2;

	          if (pd(mid, m,n))
	              r = mid;

	          else
	              l = mid;
	      }
		  return l;
	}
	public boolean pd(double a,double m,double n) {
		double c=1;
		while(m>0) {
			c=c*a;
			m--;
		}
		if(c>=n) return true;
		return false;
	}
	public static void main(String[] args) {
		Main t=new Main();
		Scanner sc=new Scanner(System.in);
		double n=sc.nextDouble();double m=sc.nextDouble();
		System.out.println(String.format("%.7f",t.mysqrt(n, m)));
	
	}
}

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值