面试谜题:跳跃游戏

Posted

技术标签:

【中文标题】面试谜题:跳跃游戏【英文标题】:Interview puzzle: Jump Game 【发布时间】:2012-02-20 22:44:18 【问题描述】:

跳跃游戏: 给定一个数组,从第一个元素开始,通过跳跃到达最后一个元素。跳转长度最多可以是数组中当前位置的值。最佳结果是您以最少的跳跃次数达到目标。

什么是寻找最佳结果的算法?

一个例子:给定数组A = 2,3,1,1,4 到达末尾的可能方式(索引列表)是

    0,2,3,4(从 2 跳到索引 2,然后从 1 跳到索引 3,然后从 1 跳到索引 4) 0,1,4(从 1 跳转到索引 1,然后从 3 跳转到索引 4)

由于第二个解决方案只有 2 次跳跃,因此它是最佳结果。

【问题讨论】:

Fastest algorithm to hop through an array 的可能重复项 这能回答你的问题吗? Fastest algorithm to hop through an array 【参考方案1】:

概述

给定您的数组a 和您当前位置的索引i,重复以下操作直到到达最后一个元素。

考虑a[i+1]a[a[i] + i] 中的所有候选“跳转到元素”。对于索引e 处的每个此类元素,计算v = a[e] + e。如果其中一个元素是最后一个元素,则跳转到最后一个元素。否则,跳转到v最大的元素。

更简单地说,在触手可及的元素中,寻找能让你在下一个跳跃中走得最远的元素。我们知道这个选择 x 是正确的,因为与您可以跳转到的所有其他元素 y 相比,可从 y 访问的元素是可从 x 访问的元素的子集(除了来自向后跳跃,这显然是不好的选择)。

这个算法在 O(n) 中运行,因为每个元素只需要考虑一次(可以跳过第二次考虑的元素)。

示例

考虑值数组a、索引、i,以及索引和值的总和v

i ->  0   1   2   3   4   5   6   7   8   9  10  11  12
a -> [4, 11,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1]
v ->  4  12   3   4   5   6   7   8   9  10  11  12  13

从索引 0 开始并考虑接下来的 4 个元素。找到最大v 的那个。该元素位于索引 1 处,因此跳转到 1。现在考虑接下来的 11 个元素。目标触手可及,所以跳到目标。

演示

见here 或here with code。

【讨论】:

在这种情况下它是如何工作的:4,11,1,1,1,1,1,1,1,1,1,1,1 ? @ElKamina 我用你的问题修改了我的答案。 如果是 3、5、1、4、1、1、1、1 怎么办? @Shahbaz,从 0:3 开始。跳转到 1:5、2:1、3:4 中 v 最大的元素,其中 3:4 最大。 3 点 4 分,目标已近,所以跳到目标。 这不是最优的。您过早地分支,不能保证以后在这条路径上不会有很大的成本。再试一次:2,6,1,15,1,1,1,1,1,1,1,1,1,1,1,1。请注意,6+1 大于 1+2。只有对所有路径进行系统搜索才能保证解决方案,而动态编程只是缓存重复的结果以更快地完成。【参考方案2】:

动态编程。

假设您有一个数组B,其中B[i] 显示到达数组A 中的索引i 所需的最小步数。你的答案当然是在B[n],因为An 元素和索引从1 开始。假设C[i]=j 表示你从索引j 跳到索引i(这是为了恢复稍后采取的路径)

所以,算法如下:

set B[i] to infinity for all i
B[1] = 0;                    <-- zero steps to reach B[1]
for i = 1 to n-1             <-- Each step updates possible jumps from A[i]
    for j = 1 to A[i]        <-- Possible jump sizes are 1, 2, ..., A[i]
        if i+j > n           <-- Array boundary check
            break
        if B[i+j] > B[i]+1   <-- If this path to B[i+j] was shorter than previous
            B[i+j] = B[i]+1  <-- Keep the shortest path value
            C[i+j] = i       <-- Keep the path itself

需要的跳转次数是B[n]。需要走的路是:

1 -> C[1] -> C[C[1]] -> C[C[C[1]]] -> ... -> n

这可以通过一个简单的循环来恢复。

该算法的时间复杂度为O(min(k,n)*n),空间复杂度为O(n)nA 中的元素个数,k 是数组中的最大值。

注意

我保留了这个答案,但是cheheen 的贪心算法是正确且更有效的。

【讨论】:

