LeetCode 1340. 跳跃游戏 V(困难)

news2025/5/24 20:40:46

题目描述

给你一个整数数组 arr 和一个整数 d 。每一步你可以从下标 i 跳到:

  • i + x ,其中 i + x < arr.length 且 0 < x <= d 。
  • i - x ,其中 i - x >= 0 且 0 < x <= d 。

除此以外,你从下标 i 跳到下标 j 需要满足:arr[i] > arr[j] 且 arr[i] > arr[k] ,其中下标 k 是所有 i 到 j 之间的数字(更正式的,min(i, j) < k < max(i, j))。

你可以选择数组的任意下标开始跳跃。请你返回你 最多 可以访问多少个下标。

请注意,任何时刻你都不能跳到数组的外面。

示例 1:

输入:arr = [6,4,14,6,8,13,9,7,10,6,12], d = 2
输出:4
解释:你可以从下标 10 出发,然后如上图依次经过 10 --> 8 --> 6 --> 7 。
注意,如果你从下标 6 开始,你只能跳到下标 7 处。你不能跳到下标 5 处因为 13 > 9 。你也不能跳到下标 4 处,因为下标 5 在下标 4 和 6 之间且 13 > 9 。
类似的,你不能从下标 3 处跳到下标 2 或者下标 1 处。

示例 2:

输入:arr = [3,3,3,3,3], d = 3
输出:1
解释:你可以从任意下标处开始且你永远无法跳到任何其他坐标。

示例 3:

输入:arr = [7,6,5,4,3,2,1], d = 1
输出:7
解释:从下标 0 处开始,你可以按照数值从大到小,访问所有的下标。

示例 4:

输入:arr = [7,1,7,1,7,1], d = 2
输出:2

示例 5:

输入:arr = [66], d = 1
输出:1

提示:

  • 1 <= arr.length <= 1000
  • 1 <= arr[i] <= 10^5
  • 1 <= d <= arr.length

问题分析

这道题是跳跃游戏系列的第五题,有以下特点:

  • 跳跃规则:
    • 可以向左或向右跳跃,跳跃距离范围是 [1, d]
    • 只能从高处往低处跳(arr[i] > arr[j])
    • 跳跃路径上的所有点都必须比起点低(arr[i] > arr[k],对于所有 i 到 j 之间的 k)
  • 目标:找出从任意起点出发,最多可以访问多少个下标。
  • 关键点:
    • 可以从任意下标开始
    • 需要计算的是最大访问点数,而不是是否能到达某个特定目标

这个问题适合使用动态规划来解决,因为跳跃决策具有重叠子问题的性质。同时,由于跳跃的方向性(只能从高处跳到低处),我们可以按高度排序来确定解决问题的顺序。


解题思路

动态规划 + 记忆化搜索

我们可以定义 dp[i] 表示从下标 i 开始跳跃,最多可以访问的下标数量(包括起点自身)。

递推关系如下:

  • 对于每个下标 i,我们尝试向左或向右跳跃距离 x(其中 1 <= x <= d)
  • 如果可以跳到下标 j,那么 dp[i] = max(dp[i], 1 + dp[j])

由于跳跃方向是从高到低,我们需要确保先计算出较低位置的 dp 值,再计算较高位置的 dp 值。一种方法是使用记忆化搜索(也称为自顶向下的动态规划)。

算法步骤

  • 创建一个 dp 数组,dp[i] 表示从下标 i 开始最多可以访问的下标数量
  • 将所有 dp[i] 初始化为 1(至少可以访问自身)
  • 使用记忆化搜索,对每个下标 i:
    • 尝试向左或向右跳跃距离 x(1 <= x <= d)
    • 检查跳跃条件:目标位置在数组范围内,且路径上所有点都比起点低
    • 如果可以跳到下标 j,则 dp[i] = max(dp[i], 1 + dp[j])
  • 返回所有 dp[i] 中的最大值

算法过程

以示例1为例:arr = [6,4,14,6,8,13,9,7,10,6,12], d = 2

