跳过数组的最快算法

13

从正数数组A的索引0开始。从索引i,您可以移动到索引i+x,其中x <= A[i]。目标是找到到达数组末尾所需的最小移动次数。

以下是一个示例:

{ 2 , 4 , 1 , 2 , 3 , 2 , 4 , 2} 

如果你每次都尽可能地走得更远,那么你将得到以下结果:

0 , 2 , 3 , 5 , 7

这需要4个步骤。但你可以通过以下方法更快地完成它。

0 , 1 , 4 , 7

这只需要3步。

我想了一会儿,做了我能想到的第一件事,但是思考了几天后,我仍然不知道如何更好地完成它。

以下是我的想法。从数组的末尾开始,并跟踪从某个位置到末尾的最小移动次数。所以对于这个例子,moves[7] = 0,因为它已经是末尾了。然后moves[6] = 1,因为到达末尾需要一次移动。我的公式是

moves[i] = 1 + min(moves[i+1], moves[i+2], ... , moves[i+A[i]])

当我到达开头的时候,我知道移动的次数。

所以这是O(n^2),我猜还有更快的方法吧?


1
你的算法(使用所谓的“动态规划”或“贝尔曼原理”)是完全正确的,可能也是人们期望的。将其视为图形问题的优点是可以使用现有的黑盒算法,但并不一定更好。此外,如果数组的条目受到数字K的限制,则它实际上并不是O(n^2)(那时它变成了O(n))。 - Alexandre C.
这是一个古老或特别著名的问题吗?我问这个问题的原因是,如果是的话,大约一年前我在一个休闲比赛中独立想出了它作为一个问题。我不是特别想获得荣誉,但如果我发明的东西被用作好公司的面试问题,那将是非常酷的事情。注意:我知道这可能是一个非常古老的问题,并且考虑到它有多容易制定/理解,我的独立发现不应该会让人感到震惊。 - Patrick87
1
@Patrick:这种问题通常是在学习动态规划时作为练习题给出的。任何一本半认真的算法书都应该有成百上千个这样的问题。 - Alexandre C.
@Alexandre:没错,就像我猜测的那样,这只是一个玩具DP问题。它太琐碎了,没有名字吗? - Patrick87
那这只是一个谜题吗?因为要知道可以采取哪种方式,您必须已经访问了每个索引,所以最终,您会进行更多的移动,访问每个索引,然后再次访问其中一些吗? - user unknown
1
可能是面试谜题:跳跃游戏的重复问题。 - Vitalii Fedorenko
10个回答

13

因为你可以选择任何一个x在[1,A[i]]之间,所以我猜有一个相当简单的解决方案:

从0开始:

选择下一个可达的元素,从中可以到达更远的元素。

i.e 选择i使得i+A[i+x]在[1,A[i]]内最大,其中x∈[1,A[i]]

直到到达列表的结尾。


例子:

{2 , 4 , 1 , 2 , 3 , 2 , 4 , 2}

从0开始

从0可以到达1或者2:

  • 从1可以到达4
  • 从2可以到达3

因此max(0+A[0+x])为i=1

选择1 从1可以到达2 3 4:

  • 从4可以到达7
  • 从3可以到达5
  • 从2可以到达3

因此max(1+A[1+x])为i=4

选择4

你可以到达7

停止

the resulting list is : 

0,1,4,7

根据我的评论,我认为复杂度为O(N),因为从i到i+x+1至少需要2*x步。


'伪证明'

你从0开始(这是最优的)

然后你选择i使得(0+A[0+x])最大化(即最大化下一个元素的可达性)

从i开始,你可以到达任何其他从0可达的元素(它是一个长句,但意思是:谁能做更多,就能做更少。如果i不是最优的,那么它和最优的一样好)

所以i是最优的

然后按照这个推理逐步进行,它证明了该方法的最优性。

如果有人知道如何更数学化地表述,请随意更新它。


