← 返回部落格列表 IMC SWE OA 復盤:網格通行券路徑 + 加權 LFU 快取
IMC

IMC SWE OA 復盤:網格通行券路徑 + 加權 LFU 快取

2026-08-09

最近做了 IMC 的 SWE OA,兩道程式題、120 分鐘,在 HackerRank 上完成,可以自選語言。整體難度中上,考的不是花俏技巧,而是演算法基本功、工程思維和高效實作。這篇復盤把兩道題的思路、Python 解法和複雜度都寫清楚,也順帶聊聊時間該怎麼分配。如果你也在排隊等 IMC 的 OA,希望這份筆記能幫你把節奏和心態調到位。

OA 環境與形式概覽

整場 OA 在 HackerRank 完成,語言不限,兩道題都會給若干可見範例和隱藏測試點,最終看通過率。題目描述偏工程化,邊界條件寫得比較克制,需要自己把角落情況想全。

項目 詳情
平台 HackerRank(自選語言)
形式 線上程式題,範例 + 隱藏測試點計分
總時長 120 分鐘
題量 2 道
難度 中上
考點 演算法基本功、工程思維、高效實作、邊界處理

時間管理建議

兩道題 120 分鐘,看似寬鬆,但第二題一旦想複雜了很容易超時。我的做法是:


Problem 1:帶通行券的網格路徑計數

題目背景

給定一個由 0 和 1 組成的網格,機器人從左上角 (0, 0) 出發,只能向右或向下移動,目標是右下角。1 表示可通行,0 表示障礙,但機器人手裡有最多 k通行券,每張券可以讓它強行穿過一個障礙格(包括起點和終點格)。求所有合法路徑的數量,對 1e9+7 取模。

思路

這是網格路徑計數的加強版,多了一個「通行券」維度,很自然地想到三維 DP:

把「進入格子消耗的券數」抽象成 cost1 格為 0,0 格為 1),轉移就統一成 dp[i][j][t] = dp[i-1][j][t-cost] + dp[i][j-1][t-cost]

Python 解法

from typing import List

MOD = 10**9 + 7


def count_paths(grid: List[List[int]], k: int) -> int:
    """統計從左上角到右下角、最多用 k 張通行券的路徑數,對 1e9+7 取模。

    dp[i][j][t] = 到達 (i, j) 且累計使用 t 張通行券的路徑數。
    格子為 1 時進入不消耗券;格子為 0 時進入消耗一張券。
    """
    n, m = len(grid), len(grid[0])

    # dp[i][j][t],t 取值 0..k
    dp = [[[0] * (k + 1) for _ in range(m)] for _ in range(n)]

    # 初始化起點:起點是障礙則進入時就消耗一張券
    start_cost = 0 if grid[0][0] == 1 else 1
    if start_cost <= k:
        dp[0][0][start_cost] = 1

    for i in range(n):
        for j in range(m):
            if i == 0 and j == 0:
                continue
            cost = 0 if grid[i][j] == 1 else 1
            # 券數不夠消耗 cost 的狀態無意義,從 cost 開始列舉
            for t in range(cost, k + 1):
                total = 0
                if i > 0:
                    total += dp[i - 1][j][t - cost]  # 從上方來
                if j > 0:
                    total += dp[i][j - 1][t - cost]  # 從左方來
                dp[i][j][t] = total % MOD

    # 終點處對 t = 0..k 求和
    return sum(dp[n - 1][m - 1][t] for t in range(k + 1)) % MOD

時間複雜度:O(n·m·k),每個格子對每個券數各算一次。 空間複雜度:O(n·m·k)。因為第 i 列只依賴第 i-1 列,可以用滾動陣列把空間壓到 O(m·k)。

Dry run 演示

用一個 2×2 的小網格最能說明券的作用:grid = [[1, 0], [0, 1]]k = 1。起點 (0,0)1,所以 dp[0][0][0] = 1

