算法学习 (门徒计划)4-1 单调队列及经典问题及经典例题 学习笔记

前言

(7.12,这是最近一课,即将赶上进度)
(本次依旧挑战最短学习时间,3倍耗时,15H以内)
(核心理念是详略得当,把握重点)

本篇为开课吧门徒计划第十讲4-1单调队列及经典问题及经典例题
(视频是标的第六章第1节: 2021.07.01 5-1 单调队列及经典问题)

本课难度进一步升高,更为抽象

本课学习的目标是:

  • 了解单调队列的性质和应用场景

学习总结(学完后记录):

  • 单调队列用于处理RMQ问题
  • 单调队列是一种抽象化的模型
  • (额外知识)在存储数据时,更应该存储能更多反应元素信息的值,比如原数据数组的下标
  • (额外知识)解题时,应该把握题目的核心思想,再决定从什么地方利用什么知识点来解,而不是对于题目套用某一个解题模板

单调队列

  • 用于维护区间最值

场景举例(RMQ)

定义RMQ函数RMQ(x,y)用于获取某数组下标x,y区间内的最小值。现在固定区间右端y=7,问:
对于下方这个数组:
arr[] = {3,1,4,5,2,9,8,12};//len = 8
最少记录几个元素就可以满足RMQ(x,7)的任何需求;
(换句话说,随着x的值变化,这个函数有几种返回情况)
(从而进一步思考,这几种返回情况x的取值范围是多少)
(再进一步思考,这几种情况的特征是什么)

可以发现:

  • 边界值下方元素在递增

由此可以得出,以这种方式,在一个震荡递增的数列中,实现了一个单调递增的数列arr_new[]={1,2,8,12};这个额外的结构是一个单调递增序列(视作单调队列的一种对外表现形式)

这个队列内部的值,都是区间(x,7)内的最小值。

应用-维护区间最值的方式

对于某一个震荡递增序列,现在需要以某种规则进行区间最值维护,此时规则是准备一个滑动窗口,从序列首部移动到末尾,并将窗口内的最值进行打印。

为了方便维护区间最值,在窗口内设计一个单调队列(本次为递增队列)。
此时进行如下规则:窗口新发现元素时,剔除一切队列内大于等于该元素的值,将改元素存入队列末尾

此外,单调队列还有一个规则,当某值从窗口内移出时,该值也将从单调队列中移出。该值就是窗口观察区间的最值。

(需要注意的是,已经移除窗口的元素,也就是不再队列内的元素不需要关注,也就是这个窗口移动最值对外输出的值,未必是单调递增的)

窗口内必须是一个单调递增数列

(如果期望将整个序列改为单调递增,需要多次运行窗口)

总结:单调队列用于处理RMQ问题(移动区间时元素最小值问题)

数据结构-自行设计单调队列

为满足最值管理,这个数据结构内在可能用堆进行实现,但是只用堆是不能实现新元素加入时只保留小于其的元素的。
(不能吗?)
如果这么设计堆,新元素一直进行入堆(入队),但是出堆时仅考虑,堆顶元素在原始数列的坐标是否应该离开窗口,离开则出堆(出队),不离开则继续移动窗口,并且判断堆顶元素是否早应该离开窗口,如果早应该离开(被剔除)则对外出堆时,不存入结果序列。

以我的理解,以这种方式设计滑动窗口,就能实现单调队列的效果。

(在java中,可以用优先队列来实现)
上方是我的思想,课上采用另一个方案

在课上是这么设计单调队列的,采用双端队列(双向链表)(下方逻辑距举例采用单调递增队列)。

  • 入队,尾部元素判断,剔除一切比新元素大的元素,然后执行入队
  • 出队,队首元素判断原始数列的坐标是否应该离开窗口是否应该离开窗口,如果应该离开就出队

并且双向链表内存储的元素可以简化为原始数列的下标。

(设计的比我的好啊…)

代码实现(java)

