LCP 09. 最小跳跃次数
为了给刷题的同学一些奖励,力扣团队引入了一个弹簧游戏机。游戏机由 N 个特殊弹簧排成一排,编号为 0 到 N-1。初始有一个小球在编号 0 的弹簧处。若小球在编号为 i 的弹簧处,通过按动弹簧,可以选择把小球向右弹射 jump[i] 的距离,或者向左弹射到任意左侧弹簧的位置。也就是说,在编号为 i 弹簧处按动弹簧,小球可以弹向 0 到 i-1 中任意弹簧或者 i+jump[i] 的弹簧(若 i+jump[i]>=N ,则表示小球弹出了机器)。小球位于编号 0 处的弹簧时不能再向左弹。
为了获得奖励,你需要将小球弹出机器。请求出最少需要按动多少次弹簧,可以将小球从编号 0 弹簧弹出整个机器,即向右越过编号 N-1 的弹簧。
示例 1:
输入:jump = [2, 5, 1, 1, 1, 1]
输出:3
解释:小 Z 最少需要按动 3 次弹簧,小球依次到达的顺序为 0 -> 2 -> 1 -> 6,最终小球弹出了机器。
限制:
1 <= jump.length <= 10^6
1 <= jump[i] <= 10000
下面纯复盘:
PS:准备的时候压根没刷BFS和DFS的图遍历算法,直接gg。看到题目的时候第一反应是和青蛙跳石头过河的题目有点相像,那道题做过,所以想着能不能用动态规划和深度遍历的方法做,动态规划的话这次的球可以往后跳,而且球可以跳出范围,所以不好确定dp数组的范围,也不知道怎么处理球往开头位置跳的情况。所以想了想深度遍历,其实就是单纯的将每一步可以到达的位置列出来,包括从这个位置往前跳的位置和往后跳的任意位置,选出下一个能跳更远的位置继续(就是说如果前面的位置能跳得更远那就选那个位置,不然就选后面的),情况是给的样例全过了,自己想的样例也过了,就是提交后一个不过,gg
PSSS:其实就是BFS套模板,为什么我不早点会。。。
题解
寻找最少按动次数,相当于到或者超过弹珠机范围的最短路径,两者是等价的
所以使用BFS,根据解答链接的说法,这是一种扩展问题,目的是求弹珠的覆盖(弹)范围,这个范围随着弹珠不断跳动而扩展,就像一个点扩展到一个面,所以用BFS求覆盖面超过范围的最少按动次数
确定算法后就是BFS的框架了,queue记录覆盖范围,每个点是弹珠能弹的位置和对应的按动次数
初值就是0,0
因为一个位置被扩展过了,后面就不用再扩展这个位置了,因为后面扩展到这个位置按动次数肯定会更多,我们需要少的,而且会重复访问
这题扩展分为两个方向:
1、向前扩展:只有idx + jump[idx];超出范围那就可以直接返回了,记录访问记录
2、向后扩展:重点,按照题意应该是从0开始到该位置idx之间的点需要扩展,问题是这样的话最坏情况下是O(n^2 )(因为假设队列的顶点位置是10,9,8,那么就需要遍历9,8,7个顶点,以此类推n^2),最坏情况会超时,因此这里巧妙的地方就在于设置了preidx,preidx都是已扩展过的顶点,这样循环就变成了判断,只需要执行一次,从 n^ 2 变成n。为什么能这么设置呢?那是因为题目说到每次向后跳是都可以覆盖之前所有的位置,因此相当于之前的位置每个点扩展一遍就够了,后面的位置想要往前跳,就只需要减掉以扩展部分即可
建议用这个版本,这个版本属于是BFS模板:重点是while(pre < cur)这里一定要这么写
不写抑或是写成for循环的话都会超时,解决办法就是记录前面数据上一个的遍历的最后位置
class Solution {
public:
int minJump(vector<int>& jump) {
queue<int> que;
unordered_set<int> hash;
que.push(0);
hash.insert(0);
int res = 0;
int pre = 0;
while(!que.empty()){
int size = que.size();
res++;
for(int i = 0;i < size;++i){
int cur = que.front();
que.pop();
if(jump[cur] + cur >= jump.size()){
return res;
}
if(hash.find(jump[cur] + cur) == hash.end()){
hash.insert(jump[cur] + cur);
que.push(jump[cur] + cur);
}
while(pre < cur){
if(hash.find(pre) == hash.end()){
hash.insert(pre);
que.push(pre);
}
++pre;
}
}
}
return -1;
}
};
这个版本是题解中的,和模板有点出入
class Solution {
public:
int minJump(vector<int>& jump) {
int n = jump.size();
queue< pair<int, int> > q; // 当前位置 idx,按动次数 d
// 初始值:编号为 0 的弹簧 按动次数 为0
q.emplace(0, 0);
// 某一个位置已经被扩展过了,就不需要被再次扩展:BFS最短路
// 记录某个位置是否被扩展过:seen
vector<bool> seen(n, false);
seen[0] = true;
int preidx = 1;
// BFS
while(!q.empty()) {
auto [idx, d] = q.front();
q.pop();
// 向右扩展
int next = idx + jump[idx];
if(next > n - 1) {
// 跳出弹簧
// BFS 最短路
return d + 1;
}
if(!seen[next]) {
seen[next] = true;
q.emplace(next, d + 1);
}
// 向左扩展:O(n^2)
// 某一个位置及其之前所有位置都已经被扩展过,那么一定是最短路,不需要再次被扩展
// preidx:记录某个位置及其之前位置均已被扩展,每次更新preidx 即可
// for(int i = 0; i < idx; i++) {
// if(!seen[i]) {
// seen[i] = true;
// q.emplace(i, d + 1);
// }
// }
while(preidx < idx) {
// 最多只会遍历一遍全部位置 :O(n)
if(!seen[preidx]) {
seen[preidx] = true;
q.emplace(preidx, d + 1);
}
preidx++;
}
}
// 遍历完整个队列,还未返回,说明无法到达
return -1;
}
};
PS:另外两道题是中序序列和后序序列建立树、再求出最长路径(这个过了80%,有20%超时),原因就是找根节点所在中序序列的位置我直接遍历整个数组,大概是这个原因正确做法是在一开始用一个map记录中序序列中所有值的下标索引
这里的重点是先建立右子树再建立左子树,这样做的原因是用于标定根节点的是后序序列的最后一个结点,因为后序是左右中,所以根节点一开始是最后一个值,然后不断往前都是右子树树根,遍历完右子树就是左子树。这样还有一个好处就是不需要标定后序序列的左右子树范围
举个例子,中序:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3],根节点位置在postorder是4,值是3,然后是3,值是20,20是右子树15 20 7的根节点,然后是2,值是7,然后是1,值是15
图是这样的:
class Solution {
int post_idx;
unordered_map<int, int> idx_map;
public:
TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& postorder){
// 如果这里没有节点构造二叉树了,就结束
if (in_left > in_right) {
return nullptr;
}
// 选择 post_idx 位置的元素作为当前子树根节点
int root_val = postorder[post_idx];
TreeNode* root = new TreeNode(root_val);
// 根据 root 所在位置分成左右两棵子树
int index = idx_map[root_val];
// 下标减一
post_idx--;
// 构造右子树
root->right = helper(index + 1, in_right, inorder, postorder);
// 构造左子树
root->left = helper(in_left, index - 1, inorder, postorder);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// 从后序遍历的最后一个元素开始
post_idx = (int)postorder.size() - 1;
// 建立(元素,下标)键值对的哈希表
int idx = 0;
for (auto& val : inorder) {
idx_map[val] = idx++;
}
return helper(0, (int)inorder.size() - 1, inorder, postorder);
}
};
附上先序和中序构建二叉树:
先序和中序
class Solution {
public:
int pre_idx;
unordered_map<int, int> idx_map;
TreeNode* helper(int in_left, int in_right, vector<int>& inorder, vector<int>& preorder){
// 如果这里没有节点构造二叉树了,就结束
if (in_left > in_right) {
return nullptr;
}
// 选择 pre_idx 位置的元素作为当前子树根节点
int root_val = preorder[pre_idx];
TreeNode* root = new TreeNode(root_val);
// 根据 root 所在位置分成左右两棵子树
int index = idx_map[root_val];
// 下标加一
pre_idx++;
// 构造左子树
root->left = helper(in_left, index - 1, inorder, preorder);
// 构造右子树
root->right = helper(index + 1, in_right, inorder, preorder);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
pre_idx = 0;
int idx = 0;
for(auto &i:inorder){
idx_map[i] = idx++;
}
return helper(0,(int)inorder.size() - 1,inorder,preorder);
}
};
第二题就是一个LRU的变种,PS:比原题简单多了
附上原题链接LRU