格子 類型 cost 轉移 結果
(0,1) 0 1 dp[0][1][1] = dp[0][0][0](左) dp[0][1][1] = 1
(1,0) 0 1 dp[1][0][1] = dp[0][0][0](上) dp[1][0][1] = 1
(1,1) 1 0 t=0: 上 0 + 左 0t=1: 上 1 + 左 1 dp[1][1][0]=0, dp[1][1][1]=2

終點對 t = 0..1 求和 = 0 + 2 = 2。對應兩條路徑:先右後下(穿過障礙 (0,1))、先下後右(穿過障礙 (1,0)),各用掉一張券。

主動補充要驗證的邊界:


Problem 2:按容量加權的 LFU 快取

題目背景

實作一個按大小加權的 LFU 快取。每個條目有 keyvaluesize 三個屬性,所有條目的 size 之和不能超過總容量 capacity。要求支援兩個操作:

思路

經典 LFU 的加權版,核心資料結構不變:

  1. 一個雜湊表 key -> Node,做 O(1) 定位。
  2. 每個頻率維護一個雙向鏈結串列,按近用順序排列,頭部最新、尾部最舊。
  3. 維護 min_freq(當前最低頻率)和 total(當前佔用的容量)。

各操作要點:

Python 解法

from collections import defaultdict


class Node:
    """快取節點,同時作為雙向鏈結串列節點。"""

    __slots__ = ("key", "value", "size", "freq", "prev", "next")

    def __init__(self, key=None, value=None, size=0):
        self.key = key
        self.value = value
        self.size = size
        self.freq = 1
        self.prev = None
        self.next = None


class DoublyLinkedList:
    """維護同一頻率下的節點,按近用順序排列:頭部最新,尾部最舊。"""

    def __init__(self):
        self.head = Node()  # 哨兵頭
        self.tail = Node()  # 哨兵尾
        self.head.next = self.tail
        self.tail.prev = self.head
        self.count = 0  # 串列內節點個數

    def add_front(self, node: Node) -> None:
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node
        self.count += 1

    def remove(self, node: Node) -> None:
        node.prev.next = node.next
        node.next.prev = node.prev
        self.count -= 1

    def remove_last(self) -> Node:
        node = self.tail.prev  # 最久未使用
        self.remove(node)
        return node

    def is_empty(self) -> bool:
        return self.count == 0


class WeightedLFUCache:
    """按 size 加權的 LFU 快取,get / put 平均 O(1)。"""

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.total = 0          # 當前佔用的容量
        self.min_freq = 0       # 當前最低頻率
        self.nodes = {}         # key -> Node
        self.freqs = defaultdict(DoublyLinkedList)  # freq -> 串列

    def _bump(self, node: Node) -> None:
        """頻率 +1,並從舊頻率串列移到新頻率串列頭部。"""
        old = node.freq
        self.freqs[old].remove(node)
        if self.freqs[old].is_empty() and old == self.min_freq:
            self.min_freq += 1
        node.freq += 1
        self.freqs[node.freq].add_front(node)

    def _evict_until(self, need: int) -> None:
        """騰出足夠空間容納 need:從最低頻串列尾部逐個淘汰。"""
        while self.nodes and self.total + need > self.capacity:
            while self.freqs[self.min_freq].is_empty():
                self.min_freq += 1
            victim = self.freqs[self.min_freq].remove_last()
            del self.nodes[victim.key]
            self.total -= victim.size

    def get(self, key):
        if key not in self.nodes:
            return -1
        node = self.nodes[key]
        self._bump(node)
        return node.value

    def put(self, key, value, size: int) -> None:
        # 單個條目就超過總容量,直接忽略
        if size > self.capacity:
            return

        if key in self.nodes:
            # 更新已有條目:頻率不變,只改 value / size 並刷新近用順序
            node = self.nodes[key]
            old_freq = node.freq
            self.freqs[old_freq].remove(node)
            self.total -= node.size
            del self.nodes[key]           # 暫時移出,避免淘汰到自己
            self._evict_until(size)
            node.value, node.size = value, size
            self.freqs[old_freq].add_front(node)
            self.nodes[key] = node
            self.total += size
            self.min_freq = min(self.min_freq, old_freq)
            return

        # 新增條目:先騰空間,再以頻率 1 插入
        self._evict_until(size)
        node = Node(key, value, size)     # 新節點頻率為 1
        self.freqs[1].add_front(node)
        self.nodes[key] = node
        self.total += size
        self.min_freq = 1

