アルゴリズム概要

💡 この問題を一言で言うと:「パスカルの三角形の、指定された行だけを無駄なく作る問題」です。

パスカルの三角形は、各行の両端が常に 1 で、 それ以外の値は「1つ上の行の隣り合う2つの数の和」になっている数の並びです。 今回の問題では三角形全体を作る必要はなく、rowIndex 番目(0-indexed=0から数え始める番号付け)の行だけを返せばよい、という点がポイントです。

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

  • 三角形を上から1行ずつ積み上げる素直な方法は、二重ループが必要になり計算量が O(rowIndex²) になってしまいます。
  • フォローアップ条件「O(rowIndex) の追加空間のみ」を満たすには、過去の行を丸ごと保持する二次元配列を作らない工夫が必要です。
O(rowIndex)
時間計算量
O(rowIndex)
追加空間計算量
binomial_recurrence
採用アルゴリズム
list[int]
使用データ構造

📥 入出力例

Example 1
rowIndex = 3
Output: [1,3,3,1]

3行目(0-indexed)の両端は1、内側は1つ上の行の隣接する値の和になるため [1,3,3,1] が正解です。

Example 2
rowIndex = 0
Output: [1]

0行目は三角形の頂点そのものなので、要素数1の [1] が正解です。

Example 3
rowIndex = 1
Output: [1,1]

1行目は両端の1だけで構成されるため [1,1] が正解です。

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

代表例 rowIndex = 4 を使って、 配列 row がどのように埋まっていくかを1ステップずつ追跡します。

Python実装

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

  1. rowIndex の型と範囲を検証する(型ガード)
  2. 結果を格納するリストを rowIndex + 1 個分だけ確保する
  3. 先頭を1に設定し、二項係数の漸化式で k=1 から rowIndex まで値を埋めていく
  4. 完成したリストを返す
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 へ戻る」ループを表します。

開始 getRow(rowIndex) を呼び出す rowIndexは正しいか int型かつ 0以上33以下か いいえ エラーを送出 TypeError / ValueError はい 配列を確保する row = [0] × (rowIndex + 1) 基底値を設定する row[0] = 1 kはrowIndex以下か k = 1, 2, ..., rowIndex はい row[k] を計算する row[k] = row[k-1] × (n-k+1) ÷ k (n = rowIndex、// で整数のまま計算) 次のkへ いいえ(完了) 終了 row を返す

🔎 入力例 rowIndex = 4 でのフロー追跡

  1. 「開始」ノード → rowIndex = 4 を受け取る
  2. 「rowIndexは正しいか」ノード → int型かつ0〜33の範囲内なので「はい」の経路へ
  3. 「配列を確保する」→「基底値を設定する」→ row = [1, 0, 0, 0, 0] になる
  4. 「kはrowIndex以下か」を4回「はい」で通過し、そのたびに「row[k]を計算する」でrowを更新する(紫の矢印で同じ行の中をループ)
  5. k=5になった時点で「いいえ(完了)」の経路に進み、「終了」ノードで [1, 4, 6, 4, 1] を返す

フローの説明:
このアルゴリズムには「行を積み上げるループ」が存在しません。緑の矢印で示した通り、 入力検証を通過した後は「配列の確保」「基底値の設定」を1回ずつ行い、 あとは紫のループバック矢印が示す通り k を 1つずつ増やしながら同じ行の中だけを計算し続けます。ループが二重になっていない点が、 三角形全体を作る方法との決定的な違いです。

計算量分析

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

O(1)
常に一定
例:辞書の直接引き
O(n)
入力に比例
例:リストを1回走査
O(n log n)
nより少し多い
例:ソートアルゴリズム
O(n²)
入力の2乗
例:二重ループ総当たり
アプローチ 時間計算量 空間計算量 備考
三角形全体を二次元リストで構築 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通りしかない」という事実に対応しています。
計算量
入力の大きさに対して、処理にかかる時間(時間計算量)やメモリ(空間計算量)がどう増えるかの目安。 O(1), O(n), O(n log n), O(n²) のようなBig-O記法で表します。
漸化式
ある項の値を、1つ前の項の値から計算するための式。今回は C(n, k) = C(n, k-1) × (n-k+1) ÷ k という二項係数の漸化式を使い、同じ行の中だけで次の値を求めています。
二項係数
n個の中からk個を選ぶ組み合わせの数。C(n, k) と表記します。パスカルの三角形のrowIndex行目k番目の値は、実は C(rowIndex, k) そのものです。
フロア除算
Pythonの // 演算子。小数点以下を切り捨てて int を返します。Pythonの /(真の除算)は割り切れる場合でも 常に float を返してしまうため、 List[int] という戻り値の型と合わせるために 今回は // を使っています。