Route a request along diagonal server redirects

0
Remix

Servers sit at distinct integer points on an infinite 2D grid. locations[i] = [x, y] is the position of server i. A request starts at server 0, which counts as visited.

You are given a list redirect_records of directions, each from 1 to 4. From the current server at (a, b), a direction picks a diagonal ray:

Direction Ray (for some Z >= 1)
1 (a + Z, b + Z) (northeast)
2 (a + Z, b - Z) (southeast)
3 (a - Z, b + Z) (northwest)
4 (a - Z, b - Z) (southwest)

For each direction in order, move the request to the unvisited server on that ray with the smallest Z, and mark it visited. If the ray has no unvisited server, skip the instruction and stay where you are.

Implement final_server(locations, redirect_records) -> [x, y], which returns the position of the server the request ends on.

Constraints: 1 <= n <= 10^5, 0 <= len(redirect_records) <= 10^5, coordinates in [-10^9, 10^9], all positions distinct. Aim for better than O(n * m).

Example 1

locations = [[0,0],[2,2],[5,5],[3,-3],[1,1],[-2,2]]
redirect_records = [1, 1, 1, 4]

Northeast goes to (1,1), then (2,2), then (5,5). Going southwest from (5,5), every server on that diagonal is visited, so the instruction is skipped. Answer: [5, 5].

Example 2

Same locations, redirect_records = [3, 2]. Northwest from (0,0) reaches (-2,2). Southeast from there passes the visited (0,0) and stops at (3,-3). Answer: [3, -3].

AmazonCodingHard

What L5 looks like

  • Reaches O(n log n + m) with doubly linked lists, and explains why the neighbour is the nearest unvisited server.
  • Notices the worst case of "sorted list + skip visited", where long runs of visited servers make it quadratic.
  • Tests edge cases on their own: one server, all skips, negative coordinates.
Interviewer expectations for every level ↓

Approach

Every diagonal move keeps one quantity fixed:

  • Northeast (1) and southwest (4) keep x - y fixed. The server lands on the same main diagonal.
  • Southeast (2) and northwest (3) keep x + y fixed. The server lands on the same anti-diagonal.

So "the nearest unvisited server in direction d" means: on the current server's diagonal, the closest unvisited server on the correct side, ordered by x. The side is larger x for directions 1 and 2, and smaller x for 3 and 4.

Brute force. For each instruction, scan all servers, keep the ones on the right diagonal and side that are not yet visited, and take the one with the smallest |dx|. That costs O(n) per instruction, so O(n·m) overall. It is fine as a first answer, but too slow when n and m both reach 10^5.

Optimal: sorted diagonals as doubly linked lists.

  1. Group server indices by x - y and by x + y. Sort each group by x.
  2. Link each group into a doubly linked list. Keep next/prev arrays per family, so each server has four pointers.
  3. The current server is always still in both of its lists. Its direct neighbour on the right side of the right list is the nearest unvisited server in that direction, because every visited server has already been unlinked.
  4. When the request moves, unlink the server it leaves from both lists in O(1). It is now visited and can never be chosen again.
  5. If the neighbour does not exist, skip the instruction. The request stays put and nothing is unlinked.

Invariant: the linked lists hold exactly the unvisited servers plus the current server, in x order along each diagonal. So neighbour lookup equals "nearest unvisited in that direction".

Code

from collections import defaultdict
from typing import Dict, List


def final_server(locations: List[List[int]], redirect_records: List[int]) -> List[int]:
    n = len(locations)
    # Family 0: main diagonals (x - y fixed). Family 1: anti-diagonals (x + y fixed).
    nxt = [[-1] * n, [-1] * n]
    prv = [[-1] * n, [-1] * n]

    for fam, key in ((0, lambda p: p[0] - p[1]), (1, lambda p: p[0] + p[1])):
        groups: Dict[int, List[int]] = defaultdict(list)
        for i, p in enumerate(locations):
            groups[key(p)].append(i)
        for members in groups.values():
            members.sort(key=lambda i: locations[i][0])
            for a, b in zip(members, members[1:]):
                nxt[fam][a] = b
                prv[fam][b] = a

    def unlink(i: int) -> None:
        for fam in (0, 1):
            p, q = prv[fam][i], nxt[fam][i]
            if p != -1:
                nxt[fam][p] = q
            if q != -1:
                prv[fam][q] = p
            prv[fam][i] = nxt[fam][i] = -1

    # direction -> (family, use next pointer?)
    moves = {1: (0, True), 4: (0, False), 2: (1, True), 3: (1, False)}

    cur = 0
    for d in redirect_records:
        fam, forward = moves[d]
        target = nxt[fam][cur] if forward else prv[fam][cur]
        if target == -1:
            continue  # no unvisited server that way: skip
        unlink(cur)  # the server we leave is now visited
        cur = target
    return list(locations[cur])


