【问题标题】:Linear time solution for this线性时间解决方案
【发布时间】:2015-12-16 06:30:21
【问题描述】:

我在一次采访中被问到这个问题。

你站在 0 点,你必须到达位置 X。你可以跳到 D(1 到 D)。如果 X > D,很明显你在初始跳跃时无法到达 X 位置。

现在每秒钟都有从 1 到 N 的随机位置出现的图块。这是作为零索引数组 A[k] 给出的,其中 A[k] 表示图块在第 k 秒出现的位置。你必须找出,在哪一秒你有可能到达(或穿越)目的地 X。

如果在初始或 A[0] 之后有可能,则返回 0,或返回最小秒数。如果即使在所有瓷砖之后也不可能,则返回 -1。

约束: 1

1

1

1

例如。

X = 7,D=3

A = {1,3,1,4,2,5}

那么答案是 3。因为在第 3 个第二个图块出现在位置 4 并且有可能达到 X=7。在那之前的任何一秒都不可能。

我知道这是一个措辞过多的问题,但如果我不能很好地沟通,我绝对可以解决任何问题。

问题是预期时间复杂度为 O(N),您可以使用额外的空间 O(X)。

我找到了一个 O(n * log n * log n) 的解决方案。即对第二个进行二分搜索并获得第一个 [1..mid] 元素,按位置对它们进行排序并验证解决方案。它似乎通过了测试用例,但它不是线性的。

我很努力,但找不到任何 O(N) 解决方案。你能帮我么?

【问题讨论】:

  • 这不就是一个累计吗?
  • @GordonLinoff 你能提供更多细节吗?我很努力,但找不到直接的解决方案。我可能错过了一个基本点。我不确定当瓷砖出现在不同的秒数时如何使用累积和?
  • 每次跳跃也需要时间吗?
  • return the minimum second at which it becomes possible for you to reach (or cross) the destination X 和尽快提供结果之间似乎有一条细线,这需要在线算法。 (我认为即使是后者也是可能的——试试amortised analysis。)

标签: algorithm language-agnostic


【解决方案1】:

线性是指瓷砖数量的线性,对吧?

如果是这样,这个解决方案(在 Java 中)只迭代瓦片数组一次。

在每次迭代中,它还需要迭代 D 和 X 次,但它相对于 tile 数组的大小是线性的。

如果它听起来与您正在寻找的相似,请告诉我。

注意:为简化起见,我假设位置“0”的图块在第二个数字“0”处可用,因此有效地将第二个“0”视为您所在的图块存在的时间,然后其他图块出现在第 1 秒、第 2 秒等处。

    public class TestTiles {

        public static int calculateSecond(Integer x, Integer d, int[] tiles) {
            // start by making all positions unreachable (false)
            boolean[] posReachable = new boolean[x+1];
            // iterate each second only once
            for (int second = 0; second < tiles.length; second++) {
                int tile = tiles[second];  // this tile is available now
                // so mark all positions from "tile" to "tile + d" reachable
                for (int pos = tile; (pos <= tile + d) && pos <= x; pos++) {
                    posReachable[pos] = true;
                }
                // are all positions reachable now? if so, this is the second to return
                boolean reachable = true;
                for (int pos = 0; pos <= x; pos++) {
                    reachable &= posReachable[pos];
                }
                if (reachable) return second;
            }
            // we can't reach the position
            return -1;
        }

        public static void main(String[] args) {
            System.out.println(calculateSecond(7, 3, new int[]{0,1,3,1,4,2,5}));
            System.out.println(calculateSecond(20, 3, new int[]{0,1,3,1,4,2,5}));
            System.out.println(calculateSecond(2, 3, new int[]{0,1,3,1,4,2,5}));
            System.out.println(calculateSecond(4, 3, new int[]{0,1,3,1,4,2,5}));
            System.out.println(calculateSecond(15, 3, new int[]{0,12,3,9,6}));
        }

    }

【讨论】:

  • @eugenjoy 很抱歉我忘了提,我正在寻找具体的 O(N) 解决方案。你给定的解决方案是 O(N D)。我会更新问题描述