让我们跟踪从下标10开始的DFS过程(这是最优起点):

  • 初始化:
    • dp[10] = 1(至少可以访问自身)
    • 当前下标:10,对应值:12
  • 尝试向左跳:
    • 检查下标10-1=9:arr[10] > arr[9] (12 > 6) ✓
      • 递归计算 dp[9]
      • dp[9] = 1(至少可以访问自身)
      • 由于没有可跳的地方,dp[9] = 1
    • 检查下标10-2=8:arr[10] > arr[8] (12 > 10) ✓
      • 递归计算 dp[8]
      • dp[8] = 1
      • 检查下标8-1=7:arr[8] > arr[7] (10 > 7) ✓
        • 递归计算 dp[7]
        • dp[7] = 1
        • 检查下标7-1=6:arr[7] > arr[6] (7 < 9) ✗ 不能跳
        • 检查下标7-2=5:超出范围(d=2)
      • 所以 dp[7] = 1
      • 更新 dp[8] = 1 + dp[7] = 2
      • 检查下标8-2=6:arr[8] > arr[6] (10 > 9) ✓
        • 已计算 dp[6] = 1(没有可跳的地方)
    • 更新 dp[8] = max(2, 1+1) = 2
    • 更新 dp[10] = max(1, 1+dp[8]) = 1+2 = 3
  • 最终路径:
    • 下标10 -> 下标8 -> 下标7 -> 可能的终点(无法继续跳跃)
    • 或者 下标10 -> 下标8 -> 下标6 -> 可能的终点
    • 最多访问4个下标(包括起点10)

事实上,完整的最优路径是:10 -> 8 -> 6 -> 7,总共访问4个下标。


详细代码实现

Java 实现

class Solution {
    private int[] dp;
    private int[] arr;
    private int d;
    
    public int maxJumps(int[] arr, int d) {
        int n = arr.length;
        this.arr = arr;
        this.d = d;
        this.dp = new int[n];
        
        // 初始化dp数组,每个位置至少可以访问自身
        Arrays.fill(dp, -1);
        
        int maxVisited = 0;
        for (int i = 0; i < n; i++) {
            maxVisited = Math.max(maxVisited, dfs(i));
        }
        
        return maxVisited;
    }
    
    private int dfs(int i) {
        // 如果已经计算过,直接返回
        if (dp[i] != -1) {
            return dp[i];
        }
        
        // 至少可以访问自身
        dp[i] = 1;
        
        // 尝试向右跳
        for (int j = i + 1; j <= Math.min(i + d, arr.length - 1); j++) {
            // 检查跳跃条件
            if (arr[i] <= arr[j]) {
                break;
            }
            
            // 检查路径上的所有点
            boolean canJump = true;
            for (int k = i + 1; k < j; k++) {
                if (arr[k] >= arr[i]) {
                    canJump = false;
                    break;
                }
            }
            
            if (canJump) {
                dp[i] = Math.max(dp[i], 1 + dfs(j));
            }
        }
        
        // 尝试向左跳
        for (int j = i - 1; j >= Math.max(i - d, 0); j--) {
            // 检查跳跃条件
            if (arr[i] <= arr[j]) {
                break;
            }
            
            // 检查路径上的所有点
            boolean canJump = true;
            for (int k = i - 1; k > j; k--) {
                if (arr[k] >= arr[i]) {
                    canJump = false;
                    break;
                }
            }
            
            if (canJump) {
                dp[i] = Math.max(dp[i], 1 + dfs(j));
            }
        }
        
        return dp[i];
    }
}

C# 实现

public class Solution {
    private int[] dp;
    private int[] arr;
    private int d;
    
    public int MaxJumps(int[] arr, int d) {
        int n = arr.Length;
        this.arr = arr;
        this.d = d;
        this.dp = new int[n];
        
        // 初始化dp数组
        for (int i = 0; i < n; i++) {
            dp[i] = -1;
        }
        
        int maxVisited = 0;
        for (int i = 0; i < n; i++) {
            maxVisited = Math.Max(maxVisited, Dfs(i));
        }
        
        return maxVisited;
    }
    
    private int Dfs(int i) {
        // 如果已经计算过,直接返回
        if (dp[i] != -1) {
            return dp[i];
        }
        
        // 至少可以访问自身
        dp[i] = 1;
        
        // 尝试向右跳
        for (int j = i + 1; j <= Math.Min(i + d, arr.Length - 1); j++) {
            // 检查跳跃条件
            if (arr[i] <= arr[j]) {
                break;
            }
            
            // 检查路径上的所有点
            bool canJump = true;
            for (int k = i + 1; k < j; k++) {
                if (arr[k] >= arr[i]) {
                    canJump = false;
                    break;
                }
            }
            
            if (canJump) {
                dp[i] = Math.Max(dp[i], 1 + Dfs(j));
            }
        }
        
        // 尝试向左跳
        for (int j = i - 1; j >= Math.Max(i - d, 0); j--) {
            // 检查跳跃条件
            if (arr[i] <= arr[j]) {
                break;
            }
            
            // 检查路径上的所有点
            bool canJump = true;
            for (int k = i - 1; k > j; k--) {
                if (arr[k] >= arr[i]) {
                    canJump = false;
                    break;
                }
            }
            
            if (canJump) {
                dp[i] = Math.Max(dp[i], 1 + Dfs(j));
            }
        }
        
        return dp[i];
    }
}

