面试谜题:跳跃游戏
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]
,因为A
有n
元素和索引从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)
。 n
是 A
中的元素个数,k
是数组中的最大值。
注意
我保留了这个答案,但是cheheen 的贪心算法是正确且更有效的。
【讨论】:
您似乎已经考虑得很周到了,但它比我提供的解决方案要复杂。你看到我的解决方案有缺陷吗?编辑:糟糕,我刚刚注意到你是回复我答案的人,而不是 ElKamina。 其实是一个非常简单的动态规划方案。它甚至不去二维。另一方面我做了很多算法设计。 @Shahbaz:这是一个简单的DP解决方案,但在时间和空间复杂度上不如Cheeken的解决方案。我知道使用已知算法会更安全(当我看到这个问题时,DP 也是我想到的第一件事),但是 O(n)/O(1) 复杂度确实是很难打败。而且我发现它不太可能比具有“许多步骤”的反例能够实现无法在 20 个步骤中演示的东西。 @kalyanaramansanthanam,关于您的编辑:if B[i+j] > B[i]+1
不需要有>=
,因为如果新路径与旧路径一样好,那么更新它就没有意义了。你不会获得任何更好的路径,而只是另一条同样好的路径。事实上,使用>=
仍然可以,但它会产生与上述算法相同的最小跳转次数的不同路径。
@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] >= 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 更新了我的答案以上是关于面试谜题:跳跃游戏的主要内容,如果未能解决你的问题,请参考以下文章