修改数据范围,可以从「简单 BFS」变为「挖掘性质」的贪心 DP 题|Java 刷题打卡

简介: 修改数据范围,可以从「简单 BFS」变为「挖掘性质」的贪心 DP 题|Java 刷题打卡

题目描述



这是 LeetCode 上的 45. 跳跃游戏 II ,难度为 中等


Tag : 「贪心」、「线性 DP」、「双指针」


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


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


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


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


示例 1:


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


示例 2:


输入: [2,3,0,1,4]
输出: 2
复制代码


提示:


  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 10510^5105


BFS



对于这一类问题,我们一般都是使用 BFS 进行求解。


本题的 BFS 解法的复杂度是 O(n2)O(n^2)O(n2),数据范围为 10310^3103,可以过。


代码:


class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        int ans = 0;
        boolean[] st = new boolean[n];
        Deque<Integer> d = new ArrayDeque<>();
        st[0] = true;
        d.addLast(0);
        while (!d.isEmpty()) {
            int size = d.size();
            while (size-- > 0) {
                int idx = d.pollFirst();
                if (idx == n - 1) return ans;
                for (int i = idx + 1; i <= idx + nums[idx] && i < n; i++) {
                    if (!st[i]) {
                        st[i] = true;
                        d.addLast(i);
                    }
                }
            }
            ans++;
        }
        return ans;
    }
}
复制代码


  • 时间复杂度:如果每个点跳跃的距离足够长的话,每次都会将当前点「后面的所有点」进行循环入队操作(由于 st 的存在,不一定都能入队,但是每个点都需要被循环一下)。复杂度为 O(n2)O(n^2)O(n2)
  • 空间复杂度:队列中最多有 n−1n - 1n1 个元素。复杂度为 O(n)O(n)O(n)


双指针 + 贪心 + 动态规划



本题数据范围只有 10310^3103,所以 O(n2)O(n^2)O(n2) 勉强能过。


如果面试官要将数据范围出到 10610^6106,又该如何求解呢?


我们需要考虑 O(n)O(n)O(n) 的做法。


其实通过 10610^6106 这个数据范围,就已经可以大概猜到是道 DP 题。


我们定义 f[i]f[i]f[i] 为到达第 iii 个位置所需要的最少步数,那么答案是 f[n−1]f[n - 1]f[n1]


学习过 路径 DP 专题 的同学应该知道,通常确定 DP 的「状态定义」有两种方法。


  • 一种是根据经验猜一个状态定义,会结合题目给定的维度,和要求的答案去猜。
  • 另外一种则是通过设计一个合理的 DFS 方法签名来确定状态定义。


这里我是采用第一种方法。


至于如何确定「状态定义」是否可靠,关键是看使用这个状态定义能否推导出合理的「状态转移方程」,来覆盖我们所有的状态。


不失一般性的考虑 f[n−1]f[n - 1]f[n1] 该如何转移:


我们知道最后一个点前面可能会有很多个点能够一步到达最后一个点。


网络异常,图片无法展示
|


也就是有 f[n−1]=min(f[n−k],...,f[n−3],f[n−2])+1f[n - 1] = min(f[n - k],...,f[n - 3],f[n - 2]) + 1f[n1]=min(f[nk],...,f[n3],f[n2])+1


然后我们再来考虑集合 f[n−k],...,f[n−3],f[n−2]f[n - k],...,f[n - 3],f[n - 2]f[nk],...,f[n3],f[n2] 有何特性。


不然发现其实必然有 f[n−k]<=...<=f[n−3]<=f[n−2]f[n - k] <= ...<= f[n - 3] <= f[n - 2]f[nk]<=...<=f[n3]<=f[n2]


推而广之,不止是经过一步能够到达最后一个点的集合,其实任意连续的区间都有这个性质。


举个🌰,比如我经过至少 5 步到达第 iii 个点,那么必然不可能出现使用步数少于 5 步就能达到第 i+1i + 1i+1 个点的情况。到达第 i+1i + 1i+1 个点的至少步数必然是 5 步或者 6 步。


搞清楚性质之后,再回头看我们的状态定义:f[i]f[i]f[i] 为到达第 iii 个位置所需要的最少步数。


因此当我们要求某一个 f[i]f[i]f[i] 的时候,我们需要找到最早能够经过一步到达 iii 点的 jjj 点。


即有状态转移方程:f[i]=f[j]+1f[i] = f[j] + 1f[i]=f[j]+1


也就是我们每次都贪心的取离 iii 点最远的点 jjj 来更新 f[i]f[i]f[i]


而这个找 jjj 的过程可以使用双指针来找。


因此这个思路其实是一个「双指针 + 贪心 + 动态规划」的一个解法。


代码:


class Solution {
    public int jump(int[] nums) {
        int n = nums.length;
        int[] f = new int[n]; 
        for (int i = 1, j = 0; i < n; i++) {
            while (j + nums[j] < i) j++;
            f[i] = f[j] + 1;
        }
        return f[n - 1];
    }
}
复制代码


  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)


最后



这是我们「刷穿 LeetCode」系列文章的第 No.45 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
4天前
|
Java 程序员 容器
Java中的变量和常量:数据的‘小盒子’和‘铁盒子’有啥不一样?
在Java中,变量是一个可以随时改变的数据容器,类似于一个可以反复打开的小盒子。定义变量时需指定数据类型和名称。例如:`int age = 25;` 表示定义一个整数类型的变量 `age`,初始值为25。 常量则是不可改变的数据容器,类似于一个锁死的铁盒子,定义时使用 `final` 关键字。例如:`final int MAX_SPEED = 120;` 表示定义一个名为 `MAX_SPEED` 的常量,值为120,且不能修改。 变量和常量的主要区别在于变量的数据可以随时修改,而常量的数据一旦确定就不能改变。常量主要用于防止意外修改、提高代码可读性和便于维护。
|
5天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
13 2
|
5天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
11 2
|
9天前
|
存储 分布式计算 Java
存算分离与计算向数据移动:深度解析与Java实现
【11月更文挑战第10天】随着大数据时代的到来,数据量的激增给传统的数据处理架构带来了巨大的挑战。传统的“存算一体”架构,即计算资源与存储资源紧密耦合,在处理海量数据时逐渐显露出其局限性。为了应对这些挑战,存算分离(Disaggregated Storage and Compute Architecture)和计算向数据移动(Compute Moves to Data)两种架构应运而生,成为大数据处理领域的热门技术。
27 2
|
15天前
|
SQL Java OLAP
java实现“数据平滑升级”
java实现“数据平滑升级”
35 2
|
20天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。
|
16天前
|
SQL Java OLAP
java实现“数据平滑升级”
java实现“数据平滑升级”
9 0
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
3天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
16 9
|
6天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####