|
| 1 | +> 🎯 **[algo-beginner スキル発火]** |
| 2 | +> 言語/カテゴリ: TypeScript |
| 3 | +> 適用ルールセット: 共通5ルール + TS固有5ルール |
| 4 | +> 参照ファイル: references/common.md + references/typescript.md |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +# Pascal's Triangle(パスカルの三角形) |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## 1. 問題の分析 |
| 13 | + |
| 14 | +> 💡 **初学者向け補足** |
| 15 | +> この問題は一言で言うと、**「三角形の形に並んだ数値の表を、上から順番に行を積み上げて作る問題」** です。 |
| 16 | +
|
| 17 | +パスカルの三角形とは何かを先に目で確認しましょう。 |
| 18 | + |
| 19 | +``` |
| 20 | +行0: [1] |
| 21 | +行1: [1, 1] |
| 22 | +行2: [1, 2, 1] |
| 23 | +行3: [1, 3, 3, 1] |
| 24 | +行4: [1, 4, 6, 4, 1] |
| 25 | +``` |
| 26 | + |
| 27 | +**ルール**はたった2つです: |
| 28 | + |
| 29 | +- 各行の **両端は必ず `1`** |
| 30 | +- 内側の要素は **真上の左と右の要素を足した値**(例:行2の `2` = 行1の `1 + 1`) |
| 31 | + |
| 32 | +--- |
| 33 | + |
| 34 | +### 競技プログラミング視点での分析 |
| 35 | + |
| 36 | +- 各行を前の行から `O(k)`(kはその行の要素数)で構築できます |
| 37 | +- 全体の要素数は `1 + 2 + 3 + ... + numRows = numRows*(numRows+1)/2` なので、**最低でも O(numRows²) の時間・空間**が必要 |
| 38 | +- `numRows ≤ 30` という制約(=入力が最大30という上限)があるため、最悪でも `30*31/2 = 465要素` しか生成しません。計算量を特別に最小化しなくても十分高速です |
| 39 | + |
| 40 | +### 業務開発視点での分析 |
| 41 | + |
| 42 | +- **型安全性**:戻り値 `number[][]`(数値の2次元配列)を明示し、各行が `number[]` であることをコンパイル時に保証します |
| 43 | +- **エラーハンドリング**:制約 `1 ≤ numRows ≤ 30` の範囲外入力を実行時に検出して弾きます |
| 44 | +- **イミュータブル志向**(=データを直接書き換えず、新しいデータとして作る考え方):各行を新しい配列として構築し、前の行を壊しません |
| 45 | + |
| 46 | +### TypeScript特有の考慮点 |
| 47 | + |
| 48 | +- **型推論**(=型を書かなくてもTypeScriptが自動判断する機能):`prev.length` などから `number` 型が自動推論されます |
| 49 | +- `readonly number[]`(=読み取り専用の数値配列)を前行の型として使い、誤って前行を書き換えるバグをコンパイル時に防止します |
| 50 | +- 戻り値の `number[][]` の明示により、呼び出し元で型エラーが起きにくくなります |
| 51 | + |
| 52 | +> 📖 **このセクションで登場した用語** |
| 53 | +> |
| 54 | +> - **コンパイル時**:TypeScriptのコードをJavaScriptに変換する段階。ここでエラーを検出すると、プログラムを実行する前にバグを発見できる |
| 55 | +> - **型安全性**:間違った型(例:数値の場所に文字列を渡すなど)のデータが紛れ込まないように守る仕組み |
| 56 | +> - **イミュータブル**:変更できない・変更しない状態のこと。元のデータを壊さないため、バグが起きにくい |
| 57 | +
|
| 58 | +--- |
| 59 | + |
| 60 | +## 2. アルゴリズムアプローチ比較 |
| 61 | + |
| 62 | +> 💡 **初学者向け補足** |
| 63 | +> 同じ問題でも解き方は複数あります。それぞれの「速さ(時間計算量(=処理にかかる手間の目安))」と「メモリの使いやすさ(空間計算量(=使うメモリ量の目安))」を比べて最適なものを選びます。 |
| 64 | +
|
| 65 | +| アプローチ | 時間計算量 | 空間計算量 | TS実装コスト | 型安全性 | 可読性 | 備考 | |
| 66 | +| ----------------------------- | ---------- | ---------- | ------------ | -------- | ------ | --------------------------------------------------------- | |
| 67 | +| **A. 逐次行構築(今回選択)** | O(n²) | O(n²) | 低 | 高 | 高 | 前の行だけ見て次の行を作る | |
| 68 | +| B. 二項係数で直接計算 | O(n²) | O(n²) | 中 | 中 | 中 | C(n,k)公式を使う。大きいnで整数オーバーフローのリスクあり | |
| 69 | +| C. 再帰(メモ化なし) | O(n³) | O(n²) | 高 | 中 | 低 | 同じ値を何度も再計算するため遅い | |
| 70 | + |
| 71 | +> 💡 **Big-O記法の読み方**(初学者向け) |
| 72 | +> |
| 73 | +> - `O(1)`:入力の大きさに関わらず、常に一定の時間・メモリ |
| 74 | +> - `O(n)`:入力が2倍になると処理も約2倍 |
| 75 | +> - `O(n²)`:入力が2倍になると処理は約4倍(二重ループに多い) |
| 76 | +
|
| 77 | +> 📖 **このセクションで登場した用語** |
| 78 | +> |
| 79 | +> - **二項係数**:組み合わせの数を表す数学的な値。`C(n, k) = n! / (k! * (n-k)!)` で計算できるが、階乗(=1×2×3×…×n)が大きくなりすぎる問題がある |
| 80 | +> - **再帰**:関数が自分自身を呼び出して問題を解く方法。ツリー構造の問題に向くが、呼び出しが深くなるとメモリを消費する |
| 81 | +
|
| 82 | +--- |
| 83 | + |
| 84 | +## 3. 選択したアルゴリズムと理由 |
| 85 | + |
| 86 | +- **選択したアプローチ**: **A. 逐次行構築(イテレーティブ=繰り返し処理による方法)** |
| 87 | +- **理由**: |
| 88 | + - **方法Bを選ばなかった理由**:二項係数は数式で一見エレガントですが、`numRows=30`のとき `30!`(30の階乗)という非常に大きな数を扱う必要があり、JavaScriptのNumber型(=浮動小数点数)で精度が落ちるリスクがあります |
| 89 | + - **方法Cを選ばなかった理由**:再帰は同じ値を何度も計算し直すため、O(n³) と最も遅く、この問題には不向きです |
| 90 | + - **方法Aを選んだ理由**:前の行だけを参照して次の行を作る「積み上げ式」のため、計算の無駄がなく、コードの流れも人間の直感と一致していて読みやすいです |
| 91 | + |
| 92 | +- **TypeScript特有の最適化ポイント**: |
| 93 | + - `readonly number[]` で前行の参照を保護し、「前行を誤って書き換えるバグ」をコンパイル時に防止します |
| 94 | + - 戻り値型 `number[][]` の明示により、呼び出し元でも型の恩恵を受けられます |
| 95 | + - 入力検証を関数冒頭で行い、不正な `numRows` が渡されたときに分かりやすいエラーを返します |
| 96 | + |
| 97 | +> 📖 **このセクションで登場した用語** |
| 98 | +> |
| 99 | +> - **イテレーティブ**:ループ(for文など)で繰り返す処理方法。再帰と対比して使う語 |
| 100 | +> - **浮動小数点数**:コンピュータが小数を近似値で表す方式。非常に大きな整数を扱うと誤差が生じることがある |
| 101 | +> - **コンパイル時エラー**:TypeScriptのコードをJavaScriptに変換する際に発見されるエラー。実行前にバグを発見できる |
| 102 | +
|
| 103 | +--- |
| 104 | + |
| 105 | +## 4. 実装コード |
| 106 | + |
| 107 | +> 💡 **このコードの大まかな構造(骨格)** |
| 108 | +> |
| 109 | +> 1. 入力値 `numRows` の検証(範囲外の値を弾く) |
| 110 | +> 2. 結果を格納する空の2次元配列を用意する |
| 111 | +> 3. 1行目(`[1]`)を無条件でセットする |
| 112 | +> 4. 2行目以降は「前の行」を参照して両端を`1`、内側を隣合う値の和で埋める |
| 113 | +> 5. 完成した三角形全体を返す |
| 114 | +
|
| 115 | +```typescript |
| 116 | +// Runtime 1 ms |
| 117 | +// Beats 33.72% |
| 118 | +// Memory 56.02 MB |
| 119 | +// Beats 78.88% |
| 120 | + |
| 121 | +/** |
| 122 | + * パスカルの三角形の最初の numRows 行を生成して返す |
| 123 | + * @param numRows - 生成する行数(1以上30以下) |
| 124 | + * @returns 各行を number[] で表した2次元配列 |
| 125 | + * @throws {RangeError} numRows が制約範囲外のとき |
| 126 | + * @complexity Time: O(n²), Space: O(n²) n = numRows |
| 127 | + */ |
| 128 | +function generate(numRows: number): number[][] { |
| 129 | + // ─── 入力検証 ───────────────────────────────────────────── |
| 130 | + // 制約「1 ≤ numRows ≤ 30」を満たさない値が来たときに |
| 131 | + // 後続処理で意味不明なバグになる前に、分かりやすいエラーを投げる |
| 132 | + if (!Number.isInteger(numRows) || numRows < 1 || numRows > 30) { |
| 133 | + throw new RangeError(`numRows must be an integer between 1 and 30, got: ${numRows}`); |
| 134 | + } |
| 135 | + |
| 136 | + // ─── 結果配列の準備 ─────────────────────────────────────── |
| 137 | + // number[][] = 「数値の配列」を要素とする配列(2次元配列) |
| 138 | + // TypeScriptが戻り値の型をここで確定させるため、明示的に型を付ける |
| 139 | + const triangle: number[][] = []; |
| 140 | + |
| 141 | + // ─── 行を1行ずつ積み上げる ──────────────────────────────── |
| 142 | + for (let rowIndex = 0; rowIndex < numRows; rowIndex++) { |
| 143 | + // 各行は rowIndex+1 個の要素を持つ(0行目は1個、1行目は2個…) |
| 144 | + // Array.from で「長さだけ決まった配列」を作り、すべて 1 で初期化する |
| 145 | + // 初期値を 1 にする理由:両端は必ず 1 なので、後で端以外だけ上書きすれば済む |
| 146 | + const currentRow: number[] = Array.from({ length: rowIndex + 1 }, () => 1); |
| 147 | + |
| 148 | + // 内側の要素を計算する(先頭と末尾は既に 1 なのでスキップ) |
| 149 | + // 例)rowIndex=3 のとき、更新するのは index 1 と 2 だけ |
| 150 | + for (let col = 1; col < rowIndex; col++) { |
| 151 | + // 前の行(readonly として参照)の左上 + 右上 = 現在のセルの値 |
| 152 | + // readonly number[] を型として用いることで、前行を誤って |
| 153 | + // 書き換えるバグをコンパイル時に防止している(TypeScript固有の恩恵) |
| 154 | + const prevRow: readonly number[] = triangle[rowIndex - 1]; |
| 155 | + currentRow[col] = prevRow[col - 1] + prevRow[col]; |
| 156 | + } |
| 157 | + |
| 158 | + // 完成した行を三角形に追加する |
| 159 | + triangle.push(currentRow); |
| 160 | + } |
| 161 | + |
| 162 | + // 全行が揃った三角形を返す |
| 163 | + return triangle; |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +--- |
| 168 | + |
| 169 | +### 💡 コードの動作トレース(`numRows = 5` の場合) |
| 170 | + |
| 171 | +入力がどのように変化していくかをステップごとに追います。 |
| 172 | + |
| 173 | +``` |
| 174 | +入力: numRows = 5 |
| 175 | +
|
| 176 | +─── 入力検証 ─── |
| 177 | +Number.isInteger(5) → true |
| 178 | +5 >= 1 かつ 5 <= 30 → 検証通過 ✅ |
| 179 | +
|
| 180 | +─── ループ開始 ─── |
| 181 | +
|
| 182 | +[rowIndex = 0] |
| 183 | + currentRow の初期 = [1] (長さ1、すべて1) |
| 184 | + 内側ループ: col の範囲 1 ~ -1 → 実行なし(両端だけの行) |
| 185 | + triangle = [[1]] |
| 186 | +
|
| 187 | +[rowIndex = 1] |
| 188 | + currentRow の初期 = [1, 1] (長さ2、すべて1) |
| 189 | + 内側ループ: col の範囲 1 ~ 0 → 実行なし(1と1の2要素だけ) |
| 190 | + triangle = [[1], [1,1]] |
| 191 | +
|
| 192 | +[rowIndex = 2] |
| 193 | + currentRow の初期 = [1, 1, 1] (長さ3、すべて1) |
| 194 | + 内側ループ: col=1 のみ |
| 195 | + prevRow = [1, 1] |
| 196 | + currentRow[1] = prevRow[0] + prevRow[1] = 1 + 1 = 2 |
| 197 | + currentRow 確定 = [1, 2, 1] |
| 198 | + triangle = [[1], [1,1], [1,2,1]] |
| 199 | +
|
| 200 | +[rowIndex = 3] |
| 201 | + currentRow の初期 = [1, 1, 1, 1] (長さ4、すべて1) |
| 202 | + 内側ループ: col=1, col=2 |
| 203 | + col=1: prevRow=[1,2,1] → currentRow[1] = 1 + 2 = 3 |
| 204 | + col=2: prevRow=[1,2,1] → currentRow[2] = 2 + 1 = 3 |
| 205 | + currentRow 確定 = [1, 3, 3, 1] |
| 206 | + triangle = [[1],[1,1],[1,2,1],[1,3,3,1]] |
| 207 | +
|
| 208 | +[rowIndex = 4] |
| 209 | + currentRow の初期 = [1, 1, 1, 1, 1] (長さ5、すべて1) |
| 210 | + 内側ループ: col=1, col=2, col=3 |
| 211 | + col=1: prevRow=[1,3,3,1] → currentRow[1] = 1 + 3 = 4 |
| 212 | + col=2: prevRow=[1,3,3,1] → currentRow[2] = 3 + 3 = 6 |
| 213 | + col=3: prevRow=[1,3,3,1] → currentRow[3] = 3 + 1 = 4 |
| 214 | + currentRow 確定 = [1, 4, 6, 4, 1] |
| 215 | + triangle = [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] |
| 216 | +
|
| 217 | +─── 完成 ─── |
| 218 | +出力: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]] ✅ |
| 219 | +``` |
| 220 | + |
| 221 | +> 📖 **このセクションで登場した用語** |
| 222 | +> |
| 223 | +> - **`readonly`**:変数の値を変更できないようにするTypeScript固有の修飾子。JavaScriptにはこの制限がなく、意図せぬ書き換えがバグの原因になりやすい。TypeScriptで `readonly` を付けるとコンパイル時に書き換えを防止できる |
| 224 | +> - **`RangeError`**:値の範囲が不正な場合に投げるエラーの種類。例えば「1〜30以外の数値」など、型は正しいが値が不正なときに使う(型そのものが違う場合は `TypeError` を使う) |
| 225 | +> - **`Array.from({ length: n }, () => 1)`**:長さ `n` の配列を作り、全要素を `1` で初期化するイディオム(=よく使われる定番の書き方)。`new Array(n).fill(1)` と同じ効果だが、こちらの方が型推論と相性が良い |
| 226 | +> - **Pure function(純粋関数)**:同じ入力を与えると必ず同じ出力を返し、外部の変数や状態を書き換えない関数。テストしやすく、バグが起きにくい |
| 227 | +
|
| 228 | +--- |
| 229 | + |
| 230 | +## 5. LeetCode 提出用コード |
| 231 | + |
| 232 | +```typescript |
| 233 | +function generate(numRows: number): number[][] { |
| 234 | + const triangle: number[][] = []; |
| 235 | + |
| 236 | + for (let rowIndex = 0; rowIndex < numRows; rowIndex++) { |
| 237 | + const currentRow: number[] = Array.from({ length: rowIndex + 1 }, () => 1); |
| 238 | + |
| 239 | + for (let col = 1; col < rowIndex; col++) { |
| 240 | + const prevRow: readonly number[] = triangle[rowIndex - 1]; |
| 241 | + currentRow[col] = prevRow[col - 1] + prevRow[col]; |
| 242 | + } |
| 243 | + |
| 244 | + triangle.push(currentRow); |
| 245 | + } |
| 246 | + |
| 247 | + return triangle; |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## TypeScript固有の最適化観点まとめ |
| 254 | + |
| 255 | +### 型安全性の活用 |
| 256 | + |
| 257 | +- **`readonly number[]`**:前行の参照に `readonly` を付けることで「前行を書き換えてしまう」バグをコンパイル時に検出できます。JavaScriptには `readonly` の概念がなく、実行して初めてバグに気づく場合があります |
| 258 | +- **`number[][]` の明示**:戻り値型を明示することで、呼び出し元でも配列の各要素が `number[]` であるという情報が伝わります。型を省略すると、IDEの補完(IntelliSense)が弱くなります |
| 259 | + |
| 260 | +### コンパイル時最適化 |
| 261 | + |
| 262 | +- **型推論の活用**:`Array.from({ length: rowIndex + 1 }, () => 1)` の戻り値は `number[]` と自動推論されるため、`:number[]` を明示しなくても型チェックが機能します |
| 263 | +- **`Number.isInteger()`** を使った入力バリデーション:`typeof numRows === 'number'` だけでは小数(例:`5.5`)が通ってしまうため、より厳密な整数チェックを行っています |
| 264 | + |
| 265 | +### 開発効率と保守性 |
| 266 | + |
| 267 | +- 各ループ変数 `rowIndex`・`col` に意味のある名前を付けることで、「何番目の行か」「何番目の列か」が一目で分かります |
| 268 | +- `triangle[rowIndex - 1]` を `prevRow` という変数に取り出すことで、コードが「前の行を参照している」という意図を明確に伝えます |
0 commit comments