if __name__ == "__main__":
    pts = [[0, 0], [2, 2], [5, 5], [3, -3], [1, 1], [-2, 2]]
    assert final_server(pts, [1]) == [1, 1]
    assert final_server(pts, [1, 1, 1]) == [5, 5]
    # Back southwest from (5,5): (2,2) and (1,1) are visited, (0,0) too -> skip.
    assert final_server(pts, [1, 1, 1, 4]) == [5, 5]
    assert final_server(pts, [2]) == [3, -3]
    assert final_server(pts, [3]) == [-2, 2]
    assert final_server(pts, [3, 2]) == [3, -3]  # jumps over the visited (0,0)
    assert final_server([[0, 0], [1, 1]], [4, 2, 3]) == [0, 0]
    assert final_server([[7, 7]], [1, 2, 3, 4]) == [7, 7]
    print("ok")

Complexity

  • Preprocessing: O(n log n) to group and sort the diagonals. O(n) space for four pointer arrays and the groups.
  • Each redirect: O(1). One pointer lookup, plus an O(1) unlink when the request moves.
  • Total: O(n log n + m) time, O(n) space. The brute force is O(n·m).

Test cases

  • locations = [[0,0],[2,2],[5,5],[3,-3],[1,1],[-2,2]], [1] → [1,1]. The nearest northeast is (1,1), not (2,2).
  • Same, [1,1,1] → [5,5].
  • Same, [1,1,1,4] → [5,5]. Everything southwest is visited, so the last redirect is skipped.
  • Same, [2] → [3,-3]. Same, [3] → [-2,2].
  • Same, [3,2] → [3,-3]. The visited start (0,0) is jumped over.
  • [[0,0],[1,1]], [4,2,3] → [0,0]. Every instruction is skipped.
  • A single server with any instructions → that server.
  • Empty redirect_records → locations[0].
  • Large coordinates (±10^9) and negative values work, because keys are plain integers in a dict.

Follow-ups

  1. Return the full path or the number of skips. Append to a list or bump a counter inside the loop. The cost is unchanged.
  2. Revisiting allowed (only the nearest server counts, visited or not). Drop the unlinking. Each diagonal becomes a static sorted array, and the neighbour is the adjacent index. Store each server's position in its two arrays.
  3. Jump to the k-th nearest unvisited server. Linked lists no longer give O(1). Use a Fenwick tree per diagonal over the sorted positions, with 1 = unvisited, and find the k-th one after the current rank in O(log n).
  4. Servers added or removed while requests run. Replace each diagonal's list with a sorted container (balanced BST or SortedList) keyed by x. Neighbour queries and updates become O(log n).
  5. Eight directions (add horizontal and vertical). Add two more families keyed by y (sorted by x) and by x (sorted by y). Unlink from all four families when leaving a server.

Interviewer expectations

Mid-level (L4)

  • Sees that a diagonal move fixes x - y or x + y, and maps all four directions correctly.
  • Gets a working solution, even a brute force or sorted lists with a visited set and linear skipping.
  • Handles skipped redirects and the visited start server.

Senior (L5)

  • Reaches O(n log n + m) with doubly linked lists, and explains why the neighbour is the nearest unvisited server.
  • Notices the worst case of "sorted list + skip visited", where long runs of visited servers make it quadratic.
  • Tests edge cases on their own: one server, all skips, negative coordinates.

Staff (L6+)

  • States the invariant precisely and argues correctness from it.
  • Covers the dynamic and k-th variants with the right structures (balanced BST, Fenwick tree) and their trade-offs.
  • Discusses the input sizes that force the optimal version, and keeps the code small and reviewable.

Make it yours

Remix it: write each part yourself, grade it against this answer, then let the coach ask you follow-ups.