java中用双向链表(LinkedList,JDK1.6以上)来实现双向队列。
(后续学习中我发现应该该用ArrayDeque,这才是双向队列,但是思路是一样的,就不重复书写了)

        LinkedList <Integer > q = new LinkedList <Integer > ();
        //添加
        q.offer(1);
        //获取末尾元素
        q.peekLast();
        //获取头元素
        q.peek();
        //弹出末尾元素
        q.pollLast();
        //弹出头元素
        q.poll();

        System.out.println(q.pollLast());

基于这个双向链表,如果要实现单调队列主要需要进行特殊的入队操作,对此我准备两个函数以应对需求

  • 入参q:单调队列
  • 入参i:原始元素的下标
  • 入参nums:原始元素数组
	//递减队列入队,用于最大队列
    private void decrementQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]<nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }

    //递增队列入队,用于最小队列
    private void incrementalQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]>nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }

例题分析(略)

题1,(略)
题2,解题思路,用单调队列(递增),分别对两个数列进行过滤,如果某一刻过滤的结果出现了矛盾,也就是单调队列内的元素不相同了,则这一刻就可以确定p(但是这个队列应该多大呢,能多大就多大,只入队不出队,最后的大小就是P)

(例题来自oj,未可转载前,略过这一块)

总结

  • 总结:单调队列用于处理RMQ问题(移动区间时元素最小值问题)(这区间移动时可能扩展也可能缩小)
  • 作为队列:
    • 新元素入队时,需要进行单调性维护
    • 当队首元素下标超出观察窗口范围,将该元素出队
    • 元素性质,队首元素必然是最值(递增队列,为最小值;递减队列,为最大值)
  • 单调队列实际维护的是元素的生命周期(生命周期由原始数组下标,元素数值值决定)

经典例题

LeetCode 239. 滑动窗口最大值

链接:https://leetcode-cn.com/problems/sliding-window-maximum

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例 1:


输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

解题思路

本题在当初学3-1 快速排序(Quick-Sort)有初步接触过

在当时我想不到有什么合适的数据结构能方便我解这题 ,现在学习了单调队列,因此本题变的很简单了,本题用单调递减队列来解。设计窗口大小为3的单调递减队列,这个队列用双向链表来实现。

而每一次移动都执行入队,根据情况出队。每一次移动后都将队首元素存入结果集合(从空开始移动)

示例代码

(没有学之前的性能:1699ms,59.5MB)
(现在的性能:31ms,52.7MB)

class Solution {
    
    
    public int[] maxSlidingWindow(int[] nums, int k) {
    
    
        LinkedList <Integer > q = new LinkedList <Integer > ();
        int[] res = new int [nums.length-k+1];


        for(int i=0;i<nums.length;i++){
    
    
            if(q.peek()!=null&&q.peek()<i-k+1) q.poll();

            if(q.size()<k){
    
    
                decrementQueueAdd(q,i,nums);
                if(i>=k-1)
                    res[i-k+1] = nums[q.peek()];
                //System.out.println(q.peek());
            }
        }




        return res;
    }

    //递减队列入队
    private void decrementQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]<nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }
}

LeetCode 剑指 Offer 59 - II. 队列的最大值

链接:https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:
输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

示例 2:
输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]

解题思路

本题期望获得最大值,因此用单调递减队列即可
依然采用方法双向链表来实现数据的存储。

但是原始元素期望依旧有队列的性质,因此本题需要先封装2个数据结构,一个是原始队列,另一个是单调递减队列

  • 在入队时,分别向两种队列进行入队
  • 在出队时,原始队列自然出队,但是单调队列需要判定是否需要出队
  • 在返回最值时,返回单调队列的队首值

另外单调队列的入队规则为:新元素必须不大于队尾元素,否则清除队位元素只到满足规则,其中对于等于规则的判断是为了出队时进行服务