您似乎已经考虑得很周到了,但它比我提供的解决方案要复杂。你看到我的解决方案有缺陷吗?编辑:糟糕,我刚刚注意到你是回复我答案的人,而不是 ElKamina。 其实是一个非常简单的动态规划方案。它甚至不去二维。另一方面我做了很多算法设计。 @Shahbaz:这是一个简单的DP解决方案,但在时间和空间复杂度上不如Cheeken的解决方案。我知道使用已知算法会更安全(当我看到这个问题时,DP 也是我想到的第一件事),但是 O(n)/O(1) 复杂度确实是很难打败。而且我发现它不太可能比具有“许多步骤”的反例能够实现无法在 20 个步骤中演示的东西。 @kalyanaramansanthanam,关于您的编辑:if B[i+j] &gt; B[i]+1 不需要有&gt;=,因为如果新路径与旧路径一样好,那么更新它就没有意义了。你不会获得任何更好的路径,而只是另一条同样好的路径。事实上,使用&gt;= 仍然可以,但它会产生与上述算法相同的最小跳转次数的不同路径。 @Shahbaz 就像你有数组 B[n],如果我们有一个数组,比如 C[n],C[i] = 到达 A[n] 所需的最小跳跃次数从“我”。我们可以从头开始,使得 C[n] = 0,我们的答案将在 C[1] 中。在每一步,如果距离 b/w 'n' 和位置 'i' 可以覆盖在 A[i] 中,则 C[i] = 1 否则 C[i] = C[i + A[i]] + 1 . 这个解决方案在运行时间和覆盖空间方面是线性的。【参考方案3】:

从数组构造一个有向图。例如: i->j if |i-j|j 作为图中的边)。现在,找到从第一个节点到最后一个节点的最短路径。

FWIW,您可以使用 Dijkstra 算法找到最短路径。复杂度为 O( | E | + | V | log | V | )。自从 | E |

【讨论】:

我不明白你为什么要 i-x[i]==j? @user973931 如果可以一步从索引 i 移动到 j,则将 i-> j 作为图中的边。 你甚至不需要 Djikstra 的。 BFS 很好,因为每条边都有一个恒定的权重。【参考方案4】:

我们可以计算远索引跳跃最大值,如果任何一个索引值大于远,我们将更新远索引值。

简单的 O(n) 时间复杂度解决方案

public boolean canJump(int[] nums) 
    int far = 0;
    for(int i = 0; i<nums.length; i++)
        if(i <= far)
            far = Math.max(far, i+nums[i]);
        
        else
            return false;
        
    
    return true;

【讨论】:

【参考方案5】:

从左(尾)开始..遍历直到数字与索引相同,使用这些数字的最大值。例如,如果列表是

   list:  2738|4|6927
   index: 0123|4|5678

一旦你从这个数字开始重复上述步骤,直到你到达极右。

273846927
000001234

如果您没有找到与索引匹配的内容,请使用索引最远且值大于索引的数字。在这种情况下为 7.(因为很快索引将大于数字,您可能只计算 9 个索引)

【讨论】:

【参考方案6】:

基本思路:

通过查找所有数组元素开始构建从结尾到开头的路径,从这些元素中可以最后一次跳转到目标元素(所有i,例如A[i] &gt;= target - i)。

将每个这样的i 视为新目标并找到通往它的路径(递归)。

选择找到的最小长度路径,附加target,返回。

python中的简单示例:

ls1 = [2,3,1,1,4]
ls2 = [4,11,1,1,1,1,1,1,1,1,1,1,1]

# finds the shortest path in ls to the target index tgti
def find_path(ls,tgti):

    # if the target is the first element in the array, return it's index.
    if tgti<= 0:
        return [0]

    # for each 0 <= i < tgti, if it it possible to reach
    # tgti from i (ls[i] <= >= tgti-i) then find the path to i

    sub_paths = [find_path(ls,i) for i in range(tgti-1,-1,-1) if ls[i] >= tgti-i]

    # find the minimum length path in sub_paths

    min_res = sub_paths[0]
    for p in sub_paths:
        if len(p) < len(min_res):
            min_res = p

    # add current target to the chosen path
    min_res.append(tgti)
    return min_res

print  find_path(ls1,len(ls1)-1)
print  find_path(ls2,len(ls2)-1)

>>>[0, 1, 4]
>>>[0, 1, 12]

【讨论】:

对于这类问题,带有良好命名变量的伪代码比带有错误命名变量的实际代码更有用(恕我直言)。你能用文字描述一下这个算法吗? 你说得对,我已经用解释和一些 cmets 更新了我的答案

以上是关于面试谜题:跳跃游戏的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode 面试最热100题 跳跃游戏

贪心——力扣55.跳跃游戏&&力扣45.跳跃游戏Ⅱ

[Leetcode]44.跳跃游戏Ⅰ&&45.跳跃游戏Ⅱ

(转)CocosCreator零基础制作游戏《极限跳跃》游戏分析

跳跃游戏 II

(转)CocosCreator零基础制作游戏《极限跳跃》制作游戏开始场景