アルゴリズム概要

問題の説明

5人の哲学者が円卓に座り、各自の間にフォークが1本ずつ(計5本)配置されています。各哲学者は「思考」と「食事」を交互に繰り返しますが、食事をするには左右両方のフォークが必要です。各フォークは同時に1人しか使用できません。

入出力例

Input: n = 1

各哲学者が1回ずつ食事を行う

Output:

出力配列の各要素 [a, b, c] は:

制約条件

戦略: リソース順序付け

核心アイデア

常に小さいフォークID → 大きいフォークIDの順でロック取得することで、循環待機を数学的に不可能にします。

主要ポイント

ステップバイステップ解説

Python実装

from threading import Lock

class DiningPhilosophers:
    """
    食事する哲学者問題の解決クラス

    リソース順序付け戦略によりデッドロックを完全防止。
    常に小さいフォーク番号→大きいフォーク番号の順でロック取得。

    Time: O(1) per call
    Space: O(1) - 固定5個のLock
    """

    __slots__ = ('_forks',)  # メモリオーバーヘッド削減

    def __init__(self) -> None:
        """5本のフォークに対応するLockを初期化"""
        self._forks = [Lock() for _ in range(5)]

    def wantsToEat(
        self,
        philosopher: int,
        pickLeftFork,
        pickRightFork,
        eat,
        putLeftFork,
        putRightFork
    ) -> None:
        """
        哲学者が食事を行う処理

        Args:
            philosopher: 哲学者ID (0-4)
            pickLeftFork: 左フォーク取得関数
            pickRightFork: 右フォーク取得関数
            eat: 食事関数
            putLeftFork: 左フォーク返却関数
            putRightFork: 右フォーク返却関数
        """
        # 右フォークIDのみ計算(philosopher自身が左フォークID)
        r = (philosopher + 1) % 5

        # リソース順序付け: 小さいID優先でロック
        # 哲学者0-3: philosopher < r (80%のケース)
        # 哲学者4: philosopher > r (20%のケース)
        if philosopher < r:
            # 左(小) → 右(大) の順でロック
            self._forks[philosopher].acquire()
            self._forks[r].acquire()
            try:
                pickLeftFork()
                pickRightFork()
                eat()
                putRightFork()
                putLeftFork()
            finally:
                # ロック解放(取得の逆順)
                self._forks[r].release()
                self._forks[philosopher].release()
        else:
            # 右(小) → 左(大) の順でロック(哲学者4のみ)
            self._forks[r].acquire()
            self._forks[philosopher].acquire()
            try:
                pickLeftFork()
                pickRightFork()
                eat()
                putRightFork()
                putLeftFork()
            finally:
                # ロック解放(取得の逆順)
                self._forks[philosopher].release()
                self._forks[r].release()

フローチャート

{/* 開始ノード */} 開始 {/* フォークID計算 */} フォークID計算 r = (philosopher + 1) % 5 {/* 矢印: 開始 → フォークID計算 */} {/* 条件分岐: philosopher < r */} philosopher < r (哲学者0-3) {/* 矢印: フォークID計算 → 条件分岐 */} {/* 左ブランチ(はい): 左→右の順でロック */} 左→右の順でロック lock(philosopher) lock(r) {/* 矢印: 条件分岐 → 左ブランチ(はい) */} はい {/* 右ブランチ(いいえ): 右→左の順でロック */} 右→左の順でロック lock(r) lock(philosopher) {/* 矢印: 条件分岐 → 右ブランチ(いいえ) */} いいえ {/* pickLeftFork & pickRightFork */} フォーク取得 pickLeftFork(), pickRightFork() {/* 矢印: 左ブランチ → pickForks */} {/* 矢印: 右ブランチ → pickForks */} {/* eat */} 食事 eat() {/* 矢印: pickForks → eat */} {/* putRightFork & putLeftFork */} フォーク返却 putRightFork(), putLeftFork() {/* 矢印: eat → putForks */} {/* ロック解放 */} ロック解放 取得の逆順で解放 {/* 矢印: putForks → ロック解放 */}

フローの説明:
1. フォークIDを計算(右フォーク = (philosopher + 1) % 5)
2. philosopher < r の場合、左→右の順でロック(哲学者0-3)
3. philosopher ≥ r の場合、右→左の順でロック(哲学者4のみ)
4. 両方のフォークを取得(pickLeftFork, pickRightFork)
5. 食事を行う(eat)
6. フォークを返却(putRightFork, putLeftFork)
7. ロックを取得の逆順で解放

計算量分析

時間計算量

操作 計算量
フォークID計算 O(1)
ロック取得 O(1)(待機時間を除く)
関数呼び出し(5回) O(1)
ロック解放 O(1)
Total O(1) per call

空間計算量

データ構造 計算量
threading.Lock × 5個 O(1)
中間変数 O(1)
Total O(1)

代替手法との比較

手法 オーバーヘッド デッドロック回避 実装複雑度
リソース順序付け(本実装) 最小(20-50ns) ✓ 数学的に保証
セマフォ(人数制限) 中(100-200ns) ✓ 同時アクセス制限
チャネル(Go風) 大(500ns+) ✓ 順序付けで可能