SWE / SDE-2 / FDE · Phone Coding / OOD

Rippling Delivery Billing:独立订单计费、截止结算与 24 小时并发司机

把高频三段题合成一篇:累计订单费用 O(1) 查询、按结束时间堆结算、逐司机合并区间后扫描并发峰值。

题目重点:把高频三段题合成一篇:累计订单费用 O(1) 查询、按结束时间堆结算、逐司机合并区间后扫描并发峰值。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 时间为 Unix 秒,配送 (start,end) 满足 0<时长≤3h;展示例按半开 [start,end);司机已注册且输入有效。
  • 训练实现用 Decimal(str(hourly_rate)) 降低金额误差,公开 API 返回 float; 的该轮面试官明确说 cents 仍要保留小数,不能未经约定就把每单付款四舍五入到整数 cents。真实发薪必须在约定节点做舍入并使用货币单位/账务规则。
  • `pay_up_to(cutoff)` 只结算当时已经记录且 end≤cutoff 的订单;之后迟到补录订单要再次调用才能结算。题目没有声明 cutoff 单调或延迟录入策略。

解题路线

  1. Part I:driver_id→费率;记录每笔配送时按 fee×秒数/3600 算其独立费用,同时更新 total 和 unpaid 两个累计值。配送重叠不影响费用,`getTotalCost()` 和 `getUnpaidAmount()` O(1)。
  2. Part II:未付配送放 (endTime,唯一序号) 最小堆;`pay_up_to` 反复弹出 end≤cutoff 的项,并从 unpaid 扣费。每单只入堆/出堆一次,所以重复或倒退 cutoff 不会二次付款。
  3. Part III:按司机收集过去 [now−86400,now) 的配送交集;先对同司机排序合并重叠或相接区间,再把每段作为 +1/-1 时间事件,按时间合并净变化,扫描最大值。这样同司机同时送两单只计一次。
  4. 若要返回最大区间而非数值,扫描相邻不同时间戳间的常值区段;同刻结束/开始按半开端点语义汇总后更新,避免瞬时虚高。数万司机×每周数百单时,Part III 每次全量扫描可考虑按时间分桶与增量索引,但先核对查询频率。

手动走一遍

司机 A 每小时 $10,同时做 0–3600 和 1800–5400 秒两单,各得 $10,总费用 $20。cutoff=3600 只结第一单、未付 $10;再次同 cutoff 不变。并发人数在 1800–3600 仍只算 A 一人;若 B 在该段上线,才是两人。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

from collections import defaultdict
from decimal import Decimal
import heapq


class DeliveryBilling:
    """In-memory, single-threaded training implementation of all three parts."""

    def __init__(self) -> None:
        self.rates: dict[int, Decimal] = {}
        self.deliveries: list[tuple[int, int, int, Decimal]] = []
        self.unpaid_heap: list[tuple[int, int]] = []
        self.total = Decimal(0)
        self.unpaid = Decimal(0)

    def add_driver(self, driver_id: int, usd_hourly_rate: float) -> None:
        if driver_id in self.rates or usd_hourly_rate < 0:
            raise ValueError("invalid driver")
        self.rates[driver_id] = Decimal(str(usd_hourly_rate))

    def record_delivery(self, driver_id: int, start: int, end: int) -> None:
        if driver_id not in self.rates or not (0 < end - start <= 10_800):
            raise ValueError("invalid delivery")
        payout = self.rates[driver_id] * Decimal(end - start) / Decimal(3600)
        index = len(self.deliveries)
        self.deliveries.append((driver_id, start, end, payout))
        heapq.heappush(self.unpaid_heap, (end, index))
        self.total += payout
        self.unpaid += payout

    def get_total_cost(self) -> float:
        return float(self.total)

    def pay_up_to(self, cutoff: int) -> None:
        while self.unpaid_heap and self.unpaid_heap[0][0] <= cutoff:
            _, index = heapq.heappop(self.unpaid_heap)
            self.unpaid -= self.deliveries[index][3]

    def get_unpaid_amount(self) -> float:
        return float(self.unpaid)

    def max_simultaneous_drivers_in_past_24_hours(self, now: int) -> int:
        window_start = now - 86_400
        by_driver: dict[int, list[tuple[int, int]]] = defaultdict(list)
        for driver, start, end, _ in self.deliveries:
            left, right = max(start, window_start), min(end, now)
            if left < right:
                by_driver[driver].append((left, right))

        changes: dict[int, int] = defaultdict(int)
        for intervals in by_driver.values():
            intervals.sort()
            left, right = intervals[0]
            for next_left, next_right in intervals[1:]:
                if next_left <= right:
                    right = max(right, next_right)
                else:
                    changes[left] += 1
                    changes[right] -= 1
                    left, right = next_left, next_right
            changes[left] += 1
            changes[right] -= 1

        active = maximum = 0
        for time in sorted(changes):
            active += changes[time]
            maximum = max(maximum, active)
        return maximum

复杂度

录单 O(log D) 因堆,累计查询 O(1);一次付款 O(k log D) 弹出 k 单,全部付款累计 O(D log D)。过去 24h 统计若窗口内 D_w 单、G 位司机,排序合并和全局事件排序 O(D_w log D_w),空间 O(D_w+G)。原始训练实现扫描全部已存订单 O(D),长期运行需时间索引减小候选集。

易错点与追问

容易失分的边界

  • 把重叠配送时长合并后计费,少付订单。
  • 并发分析直接数 delivery,司机一人多单被重复算。
  • 只按 start 扫描不按同刻 end 处理半开边界,出现虚假峰值。
  • 截止结算时每次全表扫描或重复扣费。
  • 用浮点金额累积结果当真实 payroll ledger 结算值。

继续深挖

  • 新记录的订单 end 早于上一次已结 cutoff,如何补偿?
  • 若司机费率有生效日期,recordDelivery 应按开始、结束还是各时间片费率计价?公开题库预览提费率变化但详情未给。
  • 若 pay_up_to 与 recordDelivery 并发,锁粒度和线性化点如何选?
  • 如何返回达到峰值的全部时间区间?
  • 百万历史单、每秒多次 24h 查询时怎样维护滚动并发索引?
✓已复制微信号!