1
你能证明这个贪心算法总是可行的吗?我对它在所有情况下都正确持怀疑态度。 - templatetypedef
1
@Daniel,我认为最大化(i+A[i+x])是正确的,因此它与您的示例不同。在第二步中,我更愿意选择1,因为从1可以到达4,但从2只能到达3。(我会添加一个例子) - Ricky Bobby
一个例子会很好。谢谢! - Daniel
@Ricky Bobby- 你如何在O(1)的时间复杂度内完成每个步骤?难道不需要在每一步中遍历(可能)O(n)个条目吗?(你可能会争辩说从平摊意义上来看它是O(1),但我不确定如何证明) - templatetypedef
我认为这实际上不起作用。让我们稍微改变一下你的例子,看看它是否仍然有效。如果输入是数组A,即A = [2, 4, 1, 2, 3, 0, 4, 2],现在如果我们从0开始,即A[0] = 2,则:从0可以到达1或2[A[1] = 4和A[2] = 1]:从1可以到达5,因为A[1] = 4 从2可以到达3,因为A[2] = 1 因此,当i = 1时,max(0 + A[0 + x])最大但是,正如您所看到的,A[5] = 0,我们无法继续前进,因此这种方法行不通。 - Ali_IT
显示剩余12条评论

4
将数字数组视为图形,那么这个问题就等同于最短路径问题,可以使用Dijkstra算法O(|E|+|V|log|V|)的时间内解决。
其中,E = 数字之和,V = 数字数量。

但是在这里,E 是 O(n^2),因为每个数字可能与 O(n) 个其他数字相连。这可能不比 OP 的解决方案更好。 - templatetypedef
此外,您无法在O(N)时间内转换图形。对于O(N)个节点中的每个节点,可能有O(N)个后继节点,从而得到O(N ^ 2)算法。 - templatetypedef
再看一遍,我相信你可以将它视为一个图形。因为所有“跳转”节点的信息已经包含在OP定义的结构中,无需预先计算。 - Kendall Hopkins

2
使用您的基本想法,但从头开始,您可以获得 O(n)。
目标是使序列(A_i1,A_i2,...,A_ik,...)满足以下条件:
1. 位置0,1,2,...,ik 可以在 k 步或更少的步骤中到达; 2. 位置 i(k-1)+1, i(k-1)+2,...,ik 不能在少于 k 步的步骤中到达。
基本情况很容易:
i0 = 0
i1 = A[0]

感性部分并不太复杂:

i(k+2) = max { A_(ik+1) + ik , A_(ik+1) + ik+1, ..., A_(i(k+1)) + i(k+1) }

1
这真的是O(n)吗?在每一步中进行动态规划最坏情况下需要O(n)的时间,不是吗? - templatetypedef
我相当确信它是O(n)的,因为确定i(k+2)只查看了两个条目。我将不得不编写代码来确保。我会发布一个编码版本并证明或撤回O(n)声明。 - PengOne

2
“我会违反常规地告诉你,你的算法是‘完美的’。它以最简洁的形式使用了动态规划,而且它的复杂度也不错。从这个意义上讲,我认为它很可能是面试官对你的期望。如果你对输入有一个界限(比如A[i] <= C(N)),那么它的复杂度是O(N * max(C(N), N))。例如,如果所有的输入都小于K,那么它的复杂度是O(N)。使用Dijkstra算法(或者更一般地将问题简化为最短路径问题)是聪明的,但我把它排在清晰的DP解决方案后面,因为图形算法是复杂的(如果你被问及它们,它可能会适得其反)。请注意,Dijkstra的复杂度为O(N C(N) + N log N)(N个顶点和N C(N)条边)。因此,根据C的大小,你的复杂度要么严格优于,要么等于。”

感谢您的验证(和解释)! - Daniel

1

你可以将它表述为一个图算法(实际上,哪个问题不能呢?)。让数组中的位置成为顶点,而可能的目的地从每个顶点出发。在你的例子中,顶点0将有边到1和2,而顶点1将有边到2、3、4和5。

有几种高效的图搜索算法。例如,Dijkstra的算法是O(|E| + |V|log|V|),而A*算法是O(log h*),如果你能想出一个好的启发式算法,那么它会更好。


但是,难道不是有n^2个边吗?我的答案和Djikstra的一样好,对吧?我不知道A和h是什么。 - Daniel
1
是的,你可能和Dijkstra一样优秀。然而,A*算法受启发式(我的回答中的'h')指导,将首先搜索最有前途的边缘,因此很可能会击败你的算法。例如对于一个有n^2条边的图,它将在一步中找到解决方案(因为对于这种情况,有一条直接通向终点的边)。 - carlpett
1
@Daniel:A*是另一种最短路径图算法,您可以使用自己的启发式算法。 - Alexandre C.