单调队列需要判定是否需要出队的规则为:当原始队列元素出队时,如果等于单调队列的队首值则一起出对,否则就不出队。
(这就是允许和队尾值相同的元素入队意义)
(另外java中对于队列的实现是LinkedList)

示例代码

(解题过程中遇到一个细节,java的Integer型数据不可直接比较,需要用intValue()来获取值)

class MaxQueue {
    
    

    private LinkedList <Integer> l ;

    private LinkedList <Integer> q ;

    public MaxQueue() {
    
    
        l = new LinkedList <Integer>();
        q = new LinkedList <Integer>();
    }
    
    public int max_value() {
    
    
        return q.peek()==null?-1:q.peek();
    }
    
    public void push_back(int value) {
    
    
        l.offer(value);
        while(q.peekLast()!=null&&q.peekLast()<value)q.pollLast();
        q.offer(value);
    }
    
    public int pop_front() {
    
    
        if(l.size() == 0)
            return -1;
        Integer res = l.poll();
        Integer qh = q.peek();
        if(res.intValue() == qh.intValue()){
    
    
            q.poll();
        }
        return res;
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

LeetCode 862. 和至少为 K 的最短子数组

链接:https://leetcode-cn.com/problems/shortest-subarray-with-sum-at-least-k

返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。
如果没有和至少为 K 的非空子数组,返回 -1 。

示例 1:
输入:A = [1], K = 1
输出:1

示例 2:
输入:A = [1,2], K = 4
输出:-1

解题思路

本题和知识点无关,仅考察抽象的窗口概念我的解法有漏洞但是依然先说一下我的思维

本题要返回的子数组的最短长度
如果将这个子数组看做一个窗口,就可以理解为满足条件的窗口的最小窄值
现在,假设有有这么一个窗口,那么这个窗口应该如何出现呢?
此处定义窗口的存在规则是窗口内元素和不小于K

首先固定左端为0,然后令右端从0开始向右扩张,由此将会有两种情况

  • 到某一个点,窗口满足存在规则,进入下一阶段
  • 到末尾,窗口未满足存在规则,返回-1

其次令左端从0开始向右扩张,由此也会有两种情况

  • 移动一步,窗口依然满足存在规则,此时窗口宽度减小
  • 移动一步,窗口不满足规则,因此取消移动,窗口宽度不变

再然后令右端向右扩张,也会有两种情况。

  • 移动一步,窗口依然满足存在规则,此时窗口宽度增加
  • 移动一步,窗口不满足规则,必须进一步
    最后动态的向右侧移动窗口,并记录过程中的窗口最窄值,作为题目答案

(以上是我的解法,课上是另一种,如下)

首先将原始序列生成前缀和序列,也就是新建一个数组,这个数组每一个元素都等于原始数组对应元素到之前元素的和。因此子数组的合值就是(sj-si)(其中si、j表示到下标i、j前缀和,并且j大于i)

对于前缀和序列(称为s)中的元素(si或sj),当j固定时,如果期望间距能更小,此时可用的si就应该满足一个递增的趋势,由此将所有之前的SI存入单调递增队列中,试图获取更近小的间距时,不断执行从队列出队的操作,直到临近不满足(sj-si>=k

此外如果j不固定,那么如果j每增大一重,为了满足题目的需求则i需要增大两重才能产生新的答案。

因此将全部元素时刻准备入队,一队为si,另一队为sj,都用单调递增队列来管理

运行时:

  • 先从sj出队,此时si为0,持续进行sj的出队直到满足窗口条件
  • 再从si出队,持续进行直到临近不满足窗口条件,记录当前的窗口宽度
  • 同时从si和sj出队,持续进行直到满足窗口条件,然后进行si出队尝试
  • 如果尝试成功,此时就是新的窗口宽度

(两种方案核心思路相同,是对于贪心算法的一种应用)
(我的思路是直接平移窗口,直接进行贪心求值,而课上的思路用单调队列使得窗口平移更好描述)
但是我认为,没有必要用单调队列,本题只是有关窗口思想的贪心算法,你们怎么看
(ok,讲师的思路也和我一致,是根据问题性质选择知识模板,由于本课是单调队列,所以临时用一下)
后续实现中我的方案发现了缺陷,由于只进行窗口两边的偏移,这导致如果有一些值为负数,就会形成负收益子集,这些子集在试图越过时会影响我的判断,导致需要重新进行子集的遍历来越过负收益区间)
(换而言之,如果本题所以数字均为正数,我的方案是可行的,但是由于有负数的存在,需要课上的方案)
(这个方案的优势在于用求和数列,包含了负收益区间的影响,使得判断时不再只是判断当前数,而是包含了所有的前面数。)

示例代码

(课上的方案)

class Solution {
    
    
    public int shortestSubarray(int[] nums, int k) {
    
    
        int [] sumNums = new int [nums.length+1];
        int chiLen =-1;
        sumNums[0] = nums[0];
        int pos = -1;
        for(int i=0;i<nums.length;i++){
    
    
            sumNums[i+1] = sumNums[i]+nums[i];
        }
        LinkedList <Integer> q = new LinkedList <Integer> ();


        for(int i=0;i<sumNums.length;i++){
    
    
            while (q.size()>0 && sumNums[i] - sumNums[q.peek()] >= k) {
    
    
                pos = q.poll();
            }
            
            if (pos != -1 && (i - pos < chiLen || chiLen == -1))  chiLen = i - pos;
            incrementalQueueAdd(q,i,sumNums);
        }
        return chiLen;
    }

    private void incrementalQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]>nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }
}