复杂度分析

  • 时间复杂度:O(n²),其中n是数组的长度。在最坏情况下,对于每个位置,我们需要检查最多2d个可能的跳跃目标,总时间复杂度为O(n * d)。由于d最大可达n,所以最坏情况下时间复杂度是O(n²)。
  • 空间复杂度:O(n),主要用于存储dp数组和递归调用栈。

优化:单调栈方法

上面的实现中,检查路径上的所有点是否都比起点低的时间复杂度是 O(d),我们可以使用单调栈来优化这一过程,降低时间复杂度。

Java 实现

class Solution {
    public int maxJumps(int[] arr, int d) {
        int n = arr.length;
        int[] dp = new int[n];
        Arrays.fill(dp, -1);
        
        // 将下标按照高度排序,从低到高处理
        Integer[] indices = new Integer[n];
        for (int i = 0; i < n; i++) {
            indices[i] = i;
        }
        Arrays.sort(indices, (a, b) -> arr[a] - arr[b]);
        
        int maxVisited = 0;
        for (int idx : indices) {
            maxVisited = Math.max(maxVisited, dfs(arr, d, idx, dp));
        }
        
        return maxVisited;
    }
    
    private int dfs(int[] arr, int d, int i, int[] dp) {
        if (dp[i] != -1) {
            return dp[i];
        }
        
        dp[i] = 1; // 至少可以访问自身
        
        // 向右跳
        for (int j = i + 1; j <= Math.min(i + d, arr.length - 1); j++) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.max(dp[i], 1 + dfs(arr, d, j, dp));
            }
            // 如果遇到更高或相等的点,则无法继续向右
            if (arr[j] >= arr[i]) {
                break;
            }
        }
        
        // 向左跳
        for (int j = i - 1; j >= Math.max(i - d, 0); j--) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.max(dp[i], 1 + dfs(arr, d, j, dp));
            }
            // 如果遇到更高或相等的点,则无法继续向左
            if (arr[j] >= arr[i]) {
                break;
            }
        }
        
        return dp[i];
    }
}

C#实现

public class Solution {
    public int MaxJumps(int[] arr, int d) {
        int n = arr.Length;
        int[] dp = new int[n];
        
        // 初始化dp数组,所有值设为-1表示未计算
        for (int i = 0; i < n; i++) {
            dp[i] = -1;
        }
        
        // 将下标按照高度排序,从低到高处理
        int[] indices = new int[n];
        for (int i = 0; i < n; i++) {
            indices[i] = i;
        }
        Array.Sort(indices, (a, b) => arr[a] - arr[b]);
        
        int maxVisited = 0;
        foreach (int idx in indices) {
            maxVisited = Math.Max(maxVisited, Dfs(arr, d, idx, dp));
        }
        
        return maxVisited;
    }
    
    private int Dfs(int[] arr, int d, int i, int[] dp) {
        // 如果已经计算过,直接返回
        if (dp[i] != -1) {
            return dp[i];
        }
        
        // 至少可以访问自身
        dp[i] = 1;
        
        // 向右跳
        for (int j = i + 1; j <= Math.Min(i + d, arr.Length - 1); j++) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.Max(dp[i], 1 + Dfs(arr, d, j, dp));
            }
            // 如果遇到更高或相等的点,则无法继续向右
            if (arr[j] >= arr[i]) {
                break;
            }
        }
        
        // 向左跳
        for (int j = i - 1; j >= Math.Max(i - d, 0); j--) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.Max(dp[i], 1 + Dfs(arr, d, j, dp));
            }
            // 如果遇到更高或相等的点,则无法继续向左
            if (arr[j] >= arr[i]) {
                break;
            }
        }
        
        return dp[i];
    }
}