0
我的方法: 创建一个数组reqSteps来存储输入所需的逃脱步数。
从数组末尾开始。
检查input[i]是否可以自行逃脱数组,如果是,则在minSteps中输入1;如果不是,则存储连续input[i]值的最小值+1。 结果为minSteps[0];
对于输入{10, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1},上述方法无效;
它会给出9作为答案。
正确答案是2。
public static void arrayHop()
{
        int[] input = { 10, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1 };
        int length = input.length;
        int answer = calcArrayHop(input, length);

}

public static int calcArrayHop(int[] input, int length) {
    int minSteps;
    int[] reqSteps = new int[length];
    for(int i=0;i<length;i++)
        reqSteps[i]=Integer.MAX_VALUE;
    int nextStep;

    for (int i = length - 1; i >= 0; i--) {
        int minsteps = Integer.MAX_VALUE;
        if (i + input[i] >= length) {
            reqSteps[i] = 1;
        } else
        {
            for (int j = i+1; j <= (i + input[i]); j++) {
                if(j>input.length-1)
                    break;
                if (reqSteps[j] < minsteps)
                    minsteps = reqSteps[j];
            }
        reqSteps[i] = minsteps+1;
        }


    }

    return reqSteps[0];
}

}


0

这是对Ricky Bobby答案的轻微修改,我将展示其优化:

find_shortest_path(A):
    path := [0]
    last := 0
    max_reachable = 0

    while A[last] + last < length(A) :  
        next_hop := x such that max_reachable < x <= A[last] + last and last + A[x] is maximum
        push(path, x)
        max_reachable = A[last] + last
        last := x

    return path

正确性证明: 我将使用归纳法来证明由我的算法创建的路径上的节点。

我要展示的属性是P(i) = 我的路径的第i个节点具有不小于任何最优路径的第i个节点的“可达性”

其中,可达性被定义为从该节点可以跳到的最高节点的数量,或者如果您可以越过数组的末尾,则为+无穷大

P(0)是显然的。

假设对于k >= 0,P(k)成立

现在考虑由我的算法创建的路径中的第(k + 1)个节点。由于我的算法选择了节点k,使其具有与最优路径的节点k相同的可达性,因此可能成为我的算法的第(k + 1)个节点的节点集合是任何最优路径的节点集合的超集。由于我的算法选择具有最大可达性的节点,因此P(K + 1)成立。

通过归纳法,P(k)对于所有k(直到创建的路径的大小)都成立。

由于我的算法将在到达数组末尾时结束,而且这将不会晚于任何最优路径,因此可以得出结论,我的算法创建的路径是最优的。

最优性证明: 每个数组单元格最多只被考虑一次,因此它的时间复杂度为O(n),这是渐进最优的。我认为在每种情况下都无法设计出检查更少单元格的算法。


0
动态规划解决方案:
对于每个元素,记录您可以到达该元素的最小步数以及您来自哪里。然后只需简单地遍历数组,并为每个元素更新可用的下一个位置(从i + 1到i + a [i])。
{ 2 , 4 , 1 , 2 , 3 , 2 , 4 , 2} 
  0

{ 2 , 4 , 1 , 2 , 3 , 2 , 4 , 2} 
  0   1   1 (num of steps)
      0   0 (source)
  ^         (current position)
{ 2 , 4 , 1 , 2 , 3 , 2 , 4 , 2} 
  0   1   1   2   2   2
      0   0   1   1   1
      ^
{ 2 , 4 , 1 , 2 , 3 , 2 , 4 , 2} 
  0   1   1   2   2   2
          ^
etc...

这是O(n+sum(a[i])).. 或稍微少一点,你不必越过数组的边界。

0
你可以将数组转换成图形并找到最短路径。以下是从数组到图形的转换方式。
每个数组元素都是一个节点。根据数组元素中的值,应该在节点之间绘制一条边,以便我们可以跳转到其他索引(节点)。一旦我们有了这个图形,我们就可以找到最短路径,这比O(n^2)更好。

http://i.imgur.com/Ih3UP.png


@Daniel:谢谢你指出来。我是SO的新手,没想到会看到所有答案都在同一时间发布。 - grdvnl

0

我的天真方法 - 从开始出发,通过所有路径进行广度优先搜索(子节点为A [i + 1] .. A [i + n]),将找到的路径保存到某个数组中,然后获取最短路径。当然,所有索引i + n> length(A)都被丢弃。因此,它的上限是O(n * min(n,max(A [i = 0..n]))+ n)-在实践中应该小于二次方。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接