行の積み上げ法(動的計画法)による O(n²) 実装
💡 一言で言うと:「上の行だけを見て次の行を1行ずつ積み上げ、パスカルの三角形を作る問題」
💡 この問題を一言で言うと:「三角形の形に数値を並べ、上から1行ずつ積み上げて完成させる問題」
各行の値は「真上の左と右の値を足すだけ」で決まります。この「局所ルールの積み重ね」は 動的計画法(=部分問題の答えを再利用して全体を解く技法)の最良な入門例です。
⚠️ なぜ単純な方法では解けないのか
bool が
int
のサブクラスなので、型チェックの順番を誤るとバグになる(詳細は FAQ
参照)。
📥 入力 / 📤 出力の例
✅ なぜこれが正解か:行2の
2 は行1の
1+1、 行3の
3 は行2の
1+2 および
2+1。すべて定義通り。
📐 パスカルの三角形のルール(2つだけ)
2 =
行1の 1 + 1
numRows = 5 を例に、アルゴリズムの各ステップを追います。
📋 このコードの構造(先に全体像を把握しよう)
triangle
を空で初期化し、for
ループで行を積み上げる
_build_row()
ヘルパーで構築:行0・行1は固定値を返す
[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
🗺️ フローチャートの読み方
🔎 入力例 numRows = 5 でのフロー追跡
isinstance
で型と範囲を確認)→ 検証通過
triangle = []
を初期化。ループに入る。
_build_row
→ row_index≤1 → Yes →
[1] を
append。
[1,1] を
append。
[1]+inner+[1]
→ append → row_index++。
triangle
を返す。
📖 Big-O 記法の読み方(入力 n が大きくなるにつれ処理時間がどう変わるかの目安)
| 種類 | 計算量 | 理由 |
|---|---|---|
| 時間計算量 | 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
を順に返すイテラブル。
for i in range(5)
は5回イテレーションする。
[1])と行1([1,1])が基底条件。
内側の要素が存在しないため、汎用ロジックとは別に処理する。
triangle
には正しいパスカルの行が入っている」が不変条件。
LIST_APPEND
という専用の最適化命令にコンパイルするため、 通常の
for + append()
より高速に動作する。
[式 for 変数 in イテラブル]
という形でリストを1行で作る書き方。 例:[x*2 for x in range(3)]
→
[0, 2, 4]。 CPython の最適化命令が使われるため、for + append()
より高速。
return
することで後続の処理をシンプルに保つテクニック。
「ネストを深くしない」コードスタイルの一つ。
True == 1、False == 0
として扱われる。 そのため
isinstance(True, int)
は
True
を返す。 型チェックでは
bool を先に弾くことでこのバグを防げる。