复杂度分析

  • 时间复杂度:O(n log n + n * d)
    • 排序下标需要O(n log n)时间
    • 对于每个位置,我们最多检查2d个可能的跳跃目标,总共n个位置,所以是O(n * d)
    • 综合起来就是O(n log n + n * d)
  • 空间复杂度:O(n)
    • 主要用于存储dp数组、排序后的下标数组和递归调用栈

优化与技巧

  1. 按高度排序处理:先计算高度较低的点的dp值,再计算高度较高的点的dp值,可以减少重复计算。
  2. 提前终止:如果遇到高度大于等于当前点的位置,可以提前终止搜索,因为无法跳过这个位置。
  3. 记忆化搜索:使用dp数组存储已计算的结果,避免重复计算。
  4. 边界检查:确保跳跃不会超出数组范围。
  5. 利用问题特性:由于只能从高处跳到低处,整个跳跃路径形成了一个有向无环图(DAG),这使得动态规划可以正确解决此问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2384861.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

x-cmd install | cargo-selector:优雅管理 Rust 项目二进制与示例,开发体验升级

目录 功能亮点安装优势特点适用场景总结 还在为 Rust 项目中众多的二进制文件和示例而烦恼吗&#xff1f;cargo-selector 让你告别繁琐的命令行&#xff0c;轻松选择并运行目标程序&#xff01; 功能亮点 交互式选择&#xff1a; 在终端中以交互方式浏览你的二进制文件和示例&…

大模型「瘦身」指南:从LLaMA到MobileBERT的轻量化部署实战

大模型「瘦身」指南&#xff1a;从LLaMA到MobileBERT的轻量化部署实战 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 大模型「瘦身」指南&#xff1a;从LLaMA到MobileBERT的轻量化部署实战摘要引言一、轻量化技术…

从逻辑视角学习信息论:概念框架与实践指南

文章目录 一、信息论的逻辑基础与哲学内涵1.1 信息的逻辑本质&#xff1a;区分与差异1.2 逆范围原理与信息内容 二、信息论与逻辑学的概念交汇2.1 熵作为逻辑不确定性的度量2.2 互信息与逻辑依赖2.3 信道容量的逻辑极限 三、信息论的核心原理与逻辑基础3.1 最大熵原理的逻辑正当…

TDengine 运维—容量规划

概述 若计划使用 TDengine 搭建一个时序数据平台&#xff0c;须提前对计算资源、存储资源和网络资源进行详细规划&#xff0c;以确保满足业务场景的需求。通常 TDengine 会运行多个进程&#xff0c;包括 taosd、taosadapter、taoskeeper、taos-explorer 和 taosx。 在这些进程…

PPP 拨号失败:ATD*99***1# ... failed

从日志来看&#xff0c;主要有两类问题&#xff1a; 一、led_indicator_stop 报 invalid p_handle E (5750) led_indicator: …/led_indicator.c:461 (led_indicator_stop):invalid p_handle原因分析 led_indicator_stop() 的参数 p_handle &#xff08;即之前 led_indicator…

【计网】五六章习题测试

目录 1. (单选题, 3 分)某个网络所分配到的地址块为172.16.0.0/29&#xff0c;能接收目的地址为172.16.0.7的IP分组的最大主机数是&#xff08; &#xff09;。 2. (单选题, 3 分)若将某个“/19”的CIDR地址块划分为7个子块&#xff0c;则可能的最小子块中的可分配IP地址数量…

汇川EasyPLC MODBUS-RTU通信配置和编程实现

累积流量计算(MODBUS RTU通信数据处理)数据处理相关内容。 累积流量计算(MODBUS RTU通信数据处理)_流量积算仪modbus rtu通讯-CSDN博客文章浏览阅读219次。1、常用通信数据处理MODBUS通信系列之数据处理_modbus模拟的数据变化后会在原来的基础上累加是为什么-CSDN博客MODBUS通…

从 CANopen到 PROFINET:网关助力物流中心实现复杂的自动化升级

使用 CANopen PLC 扩展改造物流中心的传送带 倍讯科技profinet转CANopen网关BX-601-EIP将新的 PROFINET PLC 系统与旧的基于 CANopen 的传送带连接起来&#xff0c;简化了物流中心的自动化升级。 新建还是升级&#xff1f;这些问题通常出现在复杂的内部物流设施中&#xff0c;…

基于Yolov8+PyQT的老人摔倒识别系统源码

