Solve and optimize menu combo DP
Company: Airbnb
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: Medium
Interview Round: Technical Screen
Quick Answer: This question evaluates dynamic programming and combinatorial optimization skills, focusing on state definition, transitions, memoization, pruning dominated offers, and minimizing state space for menu-combo minimum-cost problems in the domain of Coding & Algorithms.
Constraints
- 1 <= N <= 6
- 0 <= S <= 100
- 0 <= needs[i] <= 10
- 1 <= prices[i] <= 10^4
- Each offer is length N+1: first N non-negative integers are quantities, last is offer price (0 <= offer price <= 10^5)
- No extra items allowed: an offer can be applied only if all its quantities are <= the remaining needs
- All inputs are integers
Solution
from functools import lru_cache
from typing import List, Tuple
def min_order_cost(prices: List[int], offers: List[List[int]], needs: List[int]) -> int:
n = len(prices)
if n != len(needs):
raise ValueError("prices and needs length mismatch")
# Prune offers: wrong length, zero-quantity, exceeds needs, or not cheaper than individual
pruned: List[Tuple[Tuple[int, ...], int]] = []
for off in offers:
if len(off) != n + 1:
continue
qty = off[:-1]
cost = off[-1]
if all(q == 0 for q in qty):
continue
if any(q > needs[i] for i, q in enumerate(qty)):
continue
indiv = sum(prices[i] * qty[i] for i in range(n))
if cost >= indiv:
continue
pruned.append((tuple(qty), cost))
# Specialized bottom-up DP when N == 3
if n == 3:
a, b, c = needs
INF = 10**18
dp = [[[INF for _ in range(c + 1)] for _ in range(b + 1)] for __ in range(a + 1)]
for i in range(a + 1):
for j in range(b + 1):
for k in range(c + 1):
# baseline: buy individually
best = i * prices[0] + j * prices[1] + k * prices[2]
# try offers
for qty, ocost in pruned:
if i >= qty[0] and j >= qty[1] and k >= qty[2]:
cand = dp[i - qty[0]][j - qty[1]][k - qty[2]] + ocost
if cand < best:
best = cand
dp[i][j][k] = best
return dp[a][b][c]
# General N: top-down memoized DFS
@lru_cache(maxsize=None)
def dfs(state: Tuple[int, ...]) -> int:
# baseline: buy what's left individually
total = sum(state[i] * prices[i] for i in range(n))
# try each offer
for qty, ocost in pruned:
ok = True
nxt = [0] * n
for i in range(n):
if qty[i] > state[i]:
ok = False
break
nxt[i] = state[i] - qty[i]
if ok:
cand = ocost + dfs(tuple(nxt))
if cand < total:
total = cand
return total
return dfs(tuple(needs))
Explanation
Time complexity: O(S * Π_i (needs[i] + 1)) in the general case; for N = 3, O(S * (needs[0]+1)*(needs[1]+1)*(needs[2]+1)).. Space complexity: O(Π_i (needs[i] + 1)) for memoization/DP table; plus O(S) for pruned offers..
Hints
- Model the state as the remaining needs vector; use memoization with the state as a tuple.
- Baseline for any state: buy remaining items individually; try applying each valid offer to reduce the state.
- Prune offers whose price is >= the cost of buying their quantities individually, and offers that have any quantity exceeding needs.
- For N=3, build a bottom-up 3D DP of size (needs[0]+1)*(needs[1]+1)*(needs[2]+1), initializing with individual-buy costs and relaxing with offers.
- Skip offers that exceed current remaining needs to enforce the no-extras rule.