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

IMC OA SWE 複盤:網格通行券 DP + 加權 LFU 快取

2026-08-15

IMC 的 SWE OA 幫大家跑了一遍,題量不大,但難度在線,這裡總結一份給準備中的同學做參考。一共 2 道 coding,120 分鐘,HackerRank 自選語言,難度中等偏上,主要考察演算法基礎、工程思維以及高效實作能力。需要 OA輔助 或 OA代面 的同學,也可以照著這份複盤對齊節奏。

OA 概覽與時間分配

維度 詳情
平台 HackerRank,自選語言
時長 120 分鐘
題量 2 道 coding
難度 中等偏上
考察 演算法基礎、工程思維、高效實作

兩道題共 120 分鐘,關鍵是避免前半小時在第一題裡繞得太久,導致第二題沒有留下讀題和補邊界的時間。建議按這個節奏推進:

  1. 先完整讀兩題,判斷哪題狀態更清晰。
  2. 第一題爭取在 35 到 45 分鐘內寫出可過範例和主測試的版本。
  3. 第二題優先寫出正確狀態,再處理壓縮空間、剪枝或複雜度最佳化。
  4. 最後預留 10 分鐘,檢查空輸入、極值、重複元素、整數溢位與初始化。

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

題意

給定一個由 0 和 1 組成的網格,機器人從左上角 (0,0) 出發,只能向右或向下移動,目標是到達右下角。1 表示可以正常通過,0 表示障礙,但機器人最多可以使用 k 次通行券,每次通過一個障礙消耗一張,包括起點和終點。要求計算所有合法路徑的數量,並對 10^9 + 7 取模。

思路

使用三維動態規劃,定義 dp[i][j][t] 表示到達位置 (i,j) 且已經使用 t 張通行券的路徑數量:

最後累加終點使用 0 到 k 張通行券的所有狀態。時間複雜度 O(nmk),空間複雜度 O(nmk),也可以用滾動陣列最佳化到 O(mk)

Python 實作

MOD = 10**9 + 7


