Find exit URL via BFS API calls
Company: Ramp
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates graph traversal and resilience when interacting with HTTP APIs, covering BFS/DFS reasoning, cycle detection to avoid infinite loops, retry handling for transient HTTP 500 responses, and timeout management for slow pages.
Constraints
- 1 <= number of paths (nodes) <= 10^4
- Each value in api is either the string "Congrats" or an object with key "next_step" (list of strings) and optional keys "fail_500" and "timeout" (non-negative integers).
- All paths and next_step entries are absolute and should start with "/"; if not, treat them as if "/" were prepended.
- The starting path "/" is present in api.
- Transient error counters are per-path and finite: a path returns HTTP 500 for its first fail_500 attempts, then times out for its next timeout attempts, then succeeds.
- Return the first exit discovered in BFS order; if none is reachable, return "".
Examples
Input:
Expected Output: ac
Solution
from collections import deque, defaultdict
def find_exit(api: dict) -> str:
# Track remaining transient failures per path, initialized lazily from api
left_500 = defaultdict(int)
left_timeout = defaultdict(int)
def attempt_get(path: str):
node = api.get(path)
if node is None:
# Unknown path: treat as a page with no outgoing links
return ("ok", {"next_step": []})
if node == "Congrats":
return ("ok", "Congrats")
if isinstance(node, dict):
# Initialize counters lazily so they persist across retries
if path not in left_500:
left_500[path] = int(node.get("fail_500", 0) or 0)
if path not in left_timeout:
left_timeout[path] = int(node.get("timeout", 0) or 0)
if left_500[path] > 0:
left_500[path] -= 1
return ("500", None)
if left_timeout[path] > 0:
left_timeout[path] -= 1
return ("timeout", None)
return ("ok", node)
# Malformed entry: treat as page with no outgoing links
return ("ok", {"next_step": []})
q = deque(["/"])
visited = set()
while q:
path = q.popleft()
if path in visited:
continue
status, data = attempt_get(path)
if status != "ok":
# Transient failure: retry later
q.append(path)
continue
if data == "Congrats":
return path.lstrip("/")
# Expand neighbors
steps = data.get("next_step", []) if isinstance(data, dict) else []
if isinstance(steps, list):
for s in steps:
if not isinstance(s, str):
continue
np = s if s.startswith("/") else "/" + s
if np not in visited:
q.append(np)
visited.add(path)
return ""
Explanation
Time complexity: O(V + E + R), where V is the number of reachable paths, E is the total number of next_step edges explored, and R is the total count of transient errors/timeouts across reachable paths.. Space complexity: O(V).
Hints
- Use a queue for BFS and a visited set, but only mark a node visited after a successful fetch.
- On HTTP 500 or timeout, re-enqueue the same path to retry later instead of marking it visited.
- Normalize next_step entries to absolute paths by prepending "/" when missing.