Software Engineer New Grad · Phone Coding / OOD

Pinterest 密室玩家排行榜:O(1) 前进与同房先到先排

按 New Grad 题目的 advance/get_room O(1)、leaderboard O(N+k) 约束设计非空房间桶和到达顺序链表,并做随机状态机对照。

题目重点:按 New Grad 题目的 advance/get_room O(1)、leaderboard O(N+k) 约束设计非空房间桶和到达顺序链表,并做随机状态机对照。

题意与假设

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

  • 玩家在初始化时加入且 ID 唯一,均在房间 0;初始化列表顺序作为房间 0 的到达顺序。
  • 房间编号 0…R,已到 R 再 advance 是 no-op,不改变到达顺序;未知玩家抛异常。题目没有规定这个边界,属于训练约定。
  • 同房 tie-break 比的是最近一次进入当前房间的调用顺序,而不是玩家最初创建时间。

解题路线

  1. 按房间建立玩家双向链表,入房时追加尾部,保证同房早到者在前;玩家 ID 直达其节点,O(1) 摘除和查询。每桶维护人数,支持另一题目的 O(1) count_in_room。
  2. 另维护只含非空房间的降序双向链表与 room→bucket 索引。玩家只能 r→r+1,新房间若原本为空,它必定位于房间 r 的桶之前,故 O(1) 插入;旧桶空时 O(1) 删除。
  3. leaderboard 从最高房间开始,按每房到达顺序取前 k 位;无需遍历全部 R 个可能为空的房间。
  4. 用朴素模型在每步按 (-room,arrivalTick) 排序做独立随机对照,覆盖同房交换、空桶删除、房间人数和终点 no-op。

手动走一遍

A、B、C 初始在 0;advance(A)、advance(B)、advance(A) 后 A 在 2、B 在 1、C 在 0,前二为 A、B。若 A、B 都只前进一步,A 先进入 1,榜单中 A 排 B 前。

Python 参考实现

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

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

"""O(1) advance/get and O(k) leaderboard with ordered occupied rooms."""


class PlayerNode:
    def __init__(self, player_id, bucket):
        self.player_id = player_id
        self.bucket = bucket
        self.prev = self.next = None


class RoomBucket:
    def __init__(self, room):
        self.room = room
        self.prev = self.next = None
        self.head = PlayerNode(None, self)
        self.tail = PlayerNode(None, self)
        self.head.next = self.tail
        self.tail.prev = self.head
        self.count = 0

    def append(self, player):
        last = self.tail.prev
        last.next = player
        player.prev, player.next = last, self.tail
        self.tail.prev = player
        player.bucket = self
        self.count += 1

    def empty(self):
        return self.count == 0


class EscapeRoomLeaderboard:
    def __init__(self, players, last_room: int):
        if last_room < 0 or len(set(players)) != len(players):
            raise ValueError("invalid rooms or duplicate player")
        self.last_room = last_room
        self.first = RoomBucket(None)
        self.last = RoomBucket(None)
        self.first.next = self.last
        self.last.prev = self.first
        self.by_room = {}
        self.by_player = {}
        if players:
            bucket = RoomBucket(0)
            self._insert_before(self.last, bucket)
            self.by_room[0] = bucket
            for player_id in players:
                node = PlayerNode(player_id, bucket)
                bucket.append(node)
                self.by_player[player_id] = node

    def _insert_before(self, next_bucket, bucket):
        previous = next_bucket.prev
        previous.next = bucket
        bucket.prev, bucket.next = previous, next_bucket
        next_bucket.prev = bucket

    def _remove_bucket(self, bucket):
        bucket.prev.next = bucket.next
        bucket.next.prev = bucket.prev
        del self.by_room[bucket.room]

    def advance(self, player_id):
        player = self.by_player[player_id]
        old = player.bucket
        if old.room == self.last_room:
            return old.room
        next_room = old.room + 1
        next_bucket = self.by_room.get(next_room)
        if next_bucket is None:
            next_bucket = RoomBucket(next_room)
            self._insert_before(old, next_bucket)
            self.by_room[next_room] = next_bucket
        player.prev.next = player.next
        player.next.prev = player.prev
        old.count -= 1
        next_bucket.append(player)
        if old.empty():
            self._remove_bucket(old)
        return next_room

    def get_room(self, player_id):
        return self.by_player[player_id].bucket.room

    def count_in_room(self, room: int) -> int:
        bucket = self.by_room.get(room)
        return 0 if bucket is None else bucket.count

    def leaderboard(self, k: int):
        if k < 0:
            raise ValueError("k must be nonnegative")
        result = []
        bucket = self.first.next
        while bucket is not self.last and len(result) < k:
            player = bucket.head.next
            while player is not bucket.tail and len(result) < k:
                result.append(player.player_id)
                player = player.next
            bucket = bucket.next
        return result

复杂度

初始化 O(N);advance/get_room O(1);leaderboard(k) O(min(k,N)),满足题目 O(N+k) 上界;玩家节点和最多 min(N,R+1) 个非空房间桶共 O(N) 空间。

易错点与追问

容易失分的边界

  • 每次 leaderboard 都扫描 0…R,R 可能远大于玩家数,无法从题给约束直接推出 O(N+k)。
  • 只用 room→set,丢失同房到达顺序。
  • 从旧房间列表按值删除玩家,advance 退化 O(该房间人数)。
  • 玩家从 r 到 r+1 时把新桶插错位置,破坏降序。

继续深挖

  • 若可跳跃任意多个房间,如何保持桶有序且仍满足更新 O(1)?
  • 若允许玩家后退,同房 tie-break 如何定义?
  • 并发 advance 同一个玩家时,位置和进入时间如何原子更新?
✓已复制微信号!