【解决方案2】:

我会按照数组中的图块一一处理,跟踪最大可达位置,并维护“待处理”图块的优先级队列。

  1. 如果瓷砖大于 X,请将其扔掉。
  2. 如果瓷砖已经在可触及区域内,请将其扔掉。
  3. 如果您目前无法到达该图块,请将其添加到待处理队列中。
  4. 如果图块扩展了可达区域,请这样做,并重新处理待处理队列中现在可达的最近的图块,或者在重新处理期间变得可达。
  5. (如果现在可以到达 X,则停止)。

每个图块最多处理两次,每次处理 O(1) 步,除了在小整数优先级队列中添加和删除 min 的成本,对此有专门的算法 - 请参阅https://cs.stackexchange.com/questions/2824/most-efficient-known-priority-queue-for-inserts那个。

【讨论】:

  • 但这也涉及删除。我知道这个解决方案会比我的更快。但是我无法得到是否存在 O(N) 算法。因为问题特别提到了实现 O(N) 算法,这很奇怪。
  • 这并不是真正的 O(N)。依赖位摆弄的整数优先级队列的一些实现可能接近线性时间,直到 N > 2^32 并且您需要更改为 64 位整数,但是您必须非常努力地争辩说结果是 O( N)。
  • 没错,我同意你的看法。但我也相信面试官并没有假设我可以在 60 分钟和 O(N) 内编写以上所有内容。所以我想知道这是否可以通过一些更简单的方法来实现。甚至可能是不可能的。
【解决方案3】:

[Python中的这个解决方案类似于mcdowella的;但它没有使用优先级队列,而是简单地使用大小为 X 的数组来存储最多为 X 的位置。它的复杂度是 O(N+min(N,X)*D),所以它不是真正的线性,而是 N 中的线性 ...]

数组world 跟踪位置 1,...,X-1。通过跳转到最远可达的图块,每个图块都会更新当前位置。

def jumpAsFarAsPossible(currentPos, D, X, world):
  for jump in range(D,0,-1): # jump = D,D-1,...,1
    reachablePos = currentPos + jump
    if reachablePos >= X:
      return X
    if world[reachablePos]:
      return jumpAsFarAsPossible(reachablePos, D, X, world)
  return currentPos

def solve(X,D,A):
  currentPos = 0
  # initially there are no tiles
  world = X * [False]

  for k,tilePos in enumerate(A):
    if tilePos < X:
      world[tilePos] = True

    # how far can we move now?
    if currentPos+D >= tilePos:
      currentPos = jumpAsFarAsPossible(currentPos, D, X, world)

    # have we reached X?
    if currentPos == X:
      return k # success in k-th second

  return -1 # X could not be reached

