Implement node messaging and path discovery
Company: OpenAI
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates skills in distributed systems and algorithms, including reliable message-passing between adjacent nodes, graph traversal and path discovery, fault-tolerant protocol design, and message/time complexity analysis.
Part 1: Reliable Adjacent sendMessage
Constraints
- 0 <= max_retries <= 200000
- 0 <= len(events) <= 200000
- Each events[i] contains exactly two booleans
Examples
Input: ([(True, True)], 3)
Expected Output: (True, 1, 1)
Explanation: The message and ACK both succeed on the first attempt.
Input: ([(True, False), (True, True)], 3)
Expected Output: (True, 2, 1)
Explanation: The first ACK is lost, so the sender retries. The receiver sees a duplicate but delivers to the application only once.
Input: ([(False, False), (False, False)], 2)
Expected Output: (False, 2, 0)
Explanation: The message never reaches the receiver.
Input: ([], 3)
Expected Output: (False, 3, 0)
Explanation: Edge case: no event data is provided, so every attempt fails.
Input: ([(True, False), (False, False), (True, False)], 3)
Expected Output: (False, 3, 1)
Explanation: The receiver gets the message, but the sender never receives an ACK before retries run out.
Solution
def solution(events, max_retries):
delivered = False
app_deliveries = 0
attempts = 0
for i in range(max_retries):
attempts += 1
msg_delivered, ack_delivered = events[i] if i < len(events) else (False, False)
if msg_delivered:
if not delivered:
delivered = True
app_deliveries = 1
if ack_delivered:
return (True, attempts, app_deliveries)
return (False, attempts, app_deliveries)
Time complexity: O(max_retries). Space complexity: O(1).
Hints
- Track whether the receiver has already accepted the message once.
- The sender stops immediately on the first attempt where both the message reaches the receiver and the ACK reaches the sender.
Part 2: Count Total Nodes with Tree Message Passing
Constraints
- 0 <= n <= 200000
- If n > 0, edges has length n - 1
- 0 <= root < n when n > 0
- The input graph is a tree
Examples
Input: (0, [], -1)
Expected Output: (0, 0, 0)
Explanation: Edge case: empty network.
Input: (1, [], 0)
Expected Output: (1, 0, 0)
Explanation: A single-node tree needs no messages.
Input: (5, [(0, 1), (1, 2), (2, 3), (3, 4)], 0)
Expected Output: (5, 8, 8)
Explanation: A chain of height 4 uses 2*(5-1)=8 messages and 2*4=8 rounds.
Input: (7, [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)], 0)
Expected Output: (7, 12, 4)
Explanation: A complete binary tree of height 2 uses 12 messages and 4 synchronous rounds.
Input: (4, [(1, 0), (1, 2), (1, 3)], 1)
Expected Output: (4, 6, 2)
Explanation: The root need not be node 0.
Solution
def solution(n, edges, root):
if n <= 0 or root < 0:
return (0, 0, 0)
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
stack = [(root, -1, 0)]
count = 0
height = 0
while stack:
node, parent, depth = stack.pop()
count += 1
if depth > height:
height = depth
for nei in adj[node]:
if nei != parent:
stack.append((nei, node, depth + 1))
messages = 0 if count <= 1 else 2 * (count - 1)
rounds = 0 if count <= 1 else 2 * height
return (count, messages, rounds)
Time complexity: O(n). Space complexity: O(n).
Hints
- In a tree protocol like this, each edge carries one request downward and one reply upward.
- The completion time in synchronous rounds is determined by the farthest leaf from the root.
Part 3a: Path from a Node to the Root
Constraints
- 0 <= len(parent) <= 200000
- If parent is non-empty, it represents a valid rooted tree
- If parent is empty, start will be -1
Examples
Input: ([-1, 0, 0, 1, 1], 4)
Expected Output: [4, 1, 0]
Explanation: Node 4 goes to 1, then to root 0.
Input: ([-1], 0)
Expected Output: [0]
Explanation: Edge case: a single-node tree.
Input: ([], -1)
Expected Output: []
Explanation: Edge case: empty tree.
Input: ([2, 2, -1, 1], 3)
Expected Output: [3, 1, 2]
Explanation: The root is node 2, not node 0.
Solution
def solution(parent, start):
if not parent or start == -1:
return []
path = []
node = start
while node != -1:
path.append(node)
node = parent[node]
return path
Time complexity: O(length of path). Space complexity: O(length of path).
Hints
- Keep following parent[start] until you reach -1.
- You do not need DFS or BFS because every node already knows its parent.
Part 3b: Path Between Two Arbitrary Nodes
Constraints
- 0 <= len(parent) <= 200000
- If parent is non-empty, it represents a valid rooted tree
- If parent is empty, u and v will both be -1
Examples
Input: ([-1, 0, 0, 1, 1, 2], 3, 5)
Expected Output: [3, 1, 0, 2, 5]
Explanation: The path goes up from 3 to the root and then down to 5.
Input: ([-1, 0, 1, 2], 3, 1)
Expected Output: [3, 2, 1]
Explanation: One node is an ancestor of the other.
Input: ([-1, 0, 0, 1], 2, 2)
Expected Output: [2]
Explanation: Edge case: the path from a node to itself is just that node.
Input: ([], -1, -1)
Expected Output: []
Explanation: Edge case: empty tree.
Input: ([2, 2, -1, 1, 1], 4, 0)
Expected Output: [4, 1, 2, 0]
Explanation: The root is node 2.
Solution
def solution(parent, u, v):
if not parent or u == -1 or v == -1:
return []
path_u = []
node = u
while node != -1:
path_u.append(node)
node = parent[node]
path_v = []
node = v
while node != -1:
path_v.append(node)
node = parent[node]
i = len(path_u) - 1
j = len(path_v) - 1
while i >= 0 and j >= 0 and path_u[i] == path_v[j]:
i -= 1
j -= 1
return path_u[:i + 2] + path_v[:j + 1][::-1]
Time complexity: O(height(u) + height(v)). Space complexity: O(height(u) + height(v)).
Hints
- Build each node's path to the root, then find where those two paths stop matching.
- The last common node on the two root-paths is the lowest common ancestor.
Part 5: Simulate Failure Handling with Retries, ACKs, Idempotency, and Termination
Constraints
- 0 <= len(commands) <= 100000
- 0 <= len(events) <= 100000
- 0 <= max_retries <= 100000
- All command IDs are distinct
- Each ID in received_ids belongs to commands and is either the current command or an older command
Examples
Input: ([], [], 3)
Expected Output: (True, 0, [], 0)
Explanation: Edge case: there is nothing to send, so the protocol is already finished.
Input: ([7], [([7], False, False), ([7, 7], True, False)], 3)
Expected Output: (True, 1, [7], 2)
Explanation: The first ACK is lost, so the sender retries. The receiver applies command 7 only once.
Input: ([1, 2], [([1], True, False), ([1, 1, 2], False, False), ([1, 2], True, False)], 3)
Expected Output: (True, 2, [1, 2], 3)
Explanation: Command 2 is applied before it is ACKed, and stale duplicates of command 1 are ignored.
Input: ([5, 6], [([5], True, False), ([6], True, True)], 3)
Expected Output: (False, 1, [5, 6], 2)
Explanation: The receiver applies command 6 and then crashes before the ACK can be observed.
Input: ([42], [], 2)
Expected Output: (False, 0, [], 2)
Explanation: The sender exhausts retries because no attempt delivers the command.
Solution
def solution(commands, events, max_retries):
if not commands:
return (True, 0, [], 0)
valid = set(commands)
applied_seen = set()
applied_order = []
attempts = 0
event_idx = 0
cmd_idx = 0
retries_for_current = 0
crashed = False
while cmd_idx < len(commands):
if crashed or retries_for_current == max_retries:
return (False, cmd_idx, applied_order, attempts)
current = commands[cmd_idx]
attempts += 1
retries_for_current += 1
if event_idx < len(events):
received_ids, ack_current, crash = events[event_idx]
else:
received_ids, ack_current, crash = ([], False, False)
event_idx += 1
for cmd in received_ids:
if cmd in valid and cmd not in applied_seen:
applied_seen.add(cmd)
applied_order.append(cmd)
if crash:
crashed = True
return (False, cmd_idx, applied_order, attempts)
if ack_current and current in received_ids:
cmd_idx += 1
retries_for_current = 0
return (True, cmd_idx, applied_order, attempts)
Time complexity: O(total_attempts_used + total number of IDs across processed received_ids). Space complexity: O(len(commands)).
Hints
- Keep separate state for what the sender is waiting for and what the receiver has already applied.
- A crash after processing received_ids still means the current command is unacknowledged unless the ACK was observed before the crash, which this model does not allow.