Design a balloon stability tracker
Company: Optiver
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Take-home Project
Quick Answer: This question evaluates event-driven simulation, time-ordered state management, floating-point numerical evaluation of aggregate functions, and data-structure design for tracking dynamic object states and stability thresholds; it falls under the Coding & Algorithms domain, specifically simulation, event processing, and algorithmic data structures.
Constraints
- 1 ≤ Q = len(operations) < 2^20
- 0 < altitude < 2^15 (for both balloon altitude and wind-anchor altitude)
- 0 ≤ windSpeed < 2^5
- timestamps are floats and must be nondecreasing across all calls
- yourBalloonNames contains unique strings
Examples
Input: (["A","B"], [("SetWindSpeed",0.0,1000.0,10.0),("BalloonAscended",0.0,"A",1000.0),("BalloonAscended",0.0,"X",1000.0),("InspectBalloons",0.0),("SetWindSpeed",10.0,1000.0,20.0),("InspectBalloons",10.0),("SetWindSpeed",20.0,1000.0,10.0),("InspectBalloons",319.0),("InspectBalloons",320.0)])
Expected Output: [True, True, True, ["A"], True, [], True, [], ["A"]]
Explanation: At t=0 both A (team) and X (competitor) are stable at altitude 1000, so A is rewarded. At t=10 wind becomes unsafe (>15), making both unstable, so no stable competitors => []. After wind becomes safe again at t=20, stability is regained only after 300s of continuous safe wind, so at t=319 still unstable; at t=320 both regain stability and A is rewarded again.
Input: (["T"], [("SetWindSpeed",0.0,1000.0,16.0),("BalloonAscended",0.0,"T",1000.0),("SetWindSpeed",1.0,1000.0,0.0),("InspectBalloons",301.0),("BalloonAscended",301.0,"C",1000.0),("InspectBalloons",301.0),("BalloonAscended",302.0,"C",1200.0),("InspectBalloons",302.0)])
Expected Output: [True, True, True, [], True, ["T"], True, []]
Explanation: T starts unstable (wind 16). Wind becomes safe at t=1, so T regains stability at t=301, but with no stable competitors inspection returns []. After competitor C ascends stable at t=301, T is rewarded. When C moves to altitude 1200, competitor max altitude becomes 1200 so T (at 1000) is no longer rewarded.
Input: (["A"], [("BalloonAscended",1.0,"A",100.0),("BalloonDescended",0.5,"A"),("InspectBalloons",1.0),("BalloonAscended",2.0,"A",100.0),("BalloonAscended",3.0,"A",200.0),("BalloonDescended",4.0,"A"),("BalloonDescended",4.0,"A")])
Expected Output: [True, False, [], False, True, True, False]
Explanation: Descending at timestamp 0.5 violates nondecreasing timestamps (last was 1.0) => False. Inspect has no stable competitors => []. Re-ascending at the same altitude while airborne is invalid => False. Changing altitude while airborne is allowed => True. Descending twice: the second descent fails because already on the ground.
Input: (["AA","AB"], [("SetWindSpeed",0.0,500.0,30.0),("BalloonAscended",0.0,"AA",500.0),("BalloonAscended",0.0,"BB",400.0),("InspectBalloons",0.0),("SetWindSpeed",10.0,500.0,0.0),("InspectBalloons",309.0),("BalloonAscended",310.0,"AB",600.0),("InspectBalloons",310.0)])
Expected Output: [True, True, True, [], True, [], True, ["AA", "AB"]]
Explanation: At t=0, AA is unstable (wind 30 at 500), but competitor BB at 400 is stable because wind there is exactly 15 (safe). No team balloon qualifies => []. Wind becomes 0 at t=10, AA starts its safe period and regains stability at t=310. AB ascends at t=310 stable; competitor BB is stable, max competitor altitude is 400, so both AA (500) and AB (600) are rewarded.
Solution
def solution(yourBalloonNames, operations):
import heapq
ALT_MAX = 2 ** 15
WIND_MAX = 2 ** 5
THRESH = 15.0
RECOVER = 300.0
EPS = 1e-9
team_set = set(yourBalloonNames)
class State:
__slots__ = ("is_team", "airborne", "altitude", "stable", "safe_start", "rec_ver", "comp_ver")
def __init__(self, is_team):
self.is_team = is_team
self.airborne = False
self.altitude = None
self.stable = True
self.safe_start = None
self.rec_ver = 0 # increments when (re)starting a safe period
self.comp_ver = 0 # increments when competitor's (altitude/airborne/stable) changes
# Wind anchors (altitude -> base speed)
anchors = {}
wind_version = 0
wind_cache = {} # altitude -> (wind_version, computed_value)
def wind_at(h):
nonlocal wind_version
key = h
cached = wind_cache.get(key)
if cached is not None and cached[0] == wind_version:
return cached[1]
total = 0.0
for A, s in anchors.items():
d = (h - A) / 100.0
total += s / (1.0 + d * d)
wind_cache[key] = (wind_version, total)
return total
balloons = {} # name -> State
# altitude -> set(names) for airborne balloons
alt_to_balloons = {}
# Heap of recovery deadlines for unstable balloons currently in a safe period
# items: (due_time, name, rec_ver)
recovery_heap = []
# Max heap for stable competitor balloons: (-altitude, name, comp_ver)
comp_heap = []
def comp_invalidate_and_maybe_push(name, st):
"""Call after any change that could affect competitor max queries."""
if st.is_team:
return
st.comp_ver += 1
if st.airborne and st.stable:
heapq.heappush(comp_heap, (-st.altitude, name, st.comp_ver))
def get_comp_max_altitude():
while comp_heap:
neg_alt, name, ver = comp_heap[0]
st = balloons.get(name)
if (st is None or st.is_team or (not st.airborne) or (not st.stable) or st.comp_ver != ver or st.altitude != -neg_alt):
heapq.heappop(comp_heap)
continue
return -neg_alt
return None
def start_safe_period_if_needed(name, st, now):
if st.stable:
return
if st.safe_start is None:
st.safe_start = now
st.rec_ver += 1
heapq.heappush(recovery_heap, (now + RECOVER, name, st.rec_ver))
def clear_safe_period(st):
st.safe_start = None
def advance_time(now):
# Promote balloons that have been continuously safe for >= 300 seconds.
while recovery_heap and recovery_heap[0][0] <= now + EPS:
due, name, ver = heapq.heappop(recovery_heap)
st = balloons.get(name)
if st is None or (not st.airborne) or st.stable:
continue
if st.safe_start is None or st.rec_ver != ver:
continue
# Must still be safe at current time.
if wind_at(st.altitude) <= THRESH + EPS:
st.stable = True
st.safe_start = None
comp_invalidate_and_maybe_push(name, st)
else:
# Should be rare (wind changes are processed as operations), but keep consistent.
st.safe_start = None
last_ts = float("-inf")
def process_timestamp(ts):
nonlocal last_ts
if ts < last_ts - EPS:
return False
advance_time(ts)
last_ts = ts
return True
def valid_alt(a):
return (a is not None) and (a > 0) and (a < ALT_MAX)
def valid_wind(w):
return (w is not None) and (w >= 0) and (w < WIND_MAX)
out = []
for op in operations:
kind = op[0]
if kind == "BalloonAscended":
_, ts, name, alt = op
if not process_timestamp(ts) or (not valid_alt(alt)):
out.append(False)
continue
st = balloons.get(name)
if st is None:
st = State(name in team_set)
balloons[name] = st
if st.airborne and abs(st.altitude - alt) <= EPS:
out.append(False)
continue
# Remove from old altitude bucket if needed
if st.airborne:
old = st.altitude
s = alt_to_balloons.get(old)
if s is not None:
s.discard(name)
if not s:
alt_to_balloons.pop(old, None)
# Apply ascent/altitude-change rules
st.airborne = True
st.altitude = alt
st.stable = True
st.safe_start = None
st.rec_ver += 1 # invalidate any pending recovery entries
alt_to_balloons.setdefault(alt, set()).add(name)
# Immediate stability evaluation
if wind_at(alt) > THRESH + EPS:
st.stable = False
st.safe_start = None
comp_invalidate_and_maybe_push(name, st) # invalidates competitor entries if any
else:
comp_invalidate_and_maybe_push(name, st) # pushes if competitor
out.append(True)
elif kind == "BalloonDescended":
_, ts, name = op
if not process_timestamp(ts):
out.append(False)
continue
st = balloons.get(name)
if st is None or (not st.airborne):
out.append(False)
continue
# Remove from altitude bucket
s = alt_to_balloons.get(st.altitude)
if s is not None:
s.discard(name)
if not s:
alt_to_balloons.pop(st.altitude, None)
st.airborne = False
st.altitude = None
st.stable = True
st.safe_start = None
st.rec_ver += 1
comp_invalidate_and_maybe_push(name, st) # invalidate competitor heap entries
out.append(True)
elif kind == "SetWindSpeed":
_, ts, alt, w = op
if not process_timestamp(ts) or (not valid_alt(alt)) or (not valid_wind(w)):
out.append(False)
continue
anchors[alt] = float(w)
wind_version += 1
wind_cache.clear()
# Wind changed: re-evaluate all airborne balloons (grouped by altitude)
# (stability can flip to unstable immediately)
for a, names in list(alt_to_balloons.items()):
ws = wind_at(a)
safe = ws <= THRESH + EPS
for nm in list(names):
st = balloons[nm]
if st.stable:
if not safe:
st.stable = False
clear_safe_period(st)
comp_invalidate_and_maybe_push(nm, st)
else:
if safe:
start_safe_period_if_needed(nm, st, ts)
else:
clear_safe_period(st)
out.append(True)
elif kind == "InspectBalloons":
_, ts = op
if not process_timestamp(ts):
out.append([])
continue
comp_max = get_comp_max_altitude()
if comp_max is None:
out.append([])
continue
res = []
for nm in yourBalloonNames:
st = balloons.get(nm)
if st is not None and st.airborne and st.stable and st.altitude >= comp_max - EPS:
res.append(nm)
res.sort()
out.append(res)
else:
raise ValueError(f"Unknown operation type: {kind}")
return out
Time complexity: Let A be the number of wind anchors, B be the number of airborne balloons, and U be the number of distinct altitudes among airborne balloons. Wind evaluation at one altitude is O(A). BalloonAscended/Descended are O(A + log B) (log B for heaps). SetWindSpeed is O(U·A + B) in the worst case (re-evaluating all airborne balloons grouped by altitude). InspectBalloons is O(|team| log |team| + log B).. Space complexity: O(A + B) for anchors, balloon states, altitude buckets, and heaps..
Hints
- Track each balloon as a small state machine: airborne/ground, stable/unstable, and (if unstable) when its current safe period started.
- To avoid scanning all unstable balloons every time, push (safe_start + 300) into a min-heap; when time advances, promote any balloons whose deadline has passed (and are still safe).