HackerRank · Optiver OA

Coding Problems

The Optiver online assessment includes 3 coding problems on HackerRank within a 2 hr 45 min window. All three problems below are drawn directly from December 2025 OA reports.

Dynamic programming (Problem 2) is the most time-consuming. Candidates recommend solving Problems 1 and 3 first, then spending remaining time on DP.

Problem 01 · Array / Fenwick Tree · Medium

Counting Trading Sequences

Source
Confirmed — December 2025 OA · dev.to report
Statement
Given a sequence of n stock prices (provided as decimal strings), count all profitable trading pairs (i, j) such that i < j and price[j] > price[i]. Return the count modulo 109 + 7.
Constraints
1 ≤ n ≤ 105 · prices given as decimal strings · time limit ~2 s
Example
Input:  ["1.5", "3.0", "2.5", "4.0", "2.0"]
Output: 6

Profitable pairs (buy_idx → sell_idx):
  (0,1)  1.5 → 3.0  ✓     (1,3)  3.0 → 4.0  ✓
  (0,2)  1.5 → 2.5  ✓     (2,3)  2.5 → 4.0  ✓
  (0,3)  1.5 → 4.0  ✓
  (0,4)  1.5 → 2.0  ✓
                           Count = 6
Pitfall — Floating-Point Precision

The most-reported failure mode: using Python's float() causes approximately 89 % of test cases to fail when prices have many decimal places. Use Decimal instead.

# Wrong — float loses precision at high decimal depth
prices = [float(p) for p in price_strings]

# Correct — Decimal stores the value exactly
from decimal import Decimal
prices = [Decimal(p) for p in price_strings]

This single issue is why candidates with correct logic score 0–30 %.

Hint — Brute Force vs Optimal

Brute force O(n²): two nested loops — correct but times out for n = 105.

def count_pairs_slow(prices):
    MOD, n, count = 10**9+7, len(prices), 0
    for i in range(n):
        for j in range(i+1, n):
            if prices[j] > prices[i]:
                count = (count + 1) % MOD
    return count

Optimal O(n log n) — Fenwick Tree: for each index j, count how many elements to its left are strictly less than price[j]. This is the classic "count smaller to the left" query.

  1. Sort all unique Decimal prices and assign integer ranks 1, 2, …, k.
  2. Iterate left to right. For price at rank r: query BIT for prefix sum up to r−1.
  3. Accumulate (mod 10⁹+7). Update BIT at rank r.
Solution — Python Reference
from decimal import Decimal
from typing import List

MOD = 10**9 + 7

def _update(bit: list, i: int, n: int) -> None:
    while i <= n:
        bit[i] += 1
        i += i & (-i)

def _query(bit: list, i: int) -> int:
    s = 0
    while i > 0:
        s += bit[i]
        i -= i & (-i)
    return s

def count_profitable_pairs(price_strings: List[str]) -> int:
    prices = [Decimal(p) for p in price_strings]   # Critical: Decimal
    sorted_unique = sorted(set(prices))
    rank = {v: i + 1 for i, v in enumerate(sorted_unique)}
    m = len(sorted_unique)
    bit = [0] * (m + 2)
    result = 0
    for p in prices:
        r = rank[p]
        result = (result + _query(bit, r - 1)) % MOD
        _update(bit, r, m)
    return result

assert count_profitable_pairs(["1.5","3.0","2.5","4.0","2.0"]) == 6
assert count_profitable_pairs(["5.0","4.0","3.0","2.0","1.0"]) == 0
assert count_profitable_pairs(["1.0","2.0","3.0","4.0","5.0"]) == 10
Problem 02 · Dynamic Programming / State Machine · Hard

Maximum Profit with Cooldown

Source
Representative DP — December 2025 OA · Linkjob / Glassdoor. The exact DP problem varies between cohorts; this is the most widely reported form and illustrates the state-transition technique confirmed in every account.
Statement
Given an array of daily stock prices, find the maximum profit. You may buy and sell as many times as you like, but you must sell before buying again, and after each sale you must wait one full cooldown day before buying.
Constraints
1 ≤ n ≤ 5 × 103 · 0 ≤ prices[i] ≤ 104
Example
Input:  [1, 2, 3, 0, 2]
Output: 3

Day 0: Buy  @ 1
Day 1: Sell @ 2  → +1    (Day 2 is cooldown)
Day 3: Buy  @ 0
Day 4: Sell @ 2  → +2
               Total = 3
Hint — State Machine DP

The cooldown constraint rules out a greedy approach. Define three states at each day i:

  • HOLD(i) — maximum profit while holding stock on day i
  • SOLD(i) — maximum profit having just sold on day i
  • REST(i) — maximum profit when free to buy (not in cooldown)
HOLD(i) = max(HOLD(i−1), REST(i−1) − price[i])   # hold or buy
SOLD(i) = HOLD(i−1) + price[i]                    # sell today
REST(i) = max(REST(i−1), SOLD(i−1))               # rest or end cooldown

Init:  HOLD(0) = −price[0],  SOLD(0) = 0,  REST(0) = 0
Answer: max(SOLD[n−1], REST[n−1])

O(n) time, O(1) space — keep only the previous day's values.