LeetCode 1438. 绝对差不超过限制的最长连续子数组

链接:https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit

给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。

如果不存在满足条件的子数组,则返回 0 。

示例 1:

输入:nums = [8,2,4,7], limit = 4
输出:2 
解释:所有子数组如下:
[8] 最大绝对差 |8-8| = 0 <= 4.
[8,2] 最大绝对差 |8-2| = 6 > 4. 
[8,2,4] 最大绝对差 |8-2| = 6 > 4.
[8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
[2] 最大绝对差 |2-2| = 0 <= 4.
[2,4] 最大绝对差 |2-4| = 2 <= 4.
[2,4,7] 最大绝对差 |2-7| = 5 > 4.
[4] 最大绝对差 |4-4| = 0 <= 4.
[4,7] 最大绝对差 |4-7| = 3 <= 4.
[7] 最大绝对差 |7-7| = 0 <= 4. 
因此,满足题意的最长子数组的长度为 2 。

示例 2:
输入:nums = [10,1,2,4,7,2], limit = 5
输出:4 
解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。

解题思路

本题由于期望计算极值,从而确保最大绝对差满足条件,因此需要一个管理区间极值的关系,因此设计一个单调递增队列管理最小值和单调递减队列管理最大值。

随后试图同步向两个队列中执行入队,此时窗口右端向右平移,当入队后,进行队首判断,如果判断不通过则进行出队,并且窗口左边向右平移。

关于队首元素出队顺序,优先出队两个队列中元素序号更小的元素(窗口左端临界元素),出队一旦开始除非满足队首判断,或者队空,否则不会停下。

持续进行入队操作,直到全部元素都进行入队,并在过程中持续记录窗口的宽度最大值。

(课上进行了2分操作,但是我不理解,并且课上的代码性能不佳,如果有空我另外学习一下其他高性能的方案)

示例代码

(无二分操作,25ms,59.1MB)

class Solution {
    
    
    public int longestSubarray(int[] nums, int limit) {
    
    
        LinkedList <Integer > maxQueue = new LinkedList <Integer >();
        LinkedList <Integer > minQueue =  new LinkedList <Integer >();
        int l = 0, r = 0, res = 0;
        while (r < nums.length) {
    
    
            decrementQueueAdd(maxQueue,r,nums);
            incrementalQueueAdd(minQueue,r,nums);
            r++;
            while ( nums[maxQueue.peek()] -nums[minQueue.peek()] > limit) {
    
    
                if (maxQueue.peek() == l) maxQueue.poll();
                if (minQueue.peek() == l) minQueue.poll();   
                l += 1;
            }
            res = Math.max(res, r - l);
        }
        return res;
    }

    private void decrementQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]<nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }

    //递增队列入队
    private void incrementalQueueAdd(LinkedList <Integer > q,int i,int[] nums){
    
    
        if(q!=null){
    
    
            while(q.peekLast()!=null){
    
    
                if(nums[q.peekLast()]>nums[i])
                    q.pollLast();
                else
                    break;
            }
            q.offer(i);
        }
    }
}

