二分答案是binary search里面比较难的题型,也就是,直接求一个最值很难,但是我们可以很简单的在解空间做判定性问题,比如能不能,行不行,大了还是小了,return true or false,从而缩小解空间,记住,这种方法有个前提条件就是有单调递增或者递减的性质,才能用。这也是binary search使用的条件;
Wood Cut
有一些原木,现在想把这些木头切割成一些长度相同的小段木头,需要得到的小段的数目至少为 k
。当然,我们希望得到的小段越长越好,你需要计算能够得到的小段木头的最大长度。
Example
样例 1
输入:
L = [232, 124, 456]
k = 7
输出: 114
Explanation: 我们可以把它分成114cm的7段,而115cm不可以
思路:直接求,比较难求,但是我们可以二分解空间;
K 800 400 ..... 8 7 6
L 1 2 ...... x-1 x x+1
也就是如果L持续增大,那么能够切出来的整块在减少,那么就有个递增和递减的关系,L越大,K越小;反之亦然;
那么就可以用二分法;把一个求解最值的问题,变成一个判定性问题;
public class Solution {
/**
* @param L: Given n pieces of wood with length L[i]
* @param k: An integer
* @return: The maximum length of the small pieces
*/
public int woodCut(int[] L, int k) {
if(L == null || L.length == 0 || k <= 0) {
return 0;
}
int maxlen = 0;
for(int i = 0; i < L.length; i++) {
maxlen = Math.max(maxlen, L[i]);
}
int start = 0; int end = maxlen;
while(start + 1 < end) {
int mid = start + (end - start) / 2;
if(canCut(L, mid) >= k){
start = mid;
} else {
end = mid;
}
}
if(canCut(L, end) < k) {
return start;
}
return end;
}
private int canCut(int[] L, int s) {
int count = 0;
for(int i = 0; i < L.length; i++) {
count += L[i]/s;
}
return count;
}
}
Copy Books
给定 n
本书, 第 i
本书的页数为 pages[i]
. 现在有 k
个人来复印这些书籍, 而每个人只能复印编号连续的一段的书, 比如一个人可以复印 pages[0], pages[1], pages[2]
, 但是不可以只复印 pages[0], pages[2], pages[3]
而不复印 pages[1]
.
所有人复印的速度是一样的, 复印一页需要花费一分钟, 并且所有人同时开始复印. 怎样分配这 k
个人的任务, 使得这 n
本书能够被尽快复印完?
返回完成复印任务最少需要的分钟数.
Example
样例 1:
输入: pages = [3, 2, 4], k = 2
输出: 5
解释: 第一个人复印前两本书, 耗时 5 分钟. 第二个人复印第三本书, 耗时 4 分钟.
样例 2:
输入: pages = [3, 2, 4], k = 3
输出: 4
解释: 三个人各复印一本书.
Challenge
时间复杂度 O(nk)
思路:
二分答案,直接求不好求,那么我们就问,如果给你无穷多的人,你需要多久? Max(Length of page[i]), 如果给你1个人,你需要多久,sum (Pages[i]) ,那么问题来了,你需要求给你k个人情况下的,你需要多久。很显然,这又是个递增递减的关系;
人越多,需要的时间越少,人越少需要的时间越多;
public class Solution {
/**
* @param pages: an array of integers
* @param k: An integer
* @return: an integer
*/
public int copyBooks(int[] pages, int k) {
if(pages == null || pages.length == 0 || k <= 0) {
return 0;
}
int upper = 0;
int lower = 0;
for(int i = 0; i < pages.length; i++) {
upper += pages[i];
lower = Math.max(lower, pages[i]);
}
int start = lower, end = upper;
while(start + 1 < end) {
int mid = start + (end - start) / 2;
if(canCopy(pages, mid) > k) {
start = mid;
} else {
// canCopy(pages, mid) <= k;
end = mid;
}
}
if(canCopy(pages, start) > k) {
return end;
}
return start;
}
private int canCopy(int[] pages, int timelimit) {
int count = 0;
int sum = 0;
int i = 0;
while(i < pages.length) {
// 用sum + pages[i]来判断下一个是否满足条件;
while(i < pages.length && sum + pages[i] <= timelimit) {
sum += pages[i];
i++;
}
count++;
sum = 0;
}
return count;
}
}
Copy Books II
给定 n
本书, 每本书具有相同的页数. 现在有 k
个人来复印这些书. 其中第 i
个人需要 times[i]
分钟来复印一本书. 每个人可以复印任意数量的书. 怎样分配这 k
个人的任务, 使得这 n
本书能够被尽快复印完?
返回完成复印任务最少需要的分钟数.
Example
样例 1:
输入: n = 4, times = [3, 2, 4]
输出: 4
解释: 第一个人复印 1 本书, 花费 3 分钟. 第二个人复印 2 本书, 花费 4 分钟. 第三个人复印 1 本书, 花费 4 分钟.
思路:还是二分答案的思想,跟I很类似,给定时间T内,能否用K个人抄完N本书,如果K不变,T就越小,那么抄的书也就<N,如果T越大,那么抄的书数目就>N. CopyBooks就是在timelimit的条件下,所有人都拼命同时抄书,看能抄写多少本,然后跟N比较,从而判断时间是多了还是少了。
public class Solution {
/**
* @param n: An integer
* @param times: an array of integers
* @return: an integer
*/
public int copyBooksII(int n, int[] times) {
if(n < 0 || times == null || times.length == 0) {
return 0;
}
// 求速度最慢的人,也就是时间花的最多的人;
int max = times[0];
for(int i = 1; i < times.length; i++) {
max = Math.max(max, times[i]);
}
int start = 1;
int end = n * max; // 最慢的一个人去抄N本书;
while(start + 1 < end) {
int mid = start + (end - start) / 2;
if(copyBooks(times, mid) >= n) {
end = mid;
} else {
start = mid;
}
}
if(copyBooks(times, start) < n) {
return end;
}
return start;
}
private int copyBooks(int[] times, int timelimit) {
int count = 0;
for(int i = 0; i < times.length; i++) {
count += timelimit / times[i];
}
return count;
}
}
Sqrt(x)
实现 int sqrt(int x)
函数,计算并返回 x 的平方根。
Example
样例 1:
输入: 0
输出: 0
样例 2:
输入: 3
输出: 1
样例解释:
返回对x开根号后向下取整的结果。
样例 3:
输入: 4
输出: 2
思路:binary search,注意0的判断和long防止溢出;标准模板写的。
public class Solution {
/**
* @param x: An integer
* @return: The sqrt of x
*/
public int sqrt(int x) {
if(x < 0) {
return -1;
}
if(x == 0) {
return 0;
}
long start = 1; long end = x; // long防止溢出;
while(start + 1 < end) {
long mid = start + (end - start) / 2;
if(mid * mid > x) {
end = mid;
} else {
start = mid;
}
}
if(end * end > x) {
return (int)start;
} else {
return (int)end;
}
}
}
Sqrt(x) II
实现 double sqrt(double x)
并且 x >= 0
。
计算并返回x开根后的值。
Example
例1:
输入: n = 2
输出: 1.41421356
例2:
输入: n = 3
输出: 1.73205081
思路:还是二分,但是注意特殊处理一下,如果数字属于(0,1), end的值取1.0,因为0.1的sqrt为0.316,这个大于了0.1的范围;
public class Solution {
/**
* @param x: a double
* @return: the square root of x
*/
public double sqrt(double x) {
if(x < 0) {
return -1;
}
if(x == 0) {
return 0;
}
double start = 0;
// 数字属于(0,1)返回,上界需要取1,比如0.1 sqrt是0.3,超过了0.1;上界要超过x;
double end = Math.max(x, 1.0);
double eps = 1e-10;
while(start + eps < end) {
double mid = start + (end - start) / 2;
if(mid*mid > x) {
end = mid;
} else {
start = mid;
}
}
if(end*end > x) {
return start;
}
return end;
}
}
Find the Duplicate Number
给出一个数组 nums
包含 n + 1
个整数,每个整数是从 1
到 n
(包括边界),保证至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
Example
样例 1:
输入:
[5,5,4,3,2,1]
输出:
5
样例 2:
输入:
[5,4,4,3,2,1]
输出:
4
Notice
1.不能修改数组(假设数组只能读)
2.只能用额外的O(1)的空间
3.时间复杂度小于O(n^2)
4.数组中只有一个重复的数,但可能重复超过一次
思路:发现规律,P如果是重复的数,
如果<=P的个数>P,那么重复的数就是end = P,重复的数在左边;
如果<=P的个数,小于P,那么start = P, 重复的数在右边
public class Solution {
/**
* @param nums: an array containing n + 1 integers which is between 1 and n
* @return: the duplicate one
*/
public int findDuplicate(int[] nums) {
if(nums == null || nums.length == 0) {
return 0;
}
int min = 0; int max = 0;
for(int i = 0; i < nums.length; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
int start = min; int end = max;
while(start + 1 < end) {
int mid = start + (end - start) / 2;
if(count(nums, mid) > mid) { // 注意是重复的数count是大于自己;
end = mid;
} else {
// count(nums, mid) < mid;
start = mid;
}
}
if(count(nums, start) > start) { // 注意是重复的数count是大于自己;
return start;
}
return end;
}
private int count(int[] A, int num) {
int count = 0;
for(int i = 0; i < A.length; i++) {
if(A[i] <= num) {
count++;
}
}
return count;
}
}
Maximum Average Subarray II
给出一个整数数组,有正有负。找到这样一个子数组,他的长度大于等于 k
,且平均值最大。
Example
例1:
输入:
[1,12,-5,-6,50,3]
3
输出:
15.667
解释:
(-6 + 50 + 3) / 3 = 15.667
例2:
输入:
[5]
1
输出:
5.000
Notice
保证数组的大小 >= k
思路:直接求,不要计算,那么我们发现题目要求最大值,并且average的长度> k,那么我们先不管长度,我们先问,subarray(i,j)能够求的最大值会是多少;
(ai + .. + aj) / (j-i+1) >= avg,这个是否成立,那么转换为(ai - avg) + .. ( aj - avg) >=0
那么问题转换为B[i] 组数里面,有没有B[j] - B[i] , j - i + 1 >= k 也就是 i <= j - k + 1; 使得B[j] - B[i] >=0
那么就是一个解空间的搜索问题了,上面的B数组可以写成了一个canAverage的函数,那么就是求能否有avg,使得k
因为解空间肯定是 从小到大的avg,刚开始是可以,后面不行;
有则start = mid 否则 end = mid;
public class Solution {
/**
* @param nums: an array with positive and negative numbers
* @param k: an integer
* @return: the maximum average
*/
public double maxAverage(int[] nums, int k) {
if(nums == null || nums.length == 0 || k <= 0) {
return 0.0;
}
double min = Integer.MAX_VALUE;
double max = Integer.MIN_VALUE;
for(int i = 0; i < nums.length; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
// 注意题目有double,output有精度要求,那么这里binary search就有精度要求;
double start = min; double end = max;
while(start + 1e-5 < end) {
double mid = start + (end - start) / 2;
if(canFind(nums, mid, k)){
start = mid;
} else {
end = mid;
}
}
if(canFind(nums, end, k)) {
return end;
}
return start;
}
private boolean canFind(int[] nums, double T, int k) {
double rightsum = 0.0;
double leftsum = 0.0;
double minleftsum = 0.0;
// 先算[0,k-2];
for(int j = 0; j < k -1; j++) {
rightsum += nums[j] - T;
}
// 从第k-1开始算;也就是满足第k个了;
for(int j = k - 1; j < nums.length; j++) {
rightsum += nums[j] - T;
if(rightsum - minleftsum >= 0) {
return true;
}
// j - i + 1 >= k --> i <= j - k + 1;
// index一定是推倒出来的,不是凭空想象的;
leftsum += nums[j - k + 1] - T;
minleftsum = Math.min(minleftsum, leftsum);
}
return false;
}
}