数据结构与算法 #18 下跳棋,极富想象力的同向双指针模拟

数据结构,算法,跳棋,极富,想象力,同向,指针,模拟 · 浏览次数 : 159

小编点评

**前言** 本文为数据结构与算法系列文章的第 18 部分,介绍了一种名为 `numMovesStonesII` 的算法,该算法用于计算给定数组 `stones` 中不同大小的石头可以在滑动窗口中有多少个。 **算法简介** 该算法基于以下步骤: 1. 将数组 `stones` 排序。 2. 确定最大移动次数 `max1` 和最小移动次数 `minOp`。 3. 遍历数组 `stones` 并以右指针 `j` 为索引。 4. 对于每个右指针位置,如果窗口大小 `n - (j - i + 1)` 符合条件,则更新 `minOp` 以最小值。 5. 如果窗口大小不符合条件,则更新 `minOp` 以 `n - (j - i + 1)`。 6. 返回 `minOp` 和 `maxOp` 的值。 **复杂度分析** 时间复杂度为 `O(nlgn)`,其中 `n` 是数组长度,`l` 是窗口大小。瓶颈在于排序空间复杂度 `O(lgn)`。 **代码** ```python def numMovesStonesII(stones: List[int]) -> List[int]: n = len(stones) # 排序 stones.sort() # 最大移动次数 max1 = stones[n - 1] - stones[1] + 1 - (n - 1) # 最小移动次数 minOp = n # 遍历数组,寻找窗口大小最小的位置 j = 0 for i in range(0, n): while j < n - 1 and stones[j + 1] - stones[i] + 1 <= n: j += 1 if n - (j - i + 1) == 1: minOp = min(minOp, 2) else: minOp = min(minOp, n - (j - i + 1)) return [minOp, maxOp] ``` **总结** 该算法利用排序和动态编程来有效地计算石头可以在滑动窗口中有多少个的移动次数。

正文

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 18 篇文章,完整文章目录请移步到文章末尾~

前言

这道题是 LeetCode 上的 1040. 移动石子直到连续 II,难度是 Meduium,难度分是 2455。虽然是 Medium 题,但是打 Hard 标签一点也不为过。长期作为中等题的难度 Top1,直到去年被 2289. 使数组按非递减顺序排列
题挤下来。

标签:模拟、贪心、排序、同向双指针

问题描述

三枚石子放置在数轴上,位置分别为 a,b,c。

每一回合,你可以从两端之一拿起一枚石子(位置最大或最小),并将其放入两端之间的任一空闲位置。形式上,假设这三枚石子当前分别位于位置 x, y, z 且 x < y < z。那么就可以从位置 x 或者是位置 z 拿起一枚石子,并将该石子移动到某一整数位置 k 处,其中 x < k < z 且 k != y。

当你无法进行任何移动时,即,这些石子的位置连续时,游戏结束。

要使游戏结束,你可以执行的最小和最大移动次数分别是多少? 以长度为 2 的数组形式返回答案:answer = [minimum_moves, maximum_moves]

示例 1:

输入:a = 1, b = 2, c = 5
输出:[1, 2]
解释:将石子从 5 移动到 4 再移动到 3,或者我们可以直接将石子移动到 3。

示例 2:

输入:a = 4, b = 3, c = 2
输出:[0, 0]
解释:我们无法进行任何移动。

提示:

1 <= a <= 100
1 <= b <= 100
1 <= c <= 100
a != b, b != c, c != a

问题抽象

概括问题目标:

求移动石头直到连续的最小和最大操作次数,每次操作只能选择端点石头,且只能移动到非端点位置。

分析问题要件:

  • 限制条件 1:只能移动端点
  • 限制条件 2:只能移动到非端点位置,即需要翻越其它石头,例如示例 [1,2,4] 或 [1,2,5] 中,不能移动右端点;
  • 终止条件:如果左侧有空间,那么可以将右端点移动过去;如果右侧有空间,那么可以将左端点移动过去,因此当所有石头都连续时才无法继续移动。

提高抽象程度:

  • 空位:当不是所有石头都连续时,数组元素中间一定存在空位,空位数 = [右端点 - 左端点 + 1 - n]。当空位数 = 0 时,无法继续移动。
  • 决策模型:由于每次移动端点石头时,可以选择放位置到数组中间的空位上(满足限制条件 2),所以这是一个决策问题。

分析放置策略:

  • 思路类似于同系列问题:1033. 移动石子直到连续
  • 最大移动次数:为了放大移动次数,我们期望每次移动石头最多消耗一个空位;
  • 最少移动次数:为了缩小移动次数,我们期望每次移动石头最好一步到位;

最大移动次数分析:

  • 1、由于每次操作至少会消耗一个空位,所以最大操作次数不会高于空位个数;
  • 2、由于限制条件 2,所以每次移动石头都会完全丢弃端点石头与最近石头中间的所有空位。因此,为了放大移动次数,每次移动端点石头时当且仅当放置到最近石头相邻的空位时,移动次数是最优的(否则,下次在移动端点石头时,会放弃中间的所有空位,而移动到相邻空位则不会放弃任何空位);
  • 3、在确定最大放置策略后,由于从第二次移动开始不会丢弃任何空位,所以可以直接根据「空位数」公式算出第二次以后的最大移动空位:
    • 如果第一次移动左端点(丢弃 stones[0] 到 stones[1] 的空位,只填满 stones[1] 到 stones[n-1] 的空位),那么总操作次数为 (stones[n-1]) - (stones[1]) + 1 - (n - 1)
    • 如果第一次移动右端点(丢弃 stones[n-1] 到 stone[n-2] 的空位),那么总操作次数为 stones[n-2] - stones[0] + 1 - (n - 1)