def count_paths(grid, k):
    """grid: 0/1 二維網格;k: 通行券上限(障礙與起點終點都算消耗)。
    返回從左上到右下、向右/向下移動的合法路徑數 % 1e9+7。"""
    n, m = len(grid), len(grid[0])
    # dp[j][t] 表示當前列到達行 j、已用 t 張券的路徑數(滾動陣列)
    dp = [[0] * (k + 1) for _ in range(m)]

    start_cost = 0 if grid[0][0] == 1 else 1
    if start_cost <= k:
        dp[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   # 障礙需消耗一張券
            ndp = [0] * (k + 1)
            for t in range(cost, k + 1):
                total = 0
                if j > 0:
                    total += dp[j - 1][t - cost]  # 從左側來(已是本列)
                if i > 0:
                    total += dp[j][t - cost]      # 從上方來(dp[j] 仍是上一列)
                ndp[t] = total % MOD
            dp[j] = ndp
    return sum(dp[m - 1]) % MOD

需要留意滾動陣列裡「左側」與「上方」引用的時序:處理到 (i,j) 時,dp[j-1] 應已是本列更新後的值,dp[j] 仍是上一列的值。若擔心時序繞暈,考場上直接用完整三維陣列 dp[i][j][t] 寫最穩妥,AC 後再壓空間。

時間複雜度O(nmk)空間複雜度:滾動陣列 O(mk),完整版 O(nmk)


Q2:按容量限制的加權 LFU 快取

題意

實作一個按容量大小限制的加權 LFU 快取。每條資料包含 keyvaluesize,所有資料的 size 總和不能超過 capacity

思路

用一個 HashMap 保存 key 到快取節點的映射,再按存取頻率維護多組雙向鏈結串列,每條串列內部按最近使用順序排列,同時記錄 min_freq 和目前總大小:

借助雜湊表與雙向鏈結串列,單次存取、更新和淘汰都可以做到平均 O(1)。這裡用 Python 的 OrderedDict 按頻率分桶,天然維護每個頻率桶內的 LRU 順序,程式碼更短且等價於手寫雙向鏈結串列。

Python 實作

from collections import defaultdict, OrderedDict


class WeightedLFU:
    def __init__(self, capacity):
        self.capacity = capacity
        self.size_used = 0
        self.node = {}                          # key -> [value, size, freq]
        self.freq = defaultdict(OrderedDict)    # freq -> OrderedDict[key],頭部最久未用
        self.min_freq = 0

    def _remove_from_bucket(self, key, f):
        """把 key 從頻率桶 f 摘出,桶空則清理並按需上移 min_freq。"""
        del self.freq[f][key]
        if not self.freq[f]:
            del self.freq[f]
            if self.min_freq == f:
                self.min_freq = f + 1           # 該桶已空,最低頻率上移

    def _bump(self, key):
        """存取命中:把 key 從頻率 f 桶移到 f+1 桶尾部(最近使用)。"""
        item = self.node[key]
        f = item[2]
        self._remove_from_bucket(key, f)
        item[2] = f + 1
        self.freq[f + 1][key] = None            # 尾部 = 最近使用

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

    def _evict(self, need):
        """淘汰直到能容納 need:取 min_freq 桶頭部(最久未用)刪除。"""
        while self.size_used + need > self.capacity and self.node:
            bucket = self.freq[self.min_freq]
            old_key, _ = bucket.popitem(last=False)   # 頭部 = 最久未使用
            self.size_used -= self.node[old_key][1]
            del self.node[old_key]
            if not bucket:
                del self.freq[self.min_freq]
                # 下一輪若還需淘汰,會從新的 min_freq 桶繼續

    def put(self, key, value, size):
        if size > self.capacity:
            return                              # 單條超容量,忽略
        if key in self.node:                    # 更新:值和大小變,頻率不變,刷新順序
            item = self.node[key]
            f = item[2]
            self._remove_from_bucket(key, f)    # 先摘出,避免被自身淘汰
            self.size_used += size - item[1]
            item[0], item[1] = value, size
            self._evict(0)                      # 變大後可能超容,先騰空間
            self.freq[f][key] = None            # 放回原頻率桶尾部(最近使用)
            if f < self.min_freq or not self.freq.get(self.min_freq):
                self.min_freq = min(self.freq) if self.freq else f
            return
        self._evict(size)                       # 新增:騰出空間
        self.node[key] = [value, size, 1]
        self.freq[1][key] = None
        self.size_used += size
        self.min_freq = 1

更新已有鍵時的關鍵點:先把舊節點從其頻率桶摘出再觸發淘汰,避免剛更新的鍵被誤刪;淘汰完再放回原頻率桶(頻率不變)並刷新為最近使用,同時按剩餘的最小鍵修正 min_freq

複雜度get / put 均攤 O(1)(雜湊查找 + 頻率桶內 OrderedDict 的頭尾操作都是 O(1));淘汰單個節點 O(1),修正 min_freqmin(self.freq) 在桶數很少時可視為常數。


備考策略


FAQ

Q1:IMC 的 SWE OA 難度如何?和 LeetCode 相比是什麼水平?

兩道題、120 分鐘,難度中等偏上。演算法本身不算最難那檔,但更看重工程實作的完整度和邊界處理——DP 計數要把通行券消耗和取模處理乾淨,LFU 要把頻率桶、LRU 和容量淘汰的狀態維護對,比純 LeetCode 中等題更考細緻。

Q2:IMC SWE OA 用什麼平台、多長時間、幾道題?

HackerRank 平台,自選語言,120 分鐘,2 道 coding。時間相對寬裕,但兩題都需要一定的建模和邊界處理時間,節奏管理很重要。

Q3:Q1 的通行券路徑計數必須用三維 DP 嗎?

是的,因為「已用幾張券」是一個必須記錄的狀態維度。dp[i][j][t] 三維最直觀;AC 後可用滾動陣列把列維度壓掉,降到 O(mk) 空間。注意障礙、起點、終點都算消耗券。

Q4:加權 LFU 和普通 LFU 有什麼區別?

普通 LFU 按條數淘汰,加權 LFU 按每條資料的 size 累加,總和不能超過 capacity,所以一次 PUT 可能要淘汰多條才能騰出空間。此外「更新已有鍵不改變頻率、單條 size 超容量直接忽略」是加權版本的兩個特有規則。

Q5:如何準備 IMC 這類偏工程實作的 OA?

重點練兩類題:一是帶額外狀態維度的 DP(如本題的通行券),二是 LRU/LFU 這類需要雜湊表 + 鏈結串列組合的系統模擬題。練習時強制自己先寫正確狀態再最佳化,並養成最後過一遍邊界清單的習慣。


正在準備 IMC 或其他量化 / 交易類公司的 SWE OA? 我們熟悉 HackerRank 自選語言、兩題 120 分鐘這類節奏,能陪你把帶狀態維度的 DP、LFU/LRU 系統模擬題的建模和邊界一次打磨到位,提供全程 OA輔助 與 OA代面 支援。

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

聯絡方式