拓展例题-思维训练

下方的题目,基本和课程无关,有兴趣的可以和我一起做一下

LeetCode 513. 找树左下角的值 (广度搜索)

链接:https://leetcode-cn.com/problems/find-bottom-left-tree-value

给定一个二叉树,在树的最后一行找到最左边的值。

示例:

输入:

        1
       / \
      2   3
     /   / \
    4   5   6
       /
      7

输出:
7

解题思路

本题,看到二叉树,看到最后一行,我第一反应就是层序遍历,由于不全部遍历是不知道是否会漏子树的,因此广搜和深搜都可以(这不是上一课的知识点吗???)

(我采用广搜,代码略)

LeetCode 135. 分发糖果 (逻辑)

链接:https://leetcode-cn.com/problems/candy

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

每个孩子至少分配到 1 个糖果。
评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:
输入:[1,0,2]
输出:5
解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:
输入:[1,2,2]
输出:4
解释:你可以分别给这三个孩子分发 1、2、1 颗糖果。
     第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

解题思路

本题是一个顺序问题,分高的孩子必须比旁边分低的至少多一颗,但是如果分值一样,哎! 就可以只发一颗哦!
本题与其说是一个算法题,更像是一个人性题

由此本题只需要把握分数序列的增减趋势:

  • 增,下一位值加1。(减序列计数清空)
  • 平,下一位值变为1(减序列计数清空)
  • 减,先不判断,开始向后追寻,追寻时记录追寻的长度
  • 根据结果反向进行增推演计算最少需求,并将前一位和当前位所需进行比较,如果前一位能大于当前(满足减趋势)则不变,否则将前一位抬升直到满足需求

(根据减序列的长度,假设是n则前一位至少给与n+1个糖)
减序列的起始为当前开始下降的点,减序列的终点为最低点

(以上是我的思路,课上的思路为如下)

先从左向右计算合理的糖果值,再从右向左计算合理的糖果值,然后在两组合理的糖果值中每一位取最大值,生成最终结果。

(本质和我的的思路一致,但是结构上比我的更简单,我的解法是从左向右进行计算,当出现下降趋势时,插入从右向左)
(可能我的性能更高吧?因此我保留我的解法)

(代码简单,示例代码略)

(但是本题是一个困难题,是不是意味着我的解法不够优秀呢?我看一下其他人的解法是否有更好的方案,如果没有,则取消示例代码环节)

(学习完毕其他解法后,我意识到我的解法的确比课上的方案好,因为我不需要记录遍历的结果只需要单向的进行指针遍历,理论上空间复杂度为1,但是还有优化的空间,以下是我优化后的方案的补充说明)

本题只需要把握分数序列的增减趋势,每一组增序列的起点都是糖数为1(谷底),每一组减序列的终点都是糖数为1(谷底),因此根据每一组递增的长度inc和每一组递减序列的长度dec,就可以计算出除了峰顶外所需的全部糖数,随后根据这两个序列的长度来判断的峰值交界处(峰顶)所需的糖数。

简单距离描述为,如果为增3,减四,增2,这样的序列样式,则存在一处峰顶,该峰顶值根据最长坡决定,本次最长坡为减坡,则峰顶为4因此最终需要的糖数列为:1,2,4,3,2,1,2