Trace — prices = [1, 2, 3, 0, 2]
Day  price   HOLD              SOLD              REST
 0     1     −1                0                 0
 1     2     max(−1, 0−2)=−1   −1+2 = 1          max(0, 0)=0
 2     3     max(−1, 0−3)=−1   −1+3 = 2          max(0, 1)=1
 3     0     max(−1, 1−0)= 1   −1+0 =−1          max(1, 2)=2
 4     2     max( 1, 2−2)= 1    1+2 = 3          max(2,−1)=2

Answer: max(SOLD[4], REST[4]) = max(3, 2) = 3  ✓
Solution — Python Reference
from typing import List

def max_profit_with_cooldown(prices: List[int]) -> int:
    if len(prices) < 2:
        return 0
    hold = -prices[0]
    sold =  0
    rest =  0
    for price in prices[1:]:
        hold, sold, rest = (
            max(hold, rest - price),
            hold + price,
            max(rest, sold),
        )
    return max(sold, rest)

assert max_profit_with_cooldown([1, 2, 3, 0, 2]) == 3
assert max_profit_with_cooldown([1])              == 0
assert max_profit_with_cooldown([1, 2])           == 1
assert max_profit_with_cooldown([2, 1])           == 0
Problem 03 · Simulation / Heap · Medium

Order Book Simulation

Source
Confirmed across multiple cohorts — dev.to / Glassdoor. The most trading-specific problem on the OA and the most consistently reported.
Statement
Process a stream of buy and sell limit orders for a single security and simulate a two-sided order book. Each order has a type, a limit price, and a quantity. Return all orders that remain unmatched after the full sequence is processed.
Matching rules
  • BUY — matches the cheapest available sell at or below the buy limit.
  • SELL — matches the most expensive available buy at or above the sell limit.
  • FIFO — among orders at the same price, earlier orders match first.
  • Partial fills — the unmatched remainder stays in the book.
Worked example
Input:
  BUY  102  10     BUY  103   3     SELL 100  15
  SELL 101   5     SELL 105   8

Step-by-step:
  BUY 102@10   No sells ≤ 102.          Book: buys={102:10}
  SELL 101@5   Best buy ≥ 101 = 102.    Match 5.   Book: buys={102:5}
  BUY 103@3    No sells ≤ 103.          Book: buys={103:3, 102:5}
  SELL 105@8   No buys ≥ 105.           Book: buys={103:3, 102:5}, sells={105:8}
  SELL 100@15  Match 103→qty 3 (rem 12), match 102→qty 5 (rem 7), no more buys.
               Book: buys={}, sells={100:7, 105:8}

Output: [("SELL", 100, 7), ("SELL", 105, 8)]
Hint — Data Structures

Two priority queues drive the matching engine:

  • Sell side — min-heap: key (price, insertion_time) — always pops the cheapest sell first.
  • Buy side — max-heap: key (-price, insertion_time) — always pops the most expensive buy first.

Heaps don't support in-place updates, so use lazy deletion: store remaining quantities in a dict[insertion_time → remaining]; skip any heap entry whose remaining quantity has been reduced to zero by an earlier partial fill.

The insertion_time tie-breaks equal prices in arrival order, enforcing FIFO.

Solution — Python Reference
import heapq
from typing import List, Tuple

def simulate_order_book(
    orders: List[Tuple[str, int, int]]
) -> List[Tuple[str, int, int]]:
    sell_heap: list = []          # min-heap (price, time)
    buy_heap:  list = []          # max-heap (-price, time)
    sell_rem:  dict[int,int] = {}
    buy_rem:   dict[int,int] = {}
    t = 0

    for order_type, price, size in orders:
        remaining = size

        if order_type == "BUY":
            while remaining > 0 and sell_heap:
                sp, st = sell_heap[0]
                if sp > price: break
                if sell_rem.get(st, 0) == 0:
                    heapq.heappop(sell_heap); continue
                heapq.heappop(sell_heap)
                fill = min(remaining, sell_rem[st])
                remaining -= fill; sell_rem[st] -= fill
                if sell_rem[st] > 0:
                    heapq.heappush(sell_heap, (sp, st))
            if remaining > 0:
                buy_rem[t] = remaining
                heapq.heappush(buy_heap, (-price, t))

        else:
            while remaining > 0 and buy_heap:
                nbp, bt = buy_heap[0]
                if -nbp < price: break
                if buy_rem.get(bt, 0) == 0:
                    heapq.heappop(buy_heap); continue
                heapq.heappop(buy_heap)
                fill = min(remaining, buy_rem[bt])
                remaining -= fill; buy_rem[bt] -= fill
                if buy_rem[bt] > 0:
                    heapq.heappush(buy_heap, (nbp, bt))
            if remaining > 0:
                sell_rem[t] = remaining
                heapq.heappush(sell_heap, (price, t))
        t += 1

    result, seen = [], set()
    for sp, st in sorted(sell_heap):
        if st not in seen and sell_rem.get(st, 0) > 0:
            seen.add(st); result.append(("SELL", sp, sell_rem[st]))
    for nbp, bt in sorted(buy_heap):
        if bt not in seen and buy_rem.get(bt, 0) > 0:
            seen.add(bt); result.append(("BUY", -nbp, buy_rem[bt]))
    return result
Edge Cases
  • Exact price match — BUY at 100 must match a SELL at exactly 100 (sell_price ≤ buy_limit).
  • Partial fill re-entry — remaining quantity re-enters the book at the same price and FIFO position.
  • Multi-level sweep — a large order can cross several price levels in a single step (see order 5 in the example).
  • Stale heap entries — always validate against the remaining-size dict before using a popped entry.