力扣 —— 1345. 跳跃游戏 IV

194 阅读5分钟

题目

给你一个整数数组 arr ,你一开始在数组的第一个元素处(下标为 0)。

每一步,你可以从下标 i 跳到下标:

  • i + 1 满足:i + 1 < arr.length
  • i - 1 满足:i - 1 >= 0
  • j 满足:arr[i] == arr[j] 且 i != j

请你返回到达数组最后一个元素的下标处所需的 最少操作次数 。

注意:任何时候你都不能跳到数组外面。

实例1:

输入:arr = [100,-23,-23,404,100,23,23,23,3,404]
输出:3
解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。

实例2:

输入:arr = [7]
输出:0
解释:一开始就在最后一个元素处,所以你不需要跳跃。

实例3:

输入:arr = [7,6,9,6,9,6,9,7]
输出:1
解释:你可以直接从下标 0 处跳到下标 7 处,也就是数组的最后一个元素处。

实例4:

输入:arr = [6,1,9] 输出:2

示例 5:

输入:arr = [11,22,7,7,7,7,7,7,7,22,13] 输出:3

提示:

  • 1 <= arr.length <= 5 * 10^4
  • -10^8 <= arr[i] <= 10^8

题目分析

第一眼看到题目的要求是求最少操作数,想到的便是使用 动态规划DP ,但是当仔细地去看题时,发现使用动态规划并不是那么的好求,看了看大佬们写的题解才知道不适用动态规划:

因为这个题目有三种跳跃方式:

  1. 跳到它的前一个格子上
  2. 跳到它的后一个格子上
  3. 跳到与它值相同的格子上去

这就意味着它可以向后去,这就违反了动态规划的无后效性

无后效性:

某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响,简单的说,就是“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。具体地说,如果一个问题被划分各个阶段之后,阶段I中的状态只能由阶段I-1中的状态通过状态转移方程得来,与其它状态没有关系,特别是与未发生的状态没有关系。从图论的角度去考虑,如果把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则构成一个有向无环加权图,因此,这个图可以进行“拓扑排序”,至少可以按它们拓扑排序的顺序去划分阶段。

  • 这道的正确思路是使用 优先搜索BFS
  • 为了方便去理解,我们可以把给定的数组抽象成一个无向无权图:将数组中的每一个抽象成一个节点,每个节点与它的前一个节点与后一个节点各有一条边,与各同值节点之间都有一条边。
  • 题目的问题便转化为了图的问题:即求起点(数组第一个元素)到终点(数组最后一个元素)的最短路径长度。
  • 明白做题思路后,还需要注意的是:如果不进行剪枝操作的话,在这里单纯的使用广度去搜索会超时的(时间复杂度会达到 n^2 级):对于等值节点所构成的图,在广度遍历的时候会对所有边都进行访问一次,但对于无权图的最短路问题,这样的访问是不必要的。在第一次访问到这个子图中的某个节点时,即会将这个子图的所有其他未在队列中的节点都放入队列。在第二次访问到这个子图中的节点时,就不需要去考虑这个子图中的其他节点了,因为所有其他节点都已经在队列中或者已经被访问过了。

代码设计

  • 为了防止遍历超时,我们需要把数组中值相等的元素放在一起,当我们遍历到其中的一个时就把所有值相等的节点全部都进队,因此创建一个SameValueunordered_map<int,vector<int>> )来存放值相等的下标。并且要保证每个节点只访问一次,创建数组Visited[n].
  • 并且为了记录最小步数,我们要使用一个键值对(存放到达的节点的下标到达该所使用的步数)来表示每一次入队的节点。
  • 最关键的就是:如果访问的同值节点中的其中一个时,我们会一次性把所有同值节点都加入到队列中,因为同值节点只需要一次访问即可,加入到队列后我们就需要把这类节点从SameValue中删除掉。

复杂度分析

  • 时间复杂度:O(n) ;使用BFS来解决问题的最坏境况为:长度为 n 的数组中的每个元素都要进到队列里面。
  • 空间复杂度:O(n) ; 哈希表、队列、访问标记集合的长度最长为数组的长度n

完整代码

class Solution {
public:
    int minJumps(vector<int>& arr) {
        
        int n = arr.size();
        unordered_map<int,vector<int>> SameValue; // 记录值相等的节点
        for(int i=0; i<n; i++){
            SameValue[arr[i]].push_back(i);
        }

        vector<bool> Visited(n,false);  // 节点是否访问过
        queue<pair<int,int>> qu;

        // 首先入队起点并标记起点已访问过
        qu.emplace(0,0);
        Visited[0] = true;

        while(!qu.empty()){
            auto [index,step] = qu.front();
            qu.pop();

            // 判断是否为终点
            if(index == n-1) return step;
            int val = arr[index];
            step++;

            // 只要同值的所有结点被入过队(同值类的节点,会在遇到第一个该值的节点的时候全部入队),就会把这类节点从SameVal数组中删去,不需要重复入队去访问
            //
            if(SameValue.count(val)){  
                for(auto & i : SameValue[val]){ // 去遍历这同值类节点
                    if(!Visite[i]){
                        Visite[i] = true;
                        if(i == n-1) return step;
                        qu.emplace(i,step);//入队
                    }
                }
                // 删去
                SameValue.erase(val);
            }

            // 向前一个数去遍历
            if(index +1 <n && !Visited[index + 1]){
                Visited[index + 1] = true;
                qu.emplace(index+1,step);
            }
             // 向后一个数去遍历
            if(index -1 >=0 && !Visited[index - 1]){
                Visited[index - 1] = true;
                qu.emplace(index - 1,step);
            }
        }
        return -1; 
    }
};