時間複雜度getput 平均 O(1)(淘汰是攤還 O(1))。 空間複雜度:O(快取中條目數),用於雜湊表和各頻率串列。

Dry run 演示

capacity = 6,依序執行以下操作:

操作 結果 total 快取狀態(頻率)
put(a, 1, 3) 3 a@1
put(b, 2, 3) 6 a@1, b@1
get(a) 回傳 1 6 b@1, a@2
put(c, 3, 3) 空間不足,淘汰 b 6 a@2, c@1
get(b) 回傳 -1 6 a@2, c@1

關鍵點:get(a) 後 a 頻率升到 2;插入 c 需要 3 的空間但已滿,min_freq = 1 且頻率 1 的串列裡只有 b,於是淘汰 b(而不是頻率更高的 a)。這正是「先按頻率、頻率相同再按最久未使用」的淘汰規則。

主動補充要驗證的邊界:


備考策略

IMC 的 OA 不玩偏門,考的是扎實的基本功 + 乾淨的工程實作。兩道題分別落在兩個高頻方向:

另外,邊界處理和整數溢位是隱藏測試點的常客,寫完一定要用具體資料 dry run 一遍。


FAQ

Q1:IMC 的 SWE OA 有幾道題、多長時間?

兩道程式題,總時長 120 分鐘,在 HackerRank 上完成,可以自選語言。難度中上,考演算法基本功、工程思維和高效實作。

Q2:120 分鐘兩道題,節奏怎麼安排?

先把兩道題都讀一遍,挑思路更清晰的先做。第一題爭取 35–45 分鐘拿下能過範例和主測試點的版本;第二題先寫正確的樸素版,再最佳化空間和複雜度;最後留 10 分鐘檢查邊界。

Q3:帶通行券的網格路徑為什麼要用三維 DP?

因為除了位置,還要記錄「已用幾張券」這個額外狀態。dp[i][j][t] 把位置和券數一起編碼,進入 0 格時消耗一張券(從 t-1 轉移),進入 1 格不消耗(從 t 轉移),最後對 t = 0..k 求和即可。空間可用滾動陣列壓到 O(m·k)。

Q4:加權 LFU 和普通 LFU 的區別在哪?

普通 LFU 按條目個數限制容量,加權版按每個條目的 size 之和限制。淘汰時可能要連續踢出多個低頻條目才騰得出空間,因此淘汰邏輯寫成「迴圈從 min_freq 尾部淘汰,直到空間足夠」。

Q5:LFU 裡更新已有 key 會不會改變頻率?

不會。更新只改 valuesize 並刷新近用順序,頻率保持不變。實作上要注意:size 變大時可能需要淘汰其他條目,為避免誤淘汰自己,可以先把該節點暫時移出對映,騰完空間再放回原頻率串列頭部。

Q6:最後 10 分鐘檢查最該看哪些點?

空輸入、極端規模、重複元素、整數溢位、變數初始化。這幾類問題最容易在隱藏測試點上翻車,用具體資料走一遍最穩。


正在準備 IMC 的 SWE OA? 我們熟悉 HackerRank 的計分方式和這類中上難度題的踩坑點,能陪你把 DP 和資料結構設計的高頻題型打磨到位。

立即加入微信 Coding0201取得一對一客製備考方案

聯絡方式