最小移动次数分析:

  • 1、由于达到终止条件时所有石头都被放置在长度为 n 的窗口中,那么我们选择将游离的石头归集到石头最密集的区域时,能够缩小移动次数。具体来说,就是归集到长度为 n 的窗口中空位数最少得区域。此时,最小操作次数为 n - (石头数) = n - (j - i + 1)
  • 2、由于限制条件 2,有特例 [1,2,3,6] 和 [1,4,5,6],虽然窗口为 n 的空位数最少为 1,但总是需要 2 步才能移动完成。如 [1,2,3,6] -> [2,3,5,6] -> [3,4,5,6]。
  • 3、由于限制条件 2,有特例 [1,2,3,5] 和 [2,4,5,6],与上一条分析相似,但只要一次移动完成,由于这种特例能够被分析 1 覆盖,所以不需要特殊处理。

示意图

如何维护长度为 n 的滑动窗口:

同向双指针:

for(i in nums 索引) {
    while (j < n - 1 && 移动后窗口不大于 n) j++
}

题解

class Solution {
    fun numMovesStonesII(stones: IntArray): IntArray {
        val n = stones.size
        // 排序
        stones.sort()

        // 最大移动次数
        val max1 = stones[n - 1] - stones[1] + 1 - (n - 1)
        val max2 = stones[n - 2] - stones[0] + 1 - (n - 1)
        val maxOp = Math.max(max1, max2)

        // 最小移动次数
        var minOp = n
        var j = 0
        // 枚举左端点
        for (i in 0 until n) {
            while (j < n - 1 && stones[j + 1] - stones[i] + 1 <= n) j++
            if (n - (j - i + 1) == 1 /* 窗口空位数 == 1 */ && stones[j] - stones[i] + 1 == n - 1 /* 窗口石头数 == n - 1 */) {
                minOp = Math.min(minOp, 2)
            } else {
                minOp = Math.min(minOp, n - (j - i + 1) /* 窗口空位数 */)
            }
        }
        return intArrayOf(minOp, maxOp)
    }
}

复杂度分析:

  • 时间复杂度:$O(nlgn)$ 瓶颈来源于排序
  • 空间复杂度:$O(lgn)$ 瓶颈来源于排序

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

Java & Android 集合框架系列文章: 跳转阅读

与数据结构与算法 #18 下跳棋,极富想象力的同向双指针模拟相似的内容:

数据结构与算法 #18 下跳棋,极富想象力的同向双指针模拟

> ⭐️ **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。** > > 学习数据结构与算法的关键在于掌握问题背后的算法思

数据结构与算法大作业:走迷宫程序(C语言,DFS)(代码以及思路)

好家伙,写大作业,本篇为代码的思路讲解 1.大作业要求 走迷宫程序 问题描述: 以一个 m * n 的长方阵表示迷宫, 0和1分别表示迷宫的通路和障碍。 设计一个程序, 对任意设定的迷宫, 求出一条从入口到出口的通路, 或得出没有通路的结论。 基本要求: (1) 实现一个以链表做存储的栈类型, 然后

数据结构与算法大作业:走迷宫程序(实验报告)

好家伙,本篇为应付老师的实验报告,有需要的拿去抄吧 思路讲解在上一篇: 数据结构与算法大作业:走迷宫程序(C,代码以及思路) 一、作业目的 1、 掌握用数据结构的知识进行程序设计。 2、 应用所学的数据结构完成一个具有一定实际意义的应用程序的设计、编码、调试,锻炼实践动手能力,提高编程水平。 二、作

C#数据结构与算法入门教程,值得收藏学习!

前言 最近看到DotNetGuide技术社区交流群有不少小伙伴提问:想要系统化的学习数据结构和算法,不知道该怎么入门,有无好的教程推荐的?,今天大姚给大家推荐2个开源、免费的C#数据结构与算法入门教程,值得收藏学习! 数据结构与算法的作用 数据结构与算法在计算机科学中具有不可替代的地位和作用。通过学

跳跃表数据结构与算法分析

目前市面上充斥着大量关于跳跃表结构与Redis的源码解析,但是经过长期观察后发现大都只是在停留在代码的表面,而没有系统性地介绍跳跃表的由来以及各种常量的由来。作为一种概率数据结构,理解各种常量的由来可以更好地进行变化并应用到高性能功能开发中。本文没有重复地以对现有优秀实现进行代码分析,而是通过对跳跃表进行了系统性地介绍与形式化分析,并给出了在特定场景下的跳跃表扩展方式,方便读者更好地理解跳跃表数据

QtCreator 跨平台开发添加动态库教程(以OpenCV库举例)- Windows篇

Qt具有跨平台的特性,即Qt数据结构与算法库本身跨平台和编译脚本(.pro)跨平台。在同时具有Windows下和Linux开发的需求时,最好的建议是使用QtCreator来开发,虽然也可以使用其他的IDE配合CMake等方式,但使用QtCreator更加方便,并且操作环境完全一致。QtCreator

Java 断言 Assert 使用教程与最佳实践

本文收录于 Github.com/niumoo/JavaNotes,Java 系列文档,数据结构与算法! 本文收录于网站:https://www.wdbyte.com/,我的公众号:程序猿阿朗 作为一个 Java 开发者,如果要问你 Java 中有哪些关键字,你可能会随口说出一串,如果问你 Java

LeetCode 周赛上分之旅 #45 精妙的 O(lgn) 扫描算法与树上 DP 问题

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode

LeetCode 周赛上分之旅 #46 经典二分答案与质因数分解

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode

LeetCode 周赛上分之旅 #33 摩尔投票派上用场

> ⭐️ **本文已收录到 [AndroidFamily](https://github.com/pengxurui/AndroidFamily),技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。** > > 学习数据结构与算法的关键在于掌握问题背后的算法思