(代码略)

LeetCode 365. 水壶问题 (广度搜索)

链接:https://leetcode-cn.com/problems/water-and-jug-problem

有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?

如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。

你允许:

装满任意一个水壶
清空任意一个水壶
从一个水壶向另外一个水壶倒水,直到装满或者倒空

解题思路

本题很有趣,应该是用递归来枚举。用水壶中持有水的量和水壶构成了一个问题求解树状态。
(考虑到状态和枚举应该快速用直觉生成应该用问题求解树来进行解答)

而问题求解树,有哪几个解题的优化技巧呢(复习一下)

  • 剪枝(不可能情况的排除)
  • 记忆化(子树重叠状况的排除)
  • 状态定义(决定树的表现形态)

本题主要使用记忆化技巧,也就是当出现某种水容量出现时,必然对解题无帮助,也可能导致死循环递归,因此进行优化最有效果。

另外搜索方式只要求能不能找到,所以深搜和广搜没有区别,但我认为广搜不需要用栈,因此空间性能可能更好,综上,本题用广搜。

最后考虑状态的转移方式:

  • 空,空,满1或满2
  • 空,不空,满1,或2入1一部分,或者2清空
  • 不空,空,上述反过来
  • 不空,不空, 满1或满2,或2入1一部分,或1入2一部分,或者任1清空

综上枚举完毕有16种可能。

或者从壶本体出发:

  • 装满自己
  • 放空自己
  • 把水倒到另一个壶里

单壶3种,合在一起6种,其中部分情况可能重叠(需要剪枝)

(java的值对我还是不知道怎么处理,通常是自定义一个类进行封装)
(本题操作略多,做一遍练手)

(做完了性能很差,为什么?)

示例代码

(1遍过,但是性能很差,1498ms,131、8MB)

class Solution {
    
    
    public boolean canMeasureWater(int jug1Capacity, int jug2Capacity, int targetCapacity) {
    
    
        LinkedList<Kettle> q = new LinkedList<Kettle>();
        Set <String > set = new HashSet<>();
        q.offer(new Kettle(0,0));
        while(q.size()>0){
    
    
            Kettle k = q.poll();

            if(!set.add(k.x+"+"+k.y)) continue;

            //System.out.println(k.x+"+"+k.y);
            if(k.x+k.y==targetCapacity) return true;
            q.offer(new Kettle(k.x,0));
            q.offer(new Kettle(k.x,jug2Capacity));
            q.offer(new Kettle(0,k.y));
            q.offer(new Kettle(jug1Capacity,k.y));

            int dx = k.x+k.y - jug1Capacity;
            int dy = k.x+k.y - jug2Capacity;
            if(dx>0)
                q.offer(new Kettle(jug1Capacity,dx));
            else
                q.offer(new Kettle(k.x+k.y,0));

            if(dy>0)
                q.offer(new Kettle(dy,jug2Capacity));
            else
                q.offer(new Kettle(0,k.x+k.y));
        }    
        return false;   
    }
}
class Kettle{
    
    
    int x,y;
    public Kettle(int X,int Y){
    
    
        x = X;y=Y;
    }
}

LeetCode 1760. 袋子里最少数目的球 (二分法)

链接:https://leetcode-cn.com/problems/minimum-limit-of-balls-in-a-bag

给你一个整数数组 nums ,其中 nums[i] 表示第 i 个袋子里球的数目。同时给你一个整数 maxOperations 。

你可以进行如下操作至多 maxOperations 次:

选择任意一个袋子,并将袋子里的球分到 2 个新的袋子中,每个袋子里都有 正整数 个球。
比方说,一个袋子里有 5 个球,你可以把它们分到两个新袋子里,分别有 1 个和 4 个球,或者分别有 2 个和 3 个球。
你的开销是单个袋子里球数目的 最大值 ,你想要 最小化 开销。

