⚔️

アルゴリズム学習はまるでRPGだった話 〜データ構造は武器だ〜

に公開

🧠 はじめに

Google 面接のコーディング練習をしているとき、ふと気づいた。
「これ、まるで RPG じゃないか?」

アルゴリズムの世界では、問題が敵で、データ構造が武器だ。
そして、強い武器ほど正しく使わなければ勝てない。


🎮 ステージ 1:まずは“武器の一覧”を覚える

アルゴリズムの敵(問題)はいろいろ出てくるけど、
実は出てくる武器はだいたい同じ。

敵(問題タイプ) よく使う武器(データ構造)
頻繁な探索・存在判定 set, dict
順序を保ちたい list, deque, linked list
最小・最大をすぐ取りたい heapq
連続区間を扱う スライディングウィンドウ, bisect
出現回数のカウント Counter, defaultdict
経路探索 deque(BFS), 再帰(DFS)
グルーピングや連結性 Union-Find
部分一致・文字探索 Trie, hash, sliding window

⚙️ ステージ 2:考える順番=戦略の立て方

1️⃣ 問題を理解する
  → 何を求められているか? どんな制約があるか?

2️⃣ ロジックを考える
  → どうすれば人間が紙で解けるか?

3️⃣ データ構造を選ぶ
  → そのロジックを最速で実現できる構造は何か?

この順番を守るだけで、どんな問題でも落ち着いて対処できる。


🗺 ステージ 3:武器図鑑(データ構造 × O 記法 × コード)

構造 主な用途 操作 計算量 コード例
list 配列、順序データ append, pop, insert 末尾 O(1)、中間 O(n) a.append(1); a.pop()
deque 両端操作 appendleft, popleft O(1) dq = deque(); dq.append(1)
set 重複排除、存在判定 in, add, remove 平均 O(1) if x in s:
dict キー → 値マップ m[k], get, setdefault 平均 O(1) m = {"a": 1}
heapq 優先度付き最小値 heappush, heappop O(log n) heapq.heappush(h, x)
Counter 頻度集計 most_common, update O(n) cnt = Counter(nums)
bisect ソート+二分探索 insort, bisect_left O(log n)探索、挿入 O(n) bisect.insort(a, x)
linked list 順序制御・LRU _add, _remove O(1)(双方向) node.prev.next = node.next
Union-Find グループ連結 find, union ほぼ O(1) dsu.union(a,b)
Trie 前方一致探索 insert, search O(L)(文字列長) node = node.child[ch]

💡 ステージ 4:実際の戦い(例:LRU Cache)

🧩 問題:
最近使われた順で要素を管理し、古いものを自動で削除するキャッシュを設計せよ。

🎯 ゴール:

  • get(key) / put(key, value) を平均 O(1) で実装する。

⚙️ 戦略の立て方

  1. 「値の取得」は dict で O(1)
  2. 「順序(古い/新しい)」の管理は 双方向リスト
  3. この 2 つを組み合わせれば理想的!

✅ コード例(Python)

class Node:
    def __init__(self, key=0, val=0):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.map = {}
        self.head, self.tail = Node(), Node()
        self.head.next, self.tail.prev = self.tail, self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key: int) -> int:
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._add_front(node)
        return node.val

    def put(self, key: int, val: int) -> None:
        if key in self.map:
            self._remove(self.map[key])
        node = Node(key, val)
        self.map[key] = node
        self._add_front(node)
        if len(self.map) > self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.key]

🧩 考察と計算量

操作 計算量 理由
get O(1) dict で即参照+リスト移動も O(1)
put O(1) 追加・削除・更新すべて O(1)
空間 O(n) 要素数+ノード構造分

🧭 ステージ 5:解けない時の“思考リセット”

問題がうまく解けないときは、焦らず立ち返る
次の 3 つを順に整理すると、霧が晴れます。


① 何をやるべきか?

ゴールを一文で言い切る。

  • 「○○ を最小化する」
  • 「○○ をカウントする」
  • 「○○ の順序を保つ」

🧩 → 言葉で整理できない問題は、まだ理解できていない。


② どんな制約があるか?

N, Q, 時間制限、更新有無を読み取る。

  • N = 10⁵ → O(n²)は NG
  • 「常に更新される」→ ソート済構造は使えない
  • 「連続区間」→ スライディングウィンドウ
  • 「順序+削除」→ 双方向リスト

🧩 → 制約を読めば、使える武器が見えてくる。


③ どのデータ構造を使うべきか?

高速にしたい操作を明確に。

操作したいこと 構造 計算量
探したい dict, set O(1)
両端から出し入れ deque O(1)
最小/最大を取りたい heapq O(log n)
頻度を数えたい Counter O(n)
中間削除 linked list O(1)
つながり判定 Union-Find ほぼ O(1)

🧩 → 操作単位で考えるのがコツ。


🏁 まとめ:アルゴリズムは攻略ゲーム

  • 問題=敵
  • データ構造=武器
  • アルゴリズム=戦略
  • コード=実戦

どんな問題も、正しい武器を選べば必ず倒せる。
そして、立ち返る場所を持っている人は、どんなバトルにも強い。

今日も一問ずつ、冒険を続けよう ⚔️

GitHubで編集を提案

Discussion