遇到「最值问题」还在无脑动态规划?二分法考虑一下呗

目录

  1. 前言
  2. 二分法基础及变种结构
  3. 小试牛刀
  4. 打怪升级
  5. 出师试炼

前言

通常来讲,遇到「最值问题」通用的方法都是动态规划,而有一类「最值问题」能够用其余方法更加巧妙、简单方便的解决,这类问题的常见问法是「使……最大值尽量小」。java

这类问题也是大厂笔试面试常见题型,2020 年美团笔试题、字节面试题中都出现过。

这类问题有三大特征:面试

  1. 求最值(通常是最小值)
  2. 值的搜索空间是线性的
  3. 对该值有一个限制条件,而且若 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)$,而且思路很是简洁,代码实现也极其简单。spa

二分法基础及变种结构

在讲具体的方法以前,本汪先和你们一块儿回顾下二分法,熟悉二分法的同窗能够直接跳过这部分。设计

最先接触二分思想的地方应该是在二分搜索中。(本质上相似快排中的 partition 思想)code

二分法是在有序线性搜索空间内,利用有序性,每次搜索经过排除一半的剩余搜索空间,使 $O(n)$ 级别的线性搜索优化到 $O(logn)$ 级别的方法。视频

image
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">二叉树:我都会二分,不会还有人不会二分法吧?不会就让往西汪那小子教你</center> blog

二分法的基本代码很是简单,这里就很少提了,下面用类 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 个子数组各自和相比较,都要大于或者等于它们。

「搜索空间」、「检查条件」都找到了,下面闭着眼睛套结构吧~

image
<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 为整数)。

image

<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;
    }
}

出师试炼

我很是喜欢看我文章的小伙伴,个个都是人才,说话又好听,脑瓜子又聪明,还很主动的给我文章点赞。我相信砍翻两个小怪以后,大家已是这类问题的专家了,下面就给几道题目供大伙随便玩玩。

image
<center style="font-size:14px;color:#C0C0C0;text-decoration:underline">超喜欢往西汪的文章的</center>

关卡 1

你将会得到一系列视频片断,这些片断来自于一项持续时长为 T 秒的体育赛事。这些片断可能有所重叠,也可能长度不一。

视频片断 clips[i] 都用区间进行表示:开始于 clips[i][0]并于 clips[i][1] 结束。咱们甚至能够对这些片断自由地再剪辑,例如片断 [0, 7] 能够剪切成 [0, 1] + [1, 3] + [3, 7] 三部分。

咱们须要将这些片断进行再剪辑,并将剪辑后的内容拼接成覆盖整个运动过程的片断([0, T])。返回所需片断的最小数目,若是没法完成该任务,则返回 -1 。

关卡 2

牛牛有 n 件带水的衣服,干燥衣服有两种方式。

1、是用烘干机,能够每分钟烤干衣服的 k 滴水。

2、是天然烘干,每分钟衣服会天然烘干 1 滴水。

烘干机比较小,每次只能放进一件衣服。

注意,使用烘干机的时候,其余衣服仍然能够保持天然烘干状态,如今牛牛想知道最少要多少时间能够把衣服全烘干。

关卡 3

你太强了!我已经没有更多的题目给你玩了。你能够凭借这套武功闯荡江湖了!

……

……

……

等等,我说笑呢。你这小身板,要不先进个人其余训练营再练练呗?

啥,你问个人其余训练营在哪里?动动小手,点个关注,新的训练营已经在建设了,嘿嘿。

咳咳,暗示的很明显了<center style="font-size:14px; color:#C0C0C0">咳咳,暗示的很明显了</center>

相关文章
相关标签/搜索