题目
给你一个整数数组 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 ,但是当仔细地去看题时,发现使用动态规划并不是那么的好求,看了看大佬们写的题解才知道不适用动态规划:
因为这个题目有三种跳跃方式:
- 跳到它的
前一个格子上- 跳到它的
后一个格子上- 跳到与它
值相同的格子上去这就意味着它可以向后去,这就违反了动态规划的无后效性
无后效性:
某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响,简单的说,就是“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。具体地说,如果一个问题被划分各个阶段之后,阶段I中的状态只能由阶段I-1中的状态通过状态转移方程得来,与其它状态没有关系,特别是与未发生的状态没有关系。从图论的角度去考虑,如果把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则构成一个有向无环加权图,因此,这个图可以进行“拓扑排序”,至少可以按它们拓扑排序的顺序去划分阶段。
- 这道的正确思路是使用 优先搜索BFS。
- 为了方便去理解,我们可以把给定的数组抽象成一个
无向无权图:将数组中的每一个数抽象成一个节点,每个节点与它的前一个节点与后一个节点各有一条边,与各同值节点之间都有一条边。 - 题目的问题便转化为了图的问题:即求
起点(数组第一个元素)到终点(数组最后一个元素)的最短路径长度。 - 明白做题思路后,还需要注意的是:如果不进行剪枝操作的话,在这里单纯的使用广度去搜索会超时的(时间复杂度会达到 n^2 级):对于等值节点所构成的图,在广度遍历的时候会对所有边都进行访问一次,但对于无权图的最短路问题,这样的访问是不必要的。在第一次访问到这个子图中的某个节点时,即会将这个子图的所有其他未在队列中的节点都放入队列。在第二次访问到这个子图中的节点时,就不需要去考虑这个子图中的其他节点了,因为所有其他节点都已经在队列中或者已经被访问过了。
代码设计
- 为了防止遍历超时,我们需要把数组中值相等的元素放在一起,当我们遍历到其中的一个时就把所有值相等的节点全部都进队,因此创建一个
SameValue(unordered_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;
}
};