【讨论】:

    【解决方案4】:

    这是另一个尝试:

    创建一个大小为X的数组B。将其初始化为MAX_VALUE,然后填充元素B[A[i]] = min(B[A[i]], i),这样B的每个元素要么很大或第一次出现在该方块上的图块。

    将当前时间初始化为零,并沿 B 从左到右工作,尝试从 0 跳转到 X,最多跳过 D 的图块,使用不大于当前时间的 B 元素。如果你不能走得更远,请将当前时间增加到 B 中的任何方块中找到的最小值,让你跳得更远。

    成本是 O(X log(D)) + O(N) - 你通过 A 的成本 O(N) 来初始化 X,然后沿着 X 一步一步地工作。如果您保留一个优先级队列以在每个时间点覆盖 X 中的下一个 D 元素,您可以找到 X 的最小可达元素,其成本不超过 log(D) - 同样这些都是小整数,所以您可以做得更好。

    【讨论】:

      【解决方案5】:

      下面的提议应该花费时间 O(N * log(min(N, X/D)))。请注意,特别是在 O(N * log(N)) 中,因此比您提出的算法或 mcdowella 提出的优先级队列算法具有更好的界限;在 O(N * (X + D)) 中,因此比 eugenioy 提出的算法有更好的界限; 随着 D 的增加而增加(如 mcdowella 的数组算法、eugenioy 的算法和 coproc 的算法);而且对于固定的 X 在 O(N) 中。

      我们的想法是保留一组我们仍然需要为其找到路径的间隔。我们将把这个集合存储在一个平衡树中,它的键是区间的下限,值是区间的上限。当我们看到一个新的瓦片时,我们会找到包含这个瓦片的区间(如果有的话),并在瓦片周围分割区间,丢弃任何小于 D 的区间。当我们的地图为空时,我们就完成了。

      Haskell 中的完整实现如下。

      import Data.Ix
      import Data.Map
      import qualified Data.Map as M
      
      -- setup: the initial set of intervals is just the singleton from 0 to x
      search :: Int -> Int -> [Int] -> Maybe Int
      search d x = search_ d (insertCandidate d 0 x empty)
      
      search_ :: Int -> Map Int Int -> [Int] -> Maybe Int
      search_ d = go where
          -- first base case: we've found all the paths we care about
          go intervals _ | M.null intervals = Just 0
          -- second base case: we're out of tiles, and still don't have all the paths
          go _ [] = Nothing
          -- final case: we need to take a time step. add one, and recursively search the remaining time steps
          go intervals (tile:tiles) = (1+) <$> go newIntervals tiles where
              newIntervals = case lookupLE tile intervals of
                  Just (lo, hi) | inRange (lo, hi) tile
                      -> insertCandidate d lo tile
                      .  insertCandidate d tile hi
                      .  delete lo
                      $  intervals
                  _ -> intervals
      
      -- only keep non-trivial intervals
      insertCandidate :: Int -> Int -> Int -> Map Int Int -> Map Int Int
      insertCandidate d lo hi m
          | hi - lo <= d = m
          | otherwise    = insert lo hi m
      

      一些在 ghci 中运行的示例(我无耻地从其他答案中抄袭了示例):

      > search 3 7 [1,3,1,4,2,5]
      Just 4
      > search 3 20 [1,3,1,4,2,5]
      Nothing
      > search 3 2 [1,3,1,4,2,5]
      Just 0
      > search 3 4 [1,3,1,4,2,5]
      Just 1
      > search 3 15 [12,3,9,6]
      Just 4
      

      【讨论】:

        【解决方案6】:

        我的解决方案是O(N)+O(X/D)。我有两个论点(好吧,一个借口和一个真正的论点)来捍卫它为O(N)

        借口是我应该有O(X) 空间,我什至无法在O(N) 时间初始化它。所以我假设数组是预先初始化的,因为我的O(X/D) 部分只是将数组初始化为默认值,所以我很乐意忽略它。 (嘿,我说这是借口)。

        真正的论点X/D 不能真正大于N。我的意思是,如果我必须以最多D 个位置的步长移动X 位置,那么最小步数将是X/D(这意味着X/D-1 瓦片)。

        • 所以,X/D-1 &gt; N 的任何问题都是无法解决的。
        • 所以,算法很可能以if (X/D &gt; N+1) return -1 开头。
        • 所以,O(X/D) 永远不会大于 O(N)
        • 所以,O(N)+O(X/D) 实际上与O(N) 相同。

        也就是说,我的解决方案如下:

        数学设置

        我将假设一个“轨道”的位置为 0X,因此 0 位于左侧,X 位于右侧(我需要这个,因为我将要谈论“最左边的瓷砖”之类的东西)。轨道有X+1 位置,编号为0X。最初,0 有一个磁贴,X 有另一个磁贴。

        我将音轨分成块。块大小使得任何两个相邻块加起来正好是D 位置。第一个块是k 位置,第二个是D-k,第三个是k,第四个是D-k,依此类推,k 介于1D-1 之间。如果D 是偶数并且我们设置k=D/2,那么所有块的大小都是相等的。我觉得如果将k 设置为1 并成对处理块,我觉得实现可能会更容易一些,但我真的不知道(我还没有实现这个)并且算法对于任何一个都基本相同k 所以我会继续。

        最后一块可能会被截断,但我只是假设它是它应该的大小,即使这意味着它超出了X。那不重要。简单的例子,如果X=30D=13k=6,会有5大小为6-7-6-7-6的块(即0-56-1213-1819-24,@987654位置 31 不是轨道的一部分)。

        从现在开始,我将对块使用数组表示法,即将块号 k 称为 C[k]

        非常重要的是,两个相邻的块总是加起来正好是D 位置,因为它保证:

        • 如果每个块上都至少有一个瓦片,那么问题就解决了(即不再需要瓦片)。
        • 如果两个相邻的块上没有瓷砖,则需要更多的瓷砖。

        如果有一个块在前面的任何一种情况下都不落下(即其中没有瓦片,但前面和后面的块确实有瓦片),那么我们必须测量最左边的瓦片之间的距离在右边的块中,在左边的块中最右边的瓦片。如果这个距离小于等于D,那么这个chunk就没有问题了。

        总结一下:根据以下规则,有些块有问题,有些则没有:

        • 至少有一个图块的块永远不会有问题。
        • 没有瓷砖的块和也没有瓷砖的邻居(左、右或两者)总是有问题的。
        • 一个块 C[k] 没有瓦片,而邻居 C[k-1]C[k+1] 都带有瓦片,当且仅当 C[k+1].left - C[k-1].right &gt; D 是有问题的。

        还有,跳入问题解决方案的部分:

        • 当且仅当至少有一个有问题的块时,我们才需要更多的图块来完成轨道。

        所以,处理O(X) 位置的问题现在只处理O(N) 块。太棒了。

        实现细节

        在chunks数组中,每个chunkC[k]都会有以下属性:

        • 布尔型problematic,初始化为true
        • 整数left,初始化为-1
        • 整数right,初始化为-1

        另外,还会有一个problematicCounter,初始化为数组C中的元素个数。这将减少,当它达到零时,我们知道我们不需要更多的瓷砖了。

        算法是这样的:

        if (X/D > N+1) return -1;  // Taking care of rounding is left as an exercise
        
        Let C = array of chunks as described above
        
        For each C[k] // This is the O(X/D) part
        {
          Let C[k].problematic = true
          Let C[k].left = -1
          Let C[k].right = -1
        }
        
        Let problematicCounter = number of elements in array C
        
        Let C[k] be the chunk that contains position 0 (usually the first one, but I'll leave open the possibility of "sentinel" chunks)
        Let C[k].problematic = false
        Let C[k].left = 0
        Let C[k].right = 0
        Decrement problematicCounter
        
        // The following steps would require tweaking if there is one single chunk on the track
        // I do not consider that case as that would imply D >= 2*N, which is kind of ridiculous for this problem
        Let C[k] be the chunk containing position X (the last position on the track)
        Let C[k].problematic = false
        Let C[k].left = X
        Let C[k].right = X
        Decrease problematicCounter
        
        // Initialization done. Now for the loop.
        // Everything inside the loop is O(1), so the loop itself is O(N)
        For each A[i] in array A (the array specifying which tile to place in second i)
        {
          Let C[k] be the chunk containing position A[i]
        
          If C[k].problematic == true
          {
            Let C[k].problematic = false;
            Decrement problematicCounter
          }
        
          If C[k].first == -1 OR C[k].first > A[i]
          {
            Let C[k].first = A[i]
        
            // Checks that C[k-1] and C[k-2] don't go off array index bounds left as an exercise
            If C[k-1].problematic == true AND C[k-2].last <> -1 AND C[k].first - C[k-2].last <= D
            {
              Let C[k-1].problematic = false
              Decrement problematicCounter
            }
        
          If C[k].last == -1 OR C[k].last < A[i]
          {
            Let C[k].last = A[i]
        
            // Checks that C[k+1] and C[k+2] don't go off array index bounds left as an exercise
            If C[k+1].problematic == true AND C[k+2].first <> -1 AND C[k+2].first - C[k].last <= D
            {
              Let C[k+1].problematic = false
              Decrement problematicCounter
            }
        
            If problematicCounter == 0 Then return k // and forget everything else
        }
        
        return -1
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-05-31
          • 2020-10-24
          • 2020-11-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多