请你返回进行上述操作后的最小开销。

示例:

输入:nums = [2,4,8,2], maxOperations = 4
输出:2
解释:
- 将装有 8 个球的袋子分成装有 4 个和 4 个球的袋子。[2,4,8,2] -> [2,4,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,4,4,4,2] -> [2,2,2,4,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,4,4,2] -> [2,2,2,2,2,4,2] 。
- 将装有 4 个球的袋子分成装有 2 个和 2 个球的袋子。[2,2,2,2,2,4,2] -> [2,2,2,2,2,2,2,2] 。
装有最多球的袋子里装有 2 个球,所以开销为 2 并返回 2 。

解题思路

本题,理论上如果操作不限制,那么最后一定是1个。
另外需要了解到,只能分不能合并,并且最终影响答案的球做多的袋子,所以每一次都尽可能的应该让球最多的对半分。

如此本题应该是一个最值队列问题,用最大值队列来解,核心循环为一次出队两次入队,而最终结果为队首元素。

(但是实际上不是这样,现在考虑一种情况)

假设球数为9,允许分两次,则最大堆最小为多少?如果依照最大值队列的方式,最终结果是4,但是实际上答案应该为3,因此本题不可用最大值队列。

在实际上,应该是在均分的情况下,满足答案的球和分的关系是对(n+1)进行除法的关系(假设只有一袋球足够多,可以分n次)

因此假设某一袋球可用的分隔次数固定为K,则定义最小均分结果为X,那么这袋球在K次操作下可以分出比X更大的最大球数,但不能做到在K次下分出比X更小的最大球数(可以更差,但是不能更好)

由此可以生成一个函数,将每一堆球都分成不大于X的情况,所需要的最小操作次数k。如果k小于规定次数,则表示浪费开销,应该减少X,如果大于则违反规则,应增大X,但是等于X时也可能浪费开销。

因此,本题需要临界的X值,此时开销最低。

(这个函数很好设计,难点就在于如何优化X的取值)

这里可以采用一个二分思想,基准值先从1和现有数组的最大值之间寻找,基于二分的思想,如果失败则向上,如果成功则向下,当最终二分结束时,将结果作为新一轮的基准值进行以数值1为单位进行向下偏移。

最终实现临界点的值。

(本题是考察一种定位数值的策略,用二分算法进行数值定位,其中起到计算最小分割次数的函数仅仅是一种工具)

(代码略)

LeetCode 45. 跳跃游戏 II (抽象思维)

链接:https://leetcode-cn.com/problems/jump-game-ii

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

你的目标是使用最少的跳跃次数到达数组的最后一个位置。

假设你总是可以到达数组的最后一个位置。

示例 :

输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

解题思路

又是一道有趣的题目,一种办法是具象化问题求解树,以广搜的方式获得最少行动次数,这个方案的依据是本题期望知道最少步数。

另外本题的解题思路需要采用逆向思维,需要从终点出发寻找可以接受的上一个起跳点。由此进行状态的拓展,并且搜寻的策略应该为尽可能的选择更远的起跳点,并且记录这种行为,如果一个起跳点已经被使用,则不能使用(进行置0)

(这个依据的是广搜的深度就是最少步数,因此第二次到达一个点一定是更差的结果需要用记忆法排除掉子树重叠)

(以上是我的思路,下面是课上的解法)

本题是一个思维题。

从正向出发,由于每个数字仅是代表,最大跨越宽度,因此在某一个数字的最大跨越宽度内,其和范围内任意元素间距k和该元素的最大跨越宽度n之和才是这个数字实际上的真正能选择跨越的最大距离(k+n),并且这个选择的逻辑是可以随着每一个点进行推进的,因此当最终这个点可以包含到终点时,就可以获得答案

(显然是课上的思路好)
(这方面我是陷入到对知识点使用的习惯中了)

示例代码

(课上的思路)

