アルゴリズム概要

💡 この問題を一言で言うと:「三角形の形に数値を並べ、上から1行ずつ積み上げて完成させる問題」

各行の値は「真上の左と右の値を足すだけ」で決まります。この「局所ルールの積み重ね」は 動的計画法(=部分問題の答えを再利用して全体を解く技法)の最良な入門例です。

⚠️ なぜ単純な方法では解けないのか

  • 各行は前の行の値がなければ計算できない。行をまたいだ依存関係がある。
  • 全行を保持して返す必要があるため、出力サイズ自体が O(n²) になる。これは避けられない下限。
  • Python では boolint のサブクラスなので、型チェックの順番を誤るとバグになる(詳細は FAQ 参照)。
O(n²)
時間計算量
O(n²)
空間計算量
1 ≤ n ≤ 30
numRows の範囲
list[list[int]]
戻り値の型

📥 入力 / 📤 出力の例

# 入力
numRows = 5
# 出力
[[1],
 [1, 1],
 [1, 2, 1],
 [1, 3, 3, 1],
 [1, 4, 6, 4, 1]]

✅ なぜこれが正解か:行2の 2 は行1の 1+1、 行3の 3 は行2の 1+2 および 2+1。すべて定義通り。

📐 パスカルの三角形のルール(2つだけ)

ルール 1: 各行の両端は必ず 1
ルール 2: 内側の要素 = 真上の左 + 真上の右
例)行2の 2 = 行1の 1 + 1

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

numRows = 5 を例に、アルゴリズムの各ステップを追います。

Python 実装

📋 このコードの構造(先に全体像を把握しよう)

  1. 入力の検証:型チェック(bool を先に弾く)と範囲チェック(1〜30)
  2. 結果リスト triangle を空で初期化し、for ループで行を積み上げる
  3. 各行は _build_row() ヘルパーで構築:行0・行1は固定値を返す
  4. 行2以降:前行を参照し、リスト内包表記で内側を計算して [1]+inner+[1] で完成

▸ 業務開発版(型検証・コメント付き)

from __future__ import annotations


class Solution:
    """LeetCode 118: Pascal's Triangle — 行の積み上げ法"""

    def generate(self, numRows: int) -> list[list[int]]:
        # ── 入力検証(型チェック) ────────────────────────────────────
        # Python では bool が int のサブクラスなので先に bool を弾く
        if isinstance(numRows, bool) or not isinstance(numRows, int):
            raise TypeError(f"numRows must be int, got: {type(numRows).__name__}")

        # ── 入力検証(範囲チェック) ──────────────────────────────────
        if numRows < 1 or numRows > 30:
            raise ValueError(f"numRows must be 1‒30, got: {numRows}")

        # ── 結果格納用リスト ──────────────────────────────────────────
        triangle: list[list[int]] = []

        # ── 行を 1 行ずつ積み上げる ───────────────────────────────────
        for row_index in range(numRows):
            current_row = self._build_row(triangle, row_index)
            triangle.append(current_row)

        return triangle

    def _build_row(self, triangle: list[list[int]], row_index: int) -> list[int]:
        # 基底条件1:行0 は [1] 固定(前行が存在しないので早期リターン)
        if row_index == 0:
            return [1]

        # 基底条件2:行1 は [1, 1] 固定(内側の要素が存在しない)
        if row_index == 1:
            return [1, 1]

        # 行2以降:前の行を参照して内側の要素をリスト内包表記で計算
        prev: list[int] = triangle[row_index - 1]

        # CPython の LIST_APPEND 命令が効くため、for+append より高速
        inner: list[int] = [
            prev[col - 1] + prev[col]   # 真上の左 + 真上の右
            for col in range(1, row_index)
        ]

        return [1] + inner + [1]

▶ 入力例 numRows = 5 での動作トレース

入力: numRows = 5

[検証] isinstance(5, bool)→False, isinstance(5, int)→True, 1≤5≤30 ✅

[row_index=0] _build_row([], 0)  → [1]             triangle=[[1]]
[row_index=1] _build_row(.., 1)  → [1,1]            triangle=[[1],[1,1]]
[row_index=2] prev=[1,1]
              inner: col=1 → 1+1=2  →  inner=[2]
              return [1]+[2]+[1] = [1,2,1]            triangle=[[1],[1,1],[1,2,1]]
[row_index=3] prev=[1,2,1]
              inner: col=1→1+2=3, col=2→2+1=3  →  inner=[3,3]
              return [1,3,3,1]                        triangle=[..,[1,3,3,1]]
[row_index=4] prev=[1,3,3,1]
              inner: col=1→1+3=4, col=2→3+3=6, col=3→3+1=4  →  inner=[4,6,4]
              return [1,4,6,4,1]                      triangle=[..,[1,4,6,4,1]]

出力: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] ✅

▸ 競技プログラミング版(LeetCode 提出用・最短実装)

class Solution:
    def generate(self, numRows: int) -> list[list[int]]:
        tri: list[list[int]] = [[1]]
        for i in range(1, numRows):
            p = tri[-1]          # tri[-1] → 末尾行を O(1) で取得(Python の負インデックス)
            tri.append([1] + [p[j-1] + p[j] for j in range(1, i)] + [1])
        return tri

処理フローチャート

