该类题型一般采用二分法、回溯或两者组合求解的方法。
使用二分查找最大值最小化
划分为K个相等的子集
回溯求解
class Solution(object):
def canPartitionKSubsets(self, nums, k):
nums.sort()
target, v = sum(nums) // k, sum(nums) % k
if v or nums[-1] > target: return False
while nums and nums[-1] == target:
k -= 1
nums.pop()
return self.backtrack(nums, [0] * k, target)
def backtrack(self, nums, groups, target):
if not nums: return True
num = nums.pop()
for i in range(len(groups)):
if groups[i] + num <= target:
groups[i] += num
if self.backtrack(nums, groups, target): return True
groups[i] -= num
if not groups[i]: break
nums.append(num)
return False
爱吃香蕉的珂珂
二分法
class Solution:
import math
def minEatingSpeed(self, piles: List[int], h: int) -> int:
start, end = 1, max(piles) # 注意这里start必须从1开始,不能从min(piles)开始!!!
while start <= end:
mid = start + end >> 1
if self.bisearch(piles, mid) > h:
start = mid + 1
else:
end = mid - 1
return start
def bisearch(self, piles, k):
res = 0
for i in range(len(piles)):
res += (piles[i] + k - 1) // k # 两数相除向上取整:(a + b - 1) // b
return res
在D天内送达包裹的能力
class Solution:
def shipWithinDays(self, weights: List[int], D: int) -> int:
start, end = max(weights), sum(weights)
while start <= end:
mid = (start + end) >> 1
if self.countday(weights, mid) > D:
start = mid + 1
else:
end = mid - 1
return start
def countday(self, weights, target):
day = 1
current_w = 0
for w in weights:
current_w += w
if current_w > target:
day += 1
current_w = w
return day
制作m束花所需的最少天数
class Solution:
def minDays(self, bloomDay: List[int], m: int, k: int) -> int:
if m * k > len(bloomDay): return -1
start, end = min(bloomDay), max(bloomDay)
while start <= end:
mid = (start + end) >> 1
if self.funer(bloomDay, k, mid) < m:
start = mid + 1
else:
end = mid - 1
return start
def funer(self, bloomDay, k, day):
res = n = 0
for i in range(len(bloomDay)):
if bloomDay[i] <= day:
n += 1
if n == k:
res += 1
n = 0
else: n = 0
return res
完成所有工作的最短时间
采用二分+回溯的方法
class Solution:
def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
start, end = max(jobs), sum(jobs)
while start <= end:
mid = (start + end) >> 1
if self.check(jobs, mid, k):
end = mid - 1
else:
start = mid + 1
return start
def funer(self, nums, groups, limit):
if not nums: return True
num = nums.pop()
for i in range(len(groups)):
if groups[i] + num <= limit:
groups[i] += num
if self.funer(nums, groups, limit): return True
groups[i] -= num
if not groups[i]: break
nums.append(num)
return False
def check(self, jobs, limit, k):
nums = sorted(jobs) # 注意:此处必须用sorted函数,而不能使用sort()!!!需要复制一份,因为后面nums要改变
groups = [0] * k
return self.funer(nums, groups, limit)
分割数组的最大值
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
n = len(nums)
if n == 1: return nums[0]
# 注意这里的start起始值为max为不是min(会出错,如[1,1,4],m = 2)
# 因为是最小化最大值,则最大值最小都必须为nums中最大的那个数!!!
start, end = max(nums), sum(nums)
while start <= end:
mid = (start + end) >> 1
if self.binsearch(nums, mid) > m:
start = mid + 1
else:
end = mid - 1
return start
def binsearch(self, nums, n):
res = counter = 0
for i in range(len(nums)):
res += nums[i]
if res > n:
counter += 1
res = nums[i]
return counter + 1
该题与华为21年秋招的机试第三题类似,题目:
最优子序列:现有一个序列长度为m,我们要将其分解成k个子序列,1<=k<=m<=500,序列由整数组成,整数小于10^7。要求这k个子序列和的最小值尽可能大。在有多解的情况下,优先s(1)序列最大,如果s(1)相同,则s(i+1)依次往下排(这一说明,就要求下面的代码需要从后往前遍历)。
def make_parts(a,k,x):
parts=[]
num_part=0
i=len(a)-1
#从后往前遍历,目的是保证前面的子序列和能更大
while i >= 0:
tmp_sum = 0
part = []
j = i
#划分子区域
while j >= 0 and tmp_sum < x:
part.append(a[j])
tmp_sum += a[j]
j -= 1
i = j
#x过大,直接返回0
if tmp_sum < x:
return None
num_part += 1
#如果x过小,那么划分完后会有多余的区域没有并入,将剩余区域并入s(1)序列
if num_part == k:
while i >= 0:
part.append(a[i])
i -= 1
parts.append(part)
#x偏大,不能划分k个区域
if num_part < k:
return None
return parts
def search(a, k):
start, end = 0, sum(a)
while start <= end:
mid = (start + end) >> 1
if make_parts(a, k, mid):
start = mid + 1
else:
end = mid - 1
# return start - 1 # 如果返回的是最小值最大化的值,需要返回start - 1,这一点与最大值最小化值不一样!
return make_parts(a, k, start - 1)
a = [100, 200, 300, 400, 500, 600, 700, 800, 900]
k = 3
out = search(a, k)
print(out) # out: [[900, 800], [700, 600], [500, 400, 300, 200, 100]]
这两题类似于对偶问题,第一个是求最大值最小化问题,第二个是求最小值最大化问题,但两者并不等价,编写功能函数也不一样,二分代码最后返回的值也不一样(前者只需要return start,后者需要return start - 1)!
至于为什么后者需要return start - 1本人也琢磨不透,希望懂的能够评论告诉我呀~
与之相关的最大化最小值/最小化最大值题:
建立火车站
N, K = map(int, input().split())
nums = list(map(int, input().split()))
nums.sort()
def funer(nums, k, K):
s = 0
for i in range(1, len(nums)):
d = nums[i] - nums[i - 1] # 两个站点之间的距离
# 最小化的最大值,也是最大值,如果出现了比这个值还大的,那就往里面塞车站,让它变小
if d > k:
s += (d - 1) // k # (d + k - 1) // k - 1 == (d - 1) // k
return s > K
def binsearch(nums, K):
start, end = 1, nums[-1]
while start <= end:
mid = (start + end) >> 1
if funer(nums, mid, K):
start = mid + 1
else:
end = mid - 1
return start
out = binsearch(nums, K)
print(out)
class Solution:
def binsearch(self, nums, C):
start, end = 1, nums[-1]
while start <= end:
mid = (start + end) >> 1
if self.funer(nums, mid, C): # 说明设置的间隔小了,还可以设置更大一点
start = mid + 1
else:
end = mid - 1
return start - 1
def funer(self, nums, m, C):
s, counter = 0, 1 #在模拟放置的时候为了放置的尽可能的稀疏,要从第一个位置开始放
for i in range(1, len(nums)):
s += nums[i] - nums[i - 1]
if s >= m: #似乎这里不取等号的话,在binsearch函数的返回中可以直接return start?反正下面leetcode的一题这两者是等价的。
counter += 1
s = 0
return counter >= C
N, C = map(int, input().split())
nums = []
for _ in range(N):
nums.append(int(input()))
nums.sort()
f = Solution()
out = f.binsearch(nums, C)
print(out)
class Solution:
def maxDistance(self, position: List[int], m: int) -> int:
position.sort()
start, end = 1, position[-1]
while start <= end:
mid = (start + end) >> 1
if self.funer(position, m, mid):
start = mid + 1
else:
end = mid - 1
return start
def funer(self, position, m, d):
s, counter = 0, 1
for i in range(1, len(position)):
s += position[i] - position[i - 1]
if s > d: # 不取等号,所以maxDistance函数的返回值是start,否则为start - 1
counter += 1
s = 0
return counter >= m
class Solution:
def minTime(self, time: List[int], m: int) -> int:
if m >= len(time) or len(time) == 1: return 0
start, end = 0, sum(time)
while start <= end:
mid = (start + end) >> 1
if self.funer(time, m, mid):
start = mid + 1
else:
end = mid - 1
return start
def funer(self, nums, m, mid):
cur_sum, cur_max, counter = 0, nums[0], 1
for i in range(1, len(nums)):
if cur_sum + min(cur_max, nums[i]) <= mid:
cur_sum += min(cur_max, nums[i])
cur_max = max(cur_max, nums[i])
else:
counter += 1
cur_max = nums[i]
cur_sum = 0
return counter > m
通过以上似乎发现,不管是求最大值最小化还是最小值最大化问题,binsearch函数的返回值都可以是:return start,当然这个前提是在funer函数中判断语句不取等号。但是发现,在求最大值最小化的题中,这两者并不等价,如果funer函数取了等号,return start,答案报错(leetcode410题可自行验证)。
对于这个问题,似乎就是二分查找里面的查找左边界/右边界的区别,可以参照一下讲解:
参考链接
通过以上分析可以得出:求最大值最小化,为二分查找寻找左侧边界;求最小值最大化,为二分查找寻找右侧边界