一般来说,遇到「最值问题」通用的方法都是动态规划,而有一类「最值问题」可以用其他方法更加巧妙、简单方便的解决,这类问题的常见问法是「使……最大值尽可能小」。
这类问题也是大厂笔试面试常见题型,2020 年美团笔试题、字节面试题中都出现过。
这类问题有三大特征:
x
满足条件,则 [x, +∞)
都满足条件,反之 (-∞, x]
都不满足条件。
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">你说的我都懂,可问题是,这是啥意思呢?叉会腰,冷静一下。</center>
为了方便大家理解这类问题到底是个什么玩意儿,本汪在这里列出 leetcode 上的一道题目作为例子:
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
不熟悉二分法的同学初遇此题,看到关键词“分割成 m 份”、“最小”,就想到了动态规划。诚然此题具备了使用动态规划的一切前提条件,也确实可以通过动态规划做出来,但其时间复杂度为 $O(n^2×m)$ , 空间复杂度为 $O(n*m)$.
如果你看了本汪的这篇文章,学会了用二分法解决这一类问题,那么时间复杂度可以优化到 $O(n * logx)$, 空间复杂度可以优化到 $O(1)$,并且思路非常简洁,代码实现也极其简单。
在讲具体的方法之前,本汪先和大家一起回顾下二分法,熟悉二分法的同学可以直接跳过这部分。
最早接触二分思想的地方应该是在二分搜索中。(本质上类似快排中的 partition 思想)
二分法是在有序线性搜索空间内,利用有序性,每次搜索通过排除一半的剩余搜索空间,使 $O(n)$ 级别的线性搜索优化到 $O(logn)$ 级别的方法。
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">二叉树:我都会二分,不会还有人不会二分法吧?不会就让往西汪那小子教你</center>
二分法的基本代码非常简单,这里就不多提了,下面用类 java 语言给出其针对「最值问题」的变种结构:
// nums 数组是搜索空间,m 是限制条件 // check(x, m) 表示在搜索空间 nums 中的点 x,满足了限制条件 m public int binary(int[] nums, int m){ 初始化 l, r 为搜索空间的起始点和终点 while(l < r){ int mid = (l + r) >> 1; //二分,一次搜索可以排除 mid 的左边或者右边(剩余搜索空间的一半) if(check(mid, m)) r = mid; //因为这一类最值问题要找的是最小,mid 满足条件了,(mid, r] 就不是最小的答案了 else l = mid + 1; // mid 不满足条件,根据有序性,[l, mid] 里的数全不满足条件 } return r; }
根据结构我们可以看出,遇到这类问题的时候只需要找到「搜索空间」、「检查条件」,然后套用结构就能轻轻松松地解决问题。
下面就让我们来用二分法解决刚刚提到的问题。
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
前文提及,解决本题只需要找到「搜索空间」、「检查条件」,剩下的就是闭着眼睛套结构的事儿了。
先找「搜索空间」,因为此类问题的搜索空间都是线性的,所以找到了起始点和终点,也就找到了搜索空间。
看问题描述 “子数组各自和的最大值最小”,也就是说我们搜索的点是 "子数组各自和的最大值",那么搜索空间的起始点是数组 nums 中的最大值,即 min(nums)
,搜索空间的终点是数组 nums 中的所有元素之和,即 sum(nums)
。因此我们找到了搜索空间,为 [min(nums), sum(nums)]
.
再看「检查条件」。给定搜索空间 nums,和点 x,判断 x 是否满足限制条件 m。
本题的条件是“子数组各自和的最大值”,也就是说 x 和 m 个子数组各自和相比较,都要大于或者等于它们。
「搜索空间」、「检查条件」都找到了,下面闭着眼睛套结构吧~
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">(瑟瑟发抖.jpg) 好的,这就上代码</center>
class Solution { // 这个方法就是直接套结构 public int splitArray(int[] nums, int m) { long l = 0, r = 0; for(int num: nums) {r += num; l = Math.max(l, num);} // 初始化 l 和 r while(l < r) { long mid = (l + r) >> 1; if(check(nums, mid, m)) r = mid; else l = mid + 1; } return (int)r; } public boolean check(int[] nums, long a, int m) { // 给定搜索空间中的点 a,看它是否大于等于所有子数组的各自和 int cnt = 1; long sum = 0; for(int n: nums) { sum += n; if(sum > a) { sum = n; cnt ++; if(cnt > m) return false; } } return true; } }
咱们再来看一道题,小伙伴们可以先自己思考,有思路的话自己动手实现下。再看看本汪给出的思路,加深理解。
珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。
珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">往西汪:我请你们吃香蕉,可以给我点个赞吗</center>
老规矩,先找「搜索空间」,再找「检查条件」,最后闭着眼睛套结构。
「搜索空间」:H 小时内必须吃完 sum(piles)
根香蕉,所以初始点为 sum(piles) / H
,一次最多只能吃一堆,所以终点为 max(piles)
。因此,搜索空间为 [sum(piles) / H, max(piles)]
。
「检查条件」:“在 H 小时内吃掉所有香蕉”,因此以 K 个 / 小时
的速度吃完香蕉的用时要小于等于 H。
下面就闭着眼睛套结构吧。
class Solution { public int minEatingSpeed(int[] piles, int H) { int l = 1, r = 0; // 这里本汪为了代码的简洁性,在不影响时间复杂度的情况下,直接让初始点为 1 for(int i: piles) r = Math.max(r, i); // 一次最多吃一堆 while(l < r){ int mid = (l + r) >> 1; if(check(piles, mid, H)) r = mid; else l = mid + 1; } return r; } boolean check(int[] piles, int K, int H){ int t = 0; for(double i: piles){ t += Math.ceil(i / K); // 一堆香蕉共计 i 个,需要 ⌈i / K⌉ 个小时吃完 if(t > H) return false; // 警卫回来了还没吃完 } return true; } }
我非常喜欢看我文章的小伙伴,个个都是人才,说话又好听,脑瓜子又聪明,还很主动的给我文章点赞。我相信砍翻两个小怪之后,你们已经是这类问题的专家了,下面就给几道题目供大伙随便玩玩。
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">超喜欢往西汪的文章的</center>
你将会获得一系列视频片段,这些片段来自于一项持续时长为 T 秒的体育赛事。这些片段可能有所重叠,也可能长度不一。
视频片段 clips[i]
都用区间进行表示:开始于 clips[i][0]
并于 clips[i][1]
结束。我们甚至可以对这些片段自由地再剪辑,例如片段 [0, 7] 可以剪切成 [0, 1] + [1, 3] + [3, 7] 三部分。
我们需要将这些片段进行再剪辑,并将剪辑后的内容拼接成覆盖整个运动过程的片段([0, T])。返回所需片段的最小数目,如果无法完成该任务,则返回 -1 。
牛牛有 n 件带水的衣服,干燥衣服有两种方式。
一、是用烘干机,可以每分钟烤干衣服的 k 滴水。
二、是自然烘干,每分钟衣服会自然烘干 1 滴水。
烘干机比较小,每次只能放进一件衣服。
注意,使用烘干机的时候,其他衣服仍然可以保持自然烘干状态,现在牛牛想知道最少要多少时间可以把衣服全烘干。
你太强了!我已经没有更多的题目给你玩了。你可以凭借这套武功闯荡江湖了!
……
……
……
等等,我说笑呢。你这小身板,要不先进我的其他训练营再练练呗?
啥,你问我的其他训练营在哪里?动动小手,点个关注,新的训练营已经在建设了,嘿嘿。
<center style="font-size:14px; color:#C0C0C0">咳咳,暗示的很明显了</center>