🗺️ フローチャートの読み方

楕円(緑)= 開始・終了
四角(青)= 処理ステップ
ひし形(黄)= 条件分岐
はい いいえ
開始 入力検証 (型・範囲チェック) TypeError / ValueError No Yes triangle = [] を初期化 (結果を格納する 2 次元リスト) row_index < numRows ? (ループ継続の判定) triangle を返す No Yes _build_row() 呼び出し (現在の行番号を渡す) row_index ≤ 1 ? (基底条件の判定) [1] または [1, 1] を返す Yes No prev = triangle[row_index − 1] (前の行を参照する) リスト内包表記で inner を計算 prev[c−1] + prev[c] の全ての c row = [1] + inner + [1] (両端に 1 を付けて行を完成) triangle.append(row) (完成した行を三角形に追加) row_index += 1 (次の行へ進む) 繰り返す

🔎 入力例 numRows = 5 でのフロー追跡

  1. 「開始」→ 入力検証(isinstance で型と範囲を確認)→ 検証通過
  2. triangle = [] を初期化。ループに入る。
  3. row_index=0: ループ条件 0<5 → Yes → _build_row → row_index≤1 → Yes → [1] を append。
  4. row_index=1: 同様に [1,1] を append。
  5. row_index=2〜4: row_index≤1 → No → prev 参照 → inner 計算 → [1]+inner+[1] → append → row_index++。
  6. row_index=5: ループ条件 5<5 → No → triangle を返す。

計算量分析

📖 Big-O 記法の読み方(入力 n が大きくなるにつれ処理時間がどう変わるかの目安)

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
n より少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
種類 計算量 理由
時間計算量 O(n²) 全要素数 = 1+2+…+n = n(n+1)/2。各要素を1度だけ計算する。
空間計算量 O(n²) 全行を triangle に保持して返すため。
1行あたり O(k) k 行目の構築は k 個の要素を計算するだけ(k は行インデックス)。

🔍 なぜ O(n²) になるのか、そして下回れないのか

行 k の要素数は k+1 個なので、全要素数は 1+2+3+…+n = n(n+1)/2 ≈ n²/2 です。これは Big-O 表記で O(n²) になります。 重要なのは「出力自体が n²/2 個の要素を持つ」という点です。どんなに賢いアルゴリズムを使っても、 出力を生成するだけで O(n²) の時間が必要になります。この O(n²) は下限(どうしても下回れないコスト)であり、 本実装はその下限を達成しています。

📊 numRows ごとの要素数の増え方

numRows (n) 1 5 10 20 30
全要素数 1 15 55 210 465

📖 用語集

このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください。(五十音順)

イテラブル
for ループで繰り返せるオブジェクトの総称。 リスト・タプル・range() など。 例:range(1, 3)1, 2 を順に返すイテラブル。
イテレーション
ループの1回分の繰り返し処理のこと。for i in range(5) は5回イテレーションする。
下限(かげん)
どんなアルゴリズムを使っても越えられない計算量の最低ライン。 この問題では出力自体が O(n²) 個の要素を持つため、O(n²) が下限になる。
基底条件(きていじょうけん)
再帰やループの終了条件、または特殊ケースを処理する最初の条件。 この問題では行0([1])と行1([1,1])が基底条件。 内側の要素が存在しないため、汎用ロジックとは別に処理する。
空間計算量(くうかんけいさんりょう)
処理中に使うメモリ量が入力 n に対してどう変化するかの目安。 辞書に例えると「探偵がメモを取るためのノートの枚数」。 この問題では全行を保存するため O(n²)。
時間計算量(じかんけいさんりょう)
入力の大きさ n に対して処理にかかる手間がどう増えるかの目安。 「n が2倍になったら処理時間は何倍か」を表す Big-O 記法で表現する。 O(n²) は「n が2倍になると処理は4倍になる」という意味。
動的計画法(どうてきけいかくほう)
問題を小さな部分問題に分割し、その結果を再利用しながら全体の答えを組み立てる手法。 料理に例えると「昨日作ったスープのだしを今日の料理に使い回す」イメージ。 この問題では「前の行(部分問題の答え)を使って次の行を作る」のが動的計画法に当たる。
不変条件(ふへんじょうけん)
アルゴリズムが正しく動くために、ループ中ずっと成り立ち続けるべき条件(ループ不変条件とも言う)。 この問題では「ループ開始時に triangle には正しいパスカルの行が入っている」が不変条件。
バイトコード
Python のソースコードが実行前に変換される中間形式。 CPython はリスト内包表記を LIST_APPEND という専用の最適化命令にコンパイルするため、 通常の for + append() より高速に動作する。
リスト内包表記
[式 for 変数 in イテラブル] という形でリストを1行で作る書き方。 例:[x*2 for x in range(3)][0, 2, 4]。 CPython の最適化命令が使われるため、for + append() より高速。
早期リターン(そうきりたーん)
関数の先頭で特殊なケースを判定し、すぐに return することで後続の処理をシンプルに保つテクニック。 「ネストを深くしない」コードスタイルの一つ。
bool は int のサブクラス
Python では True == 1False == 0 として扱われる。 そのため isinstance(True, int)True を返す。 型チェックでは bool を先に弾くことでこのバグを防げる。