概述 ​​基于Yolov8PyQT的老人摔倒识别系统​​&#xff0c;该系统通过深度学习算法实时检测人体姿态&#xff0c;精准识别站立、摔倒中等3种状态&#xff0c;为家庭或养老机构提供及时预警功能。 主要内容 ​​完整可运行代码​​ 项目采用Yolov8目标检测框架结合PyQT5开发…

wsl2 不能联网

wsl2 安装后用 wifi 共享是能联网&#xff0c;问题出在公司网络限制 wsl2 IP 访问网络&#xff0c;但是主机可以上网。 解决办法&#xff0c;在主机用 nginx 设置代理&#xff0c;可能需要开端口权限 server {listen 9000;server_name localhost;location /ubuntu/ {#…

Java[IDEA]里的debug

目录 前言 Debug 使用Debug 总结 前言 这里我说一下就是 java IDEA 工具里的debug工具 里的一个小问题 就是 当我们使用debug去查看内部文档 查看不到 是为什么 Debug 所谓 debug 工具 他就是用来调试程序的 当我们写代码 报错 出错时 我们就可以使用这个工具 因此这个工具…

DAO模式

1. 持久化 简单来说&#xff0c;就是把代码的处理结果转换成需要的格式进行储存。 2. JDBC的封装 3. DAO模式 4. Properties类与Properties配置文件 添加 读取 5. 使用实体类传递数据 6. 总结 附录&#xff1a; BaseDao指南 BaseDao指南-CSDN博客

ECharts图表工厂,完整代码+思路逻辑

Echart工厂支持柱状图&#xff08;bar&#xff09;折线图&#xff08;line&#xff09;散点图&#xff08;scatter&#xff09;饼图&#xff08;pie&#xff09;雷达图&#xff08;radar&#xff09;极坐标柱状图&#xff08;polarBar&#xff09;和极坐标折线图&#xff08;po…

CSS:margin的塌陷与合并问题

文章目录 一、margin塌陷问题二、margin合并问题 一、margin塌陷问题 二、margin合并问题

探索服务网格(Service Mesh):云原生时代的网络新范式

文章目录 一、引言二、什么是服务网格基本定义形象比喻 三、服务网格解决了哪些问题微服务通信复杂性可观察性安全性 四、常见的服务网格实现IstioLinkerdConsul Connect 五、服务网格的应用场景大型微服务架构混合云环境 六、服务网格的未来发展与其他技术的融合标准化和行业规…

SQL SERVER中实现类似LEAST函数的功能,返回多列数据中的最小值

使用 LEAST&#xff08;&#xff09;函数可以简洁地在一行SQL语句中找出多个值中的最小值&#xff0c;但在SQLServer数据库中&#xff0c;没有内置的LEAST函数。 我们可以使用values子句创建临时的数据集的办法&#xff0c;返回多列数据中的最小值。 创建表 CREATE TABLE stu…

SymPy | 获取表达式自由变量方法与因式分解

SymPy 是 Python 中强大的符号计算库&#xff0c;广泛应用于数学建模、公式推导和科学计算。本文将从两个核心功能展开&#xff1a;表达式中自由变量的获取与因式分解的实现&#xff0c;通过完整代码示例和深入分析&#xff0c;帮助读者掌握其使用方法。 第一部分&#xff1a;获…

深度剖析并发I/O模型select、poll、epoll与IOCP核心机制

核心概要&#xff1a;select、poll、epoll 和 IOCP 是四种用于提升服务器并发处理能力的I/O模型或机制。前三者主要属于I/O多路复用范畴&#xff0c;允许单个进程或线程监视多个I/O流的状态&#xff1b;而 IOCP 则是一种更为彻底的异步I/O模型。 一、引言&#xff1a;为何需要这…

数据结构 -- 交换排序(冒泡排序和快速排序)

冒泡排序 基于“交换”的排序&#xff1a;根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置 //交换 void swap(int &a,int &b){int temp a;a b;b temp; }//冒泡排序 void BubbleSort(int A[],int n){for(int i0;i<n-1;i){bool flag false; …

【算法】: 前缀和算法(利用o(1)的时间复杂度快速求区间和)

前缀和算法&#xff1a;高效处理区间求和的利器 目录 引言什么是前缀和前缀和的基本实现前缀和的作用前缀和的典型应用场景前缀和的优缺点分析实战例题解析 引言 区间求和问题的普遍性暴力解法的时间复杂度问题前缀和算法的核心思想 什么是前缀和 前缀和的数学定义 通俗来…