二項係数の漸化式による O(rowIndex) 時間・O(rowIndex) 追加空間の実装
💡 一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」
💡 この問題を一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」です。
パスカルの三角形は、各行の両端が常に
1 で、
それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。
今回の問題では三角形全体を作る必要はなく、rowIndex
番目(0-indexed=0から数え始める番号付け)の行だけを返せばよい、という点がポイントです。
⚠️ なぜ単純な方法では解けないのか
📥 入出力例
3行目(0-indexed)の両端は1、内側は1つ上の行の隣接する値の和になるため [1,3,3,1] が正解です。
0行目は三角形の頂点そのものなので、要素数1の [1] が正解です。
1行目は両端の1だけで構成されるため [1,1] が正解です。
代表例
rowIndex = 4
を使って、 配列
row
がどのように埋まっていくかを1ステップずつ追跡します。
📋 このコードの構造(先に全体像を把握しよう)
from __future__ import annotations
from typing import List
class Solution:
"""
119. Pascal's Triangle II を解決するクラス
パスカルの三角形の rowIndex 行目を、二項係数の漸化式を使って
O(rowIndex) 時間・O(rowIndex) 追加空間で求める。
"""
def getRow(self, rowIndex: int) -> List[int]:
"""
パスカルの三角形の rowIndex 行目(0-indexed)を返す
Args:
rowIndex: 求めたい行番号(0 <= rowIndex <= 33)
Returns:
rowIndex 行目の値を格納したリスト
Raises:
TypeError: rowIndex が int 型でない場合
ValueError: rowIndex が 0〜33 の範囲外の場合
"""
# 型ヒントは実行時には無視される。呼び出し元が誤った型を渡した場合に
# 実行時までエラーに気づけないため、isinstance() で別途検証する。
self._validate_input(rowIndex)
# 結果を格納するリストを先に確保する。
# フォローアップの「O(rowIndex)の追加空間」を満たすため、
# 過去の行を丸ごと保持する二次元リストは作らず、この1本のリストだけを使い回す。
row: List[int] = [0] * (rowIndex + 1)
# パスカルの三角形の左端は、どの行でも必ず1になる
# (rowIndex個からk=0個を選ぶ組み合わせは「何も選ばない」の1通りしかないため)
row[0] = 1
# 二項係数の漸化式 C(n, k) = C(n, k-1) * (n - k + 1) // k を使い、
# 1つ左の値から次の値を順番に計算していく(同じ行の中だけで完結する)
for k in range(1, rowIndex + 1):
# 掛け算を先に行ってから割り算をする。
# 「//」(フロア除算)を使うのは、Pythonの「/」が常にfloatを返してしまい
# List[int]という戻り値の型と食い違ってしまうため。
# 二項係数は必ず整数になる値なので、掛け算を先に行えば割り切れずに誤差が出ることはない。
row[k] = row[k - 1] * (rowIndex - k + 1) // k
return row
def _validate_input(self, row_index: int) -> None:
"""入力値が問題の制約を満たすかを検証する"""
# isinstance() で型チェックする。
# bool は int のサブクラスなので、isinstance(True, int) は True になってしまう。
# そのため True/False が誤って渡されるケースも別途弾いておく。
if not isinstance(row_index, int) or isinstance(row_index, bool):
raise TypeError("rowIndex must be an int")
if not (0 <= row_index <= 33):
raise ValueError("rowIndex must be between 0 and 33")
▶ 入力例 rowIndex = 3 での動作トレース
呼び出し: Solution().getRow(3) Step 1: _validate_input(3) → isinstance(3, int)かつboolではない → OK、0<=3<=33 → OK Step 2: row = [0, 0, 0, 0] を確保 → row[0] = 1 → row = [1, 0, 0, 0] Step 3: k=1 → row[1] = row[0] * (3-1+1) // 1 = 1 * 3 // 1 = 3 → row = [1, 3, 0, 0] Step 4: k=2 → row[2] = row[1] * (3-2+1) // 2 = 3 * 2 // 2 = 3 → row = [1, 3, 3, 0] Step 5: k=3 → row[3] = row[2] * (3-3+1) // 3 = 3 * 1 // 3 = 1 → row = [1, 3, 3, 1] 最終結果: [1, 3, 3, 1](Example 1の出力と一致)
🗺️ フローチャートの読み方
紫の矢印は「同じ行の中で次の k へ戻る」ループを表します。
🔎 入力例 rowIndex = 4 でのフロー追跡
フローの説明:
このアルゴリズムには「行を積み上げるループ」が存在しません。緑の矢印で示した通り、
入力検証を通過した後は「配列の確保」「基底値の設定」を1回ずつ行い、
あとは紫のループバック矢印が示す通り
k を
1つずつ増やしながら同じ行の中だけを計算し続けます。ループが二重になっていない点が、
三角形全体を作る方法との決定的な違いです。
📖 Big-O 記法の読み方(入力サイズ n が大きくなるにつれて処理時間がどう増えるかの目安)
| アプローチ | 時間計算量 | 空間計算量 | 備考 |
|---|---|---|---|
| 三角形全体を二次元リストで構築 | O(n²) | O(n²) | 直感的だが不要な行まで保持してしまう |
| 1本のリストを右から左へ更新(in-place) | O(n²) | O(n) | 追加空間は最小だが二重ループが必要 |
| 二項係数の漸化式(採用) | O(n) | O(n) | 同じ行の中だけで完結するため最速 |
🔍 なぜこの計算量になるのか
採用した実装では、ループが
k = 1 から
rowIndex
までの1重だけで、
各回の計算が「1つ前の値」を使った掛け算1回・割り算1回という定数時間で終わります。
そのため合計の時間計算量は入力(rowIndex)に比例する O(n) になります。
空間計算量も、出力用のリスト以外に一時配列や二次元リストを作らないため O(n)
に抑えられており、 フォローアップ条件をそのまま満たしています。
このページで登場した専門用語をまとめました。分からない言葉が出てきたときに参照してください(五十音順)。
def getRow(self, rowIndex: int) -> List[int]:
のように書きます。Pythonは動的型付け言語なので実行時には無視されますが、pylance(VSCodeの型チェッカー)が
実行前に誤った呼び出しを検出してくれます。
row[0] = 1
が基底値にあたります。
「rowIndex個から0個を選ぶ組み合わせは、何も選ばないという1通りしかない」という事実に対応しています。
C(n, k) = C(n, k-1) × (n-k+1) ÷ k
という二項係数の漸化式を使い、同じ行の中だけで次の値を求めています。
C(n, k)
と表記します。パスカルの三角形のrowIndex行目k番目の値は、実は
C(rowIndex, k)
そのものです。
//
演算子。小数点以下を切り捨てて
int
を返します。Pythonの
/(真の除算)は割り切れる場合でも 常に
float
を返してしまうため、
List[int]
という戻り値の型と合わせるために 今回は
//
を使っています。