Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions 0070.Climbing-Stairs/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 70. Climbing Stairs

## step1
再帰関数が素直な解法と思われる

## step2
DPでも解ける。二状態。

## 他の人のコード

https://github.com/huyfififi/coding-challenges/pull/16

https://github.com/NobukiFukui/Grind75-ProgrammingTraining/pull/30

https://github.com/rihib/leetcode/pull/35



> ぱっと思いつくのは、
>
> 深さ優先探索
> キャッシュ付き深さ優先探索
> 動的計画法
> 行列の n 乗
> フィボナッチ数列の一般項
> あたりでした。自分が面接官なら、動的計画法までを期待値とすると思います。すらすら解けたら、行列の n 乗の方法を説明したうえで、 m * m 正方行列の乗算のコードを書けるかどうか試すと思います。

期待値はクリアしたか?

行列のn乗は数列漸化式を行列で表すことを言っていると思う

> 結果をペアで返すことによっても速度が上げられます。

なるほど。知らなかった(認識していなかった)。

> 最終的に 1 の足し算に分解されるので計算量が O(フィボナッチ数) になります。
> 黄金比で評価できるまでは確認しておいて下さい。

メモ化再帰なしの計算量が O([黄金比]^n)になる

数列の漸化式から導かれる特性方程式の解をa, bとすると、そのうち一つが黄金比になる(aとする)。
漸化式の一般項は ka^{n} + lb^{n}と表される。a > bなのでnが十分大きい時には a^{n} の項が支配的となる(つまりF_{n+1}/F_{n}が黄金比に近づく)。
メモ化なしの再帰の場合にはフィボナッチ数列の値の数だけ呼び出しがおこなわれるので、呼び出し回数がO([黄金比]^n)と評価できる。

色々LLMにまとめさせる

### 補足(素朴再帰・フィボナッチ・黄金比)

- **O(フィボナッチ数)**
入力 `n` に対する **答えそのもの** がフィボナッチに関係する、という話だけではない。**メモなしで** `f(n)=f(n-1)+f(n-2)` を再帰すると、同じ部分問題を何度も評価するので、**必要な加算の回数や関数呼び出しの総数の増え方**がフィボナッチ型の漸化式に近づく、という意味での「フィボナッチオーダー」と捉えるとよい。

- **なぜ「1 の足し算に分解」と言えるか**
再帰の最終的な結果は、ベースケースの値どうしを木を遡って **足し算で合成** されていく。メモが無いと、その合成のために **同じ部分木の評価が何度も繰り返される** ので、葉や内部での「+1」相当の仕事量全体が爆増する。

- **時間のきつめの評価: O(φⁿ)**
呼び出し回数や演算回数の漸化が `T(n) ≈ T(n-1) + T(n-2) + O(1)` の形になると **特性方程式**
`λ² = λ + 1`
を考える。最大根は **黄金比 φ = (1+√5)/2 ≈ 1.618…**、もう一方は **ψ = (1−√5)/2**(絶対値 < 1)。
よって `T(n)` はおおむね **φⁿ に比例**(厳密にはもう一方の根の項もあるが、`|ψ|<1` で指数小的に効く)。
**O(2ⁿ)** は上界としては正しいが **かなりゆるい**。「完全二分のように常に2分叉する」と誤解すると 2ⁿ と言いがちだが、実際の再帰木は **共有されていない部分問題の再計算** であっても、増え方は **φⁿ オーダー** の方が実態に近い。

- **フィボナッチ数列との対応**
`F_n = F_{n-1} + F_{n-2}`, `F_0=0,F_1=1` なら
`F_n = (φⁿ − ψⁿ) / √5`
(ビネの公式)。`n` が大きいと **φⁿ / √5** が支配的。Climbing Stairs の数え方は初期条件をずらした **フィボナッチ型の列** になる(問題の定義とベースケースの取り方次第で 1 インデックスずれるだけ)。

- **対比(確認のため)**
**ボトムアップ DP**・**2 変数のループ**・**メモ化再帰**なら各 `k` は(高々)一度なので **時間 O(n)**、素朴再帰だけが **指数時間(底 φ)** のワナ。引用はこの対比を踏まえた **注意書き**。

### 行列の n 乗(線形漸化式・対数回の乗算)

階段問題の答え `ways(n)` は、よくある定義(`ways(1)=1`, `ways(2)=2`, …)では **通常のフィボナッチ数を 1 つインデックスずらしたもの**と一致する(例: `ways(n) = F_{n+1}` で `F_0=0, F_1=1, F_{n}=F_{n-1}+F_{n-2}`)。

連立 `F_{n+1} = F_n + F_{n-1}` と「いまの `F_n` を次のベクトルの第 2 成分にそのまま載せる」をまとめると

`(F_{n+1}, F_n)^T = Q × (F_n, F_{n-1})^T` ただし `Q = [[1,1],[1,0]]`

なので(繰り返し適用)

`(F_{n+1}, F_n)^T = Q^n × (F_1, F_0)^T = Q^n × (1, 0)^T`

(**`Q^n` の成分に `F_{n+1}` や `F_n` が並ぶ**と覚えておけば十分なことが多い。)

**ポイント**

- **サイズ固定(ここでは 2×2)の行列を二進法で累乗**すれば、**乗算の回数は O(log n)**(各ステップで 2×2 を定数回かけるだけ)。
- 面接で「**一般的な m×m 行列の積を書けるか**」と言われたら、まず **三重ループの定義**を書き、`n` 乗は **スカラーと同様の繰り返し二乗(binary exponentiation)**で **`n` をビット分解して行列を累積**すればよい、と説明できるとよい。
- **注意**: `ways(n)` の値自体の桁数は `n` に比例して増えるので、**多倍長整数**を使うと「乗算 1 回あたり」のコストも増え、**ビット演算量としての話は別**になる。LeetCode の `n ≤ 45` なら端末の整数で足りる。

**関連**: 2×2 に特化せずともよい **fast doubling(倍角公式)**も同じく O(log n) ステップでフィボナッチ型を求める別ルート。



13 changes: 13 additions & 0 deletions 0070.Climbing-Stairs/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import functools


class Solution:
def climbStairs(self, n: int) -> int:
@functools.cache
def climb_stairs_helper(n):
if n == 0 or n == 1:
return 1

return climb_stairs_helper(n - 1) + climb_stairs_helper(n - 2)

return climb_stairs_helper(n)
23 changes: 23 additions & 0 deletions 0070.Climbing-Stairs/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1

ways = 1
ways_previous = 1

for _ in range(n - 1):
ways, ways_previous = ways + ways_previous, ways

return ways


class Solution:
def climbStairs(self, n: int) -> int:
def helper(step: int) -> tuple[int, int]:
if step == 1:
return 1, 1
ways_prev_step, ways_two_steps_back = helper(step - 1)
return ways_prev_step + ways_two_steps_back, ways_prev_step

return helper(n)[0]