class Solution {
    
    
    public int jump(int[] nums) {
    
    
        int res =0;
        if(nums.length<=1) return res;
        int now = 0;
        res++;
        while(1+now+nums[now]<nums.length){
    
    
            int end = now+nums[now];
            int i =now+1;
            int next = 0;
            for(;i<= end;i++){
    
    
                if(next+nums[next]<i+nums[i])
                    next = i;
            }
            //System.out.println(now);
            now = next;
            res++;
        }
        return res;
    }
}

LeetCode 93. 复原 IP 地址 (深度搜索)

链接:https://leetcode-cn.com/problems/restore-ip-addresses

给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 无效 IP 地址。

示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:
输入:s = "0000"
输出:["0.0.0.0"]

解题思路

本题是一个字符串分割的题目,需要把握的只有IP地址的规则。

但是具体怎么切割每一次都是有多重可能的,因此是一个需要枚举状态的解法。
(枚举状态,就和问题求解树相关)

(警惕思维定式,本题是否是只能进行分割尝试?是的,所以确实可以用问题求解树)

(问题求解树就很熟练了)

本题用可以深搜来解,进行递归,向下传递的内容为当前插入的结果,和剩余需要分割的次数。

最终将所有问题求解树的有效叶子节点,生成结论传递到结果数组中。

(可以添加剩余失败情况,用记忆化减少子树重叠)

(代码略)

LeetCode 46. 全排列 (递归)

链接:https://leetcode-cn.com/problems/permutations

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]

解题思路

本题的数字不会重复,所以只需要返回所有的枚举结果。

对于这种题目可以转换为问题求解树用深搜或者广搜来解。最后将所有有效叶子节点输出到结果集合。

(示例代码略)

(Java中有一个库函数能完成这个全排列的获取吗,没找到)

LeetCode 43. 字符串相乘 (大数基本运算)

链接:https://leetcode-cn.com/problems/multiply-strings

给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。

示例 1:
输入: num1 = "2", num2 = "3"
输出: "6"

示例 2:
输入: num1 = "123", num2 = "456"
输出: "56088"

说明:

  • num1 和 num2 的长度小于110。
  • num1 和 num2 只包含数字 0-9。
  • num1 和 num2 均不以零开头,除非是数字 0 本身。
  • 不能使用任何标准库的大数类型(比如 BigInteger)或直接将输入转换为整数来处理。

解题思路

本题就是自行设计一个方案实现大数相乘的效果,困难点在于设计一个方式存储乘法运行时的中间变量

对此根据字符串长度已知,所以只需要准备3个中间变量数组,用于保存两个数字的值和结果值,随后只需要进行循环遍历数组,进行每一位的相乘,并且将相乘的结果存入结果数组中(需要注意进位)。

最终将结果数组转换为字符串即可

(操作中为了方便可以让大数的低位存在数组索引小的位置,也是大数存储的一个技巧,倒着存,但是非要正着存也是可以的,只要自身能处理好对应关系)

(代码略)

结语

用时:

  • 概念 (课时+笔记整理)
    0.5H+0.7H
  • 习题 (课时+思路整理)
    3.7+0.5H
  • 代码 (有兴趣的题目手写代码)
    4.8H

总计10.2H,近似2.5倍耗时,进步了

本课学习过程中,感到吃力,举例困难,可能是我脑子糊涂,期望我能有所改进。
(课程中讲师也表示睡眠不足导致思维迟缓。因此,学习不可冒进,不是完成任务)

另外,本次我也发现如果我比较疲劳,那么一旦我想到了一个可行的解决方案(比如45题、365题),我就没有办法想到其他方案了,对此我期望下一次能有所改善。

并且发现有大量的题目,都在编码过程中出现了细节的失误,期望下一次能有所改善。

_φ(❐_❐✧ 人丑就要多读书

猜你喜欢

转载自blog.csdn.net/ex_xyz/article/details/118936790
今日推荐