diff --git a/62. Unique Paths.md b/62. Unique Paths.md new file mode 100644 index 0000000..9946cf9 --- /dev/null +++ b/62. Unique Paths.md @@ -0,0 +1,145 @@ +### Step1 + +- 算数の問題? +- 小数点のずれが怖いので、最初に全部かけてから後で割った + +```python + +class Solution: + def comb(all, choice): + comb_number = 1 + for n in range(choice): + comb_number *= (all - n) + for n in range(choice): + comb_number /= (n + 1) + return comb_number + + def uniquePaths(self, m: int, n: int) -> int: + down_move_count = m - 1 + right_move_count = n - 1 + all_move_count = down_move_count + right_move_count + return comb(all_move_count, down_move_count) +``` + +## Step2 + +- 再帰を試したらTLE、再帰の回数が多すぎ + - 呼び出し回数、2^(m+n)かな? + +```python + +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + if m == 1 or n == 1: + return 1 + return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1) +``` + +- メモ化してDPでやるしかないかな + +```python +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + num_path = [[0] * m for _ in range(n)] + for row in range(n): + for column in range(m): + if row == 0 or column == 0: + num_path[row][column] = 1 + continue + num_path[row][column] = num_path[row - 1][column] + num_path[row][column - 1] + return num_path[n - 1][m - 1] +``` +- キャッシュデコレータ使えば再帰でもOK + +```python + +from functools import cache +class Solution: + @cache + def uniquePaths(self, m: int, n: int) -> int: + if m == 1 or n == 1: + return 1 + return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1) +``` +- メモリ線形バージョン + - メモリを節約する必要がある実用上の場面、実はよくわかっていない + - for文の_とiは、ちょっと今回の問題の趣旨だとわかりにくくてやばかったかも + +```python + +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + num_path = [0] * n + for _ in range(m): + for i in range(n): + if i == 0: + num_path[i] = 1 + continue + num_path[i] += num_path[i - 1] + return num_path[n - 1] +``` + +- 上のを、以下のを参考にして直す + - https://github.com/hayashi-ay/leetcode/pull/39/files + - これも見てみる(https://github.com/SuperHotDogCat/coding-interview/pull/16/files) + - O(min(m, n))のメモリにできますね、なるほど + - 個人的には、初期化はall zeroで、all oneの配列をfor文の中で作りたいかも + - 上の二次元DPの処理とも一致する + - 今までのコード、最後に配列を返すとき[-1]の方がいいか迷う + - 1で初期化する手もあったが、なんとなくスッキリしなくて後で1を代入することにした + + ```python + class Solution: + def uniquePaths(self, m: int, n: int) -> int: + if n > m: + return self.uniquePaths(n, m) + num_path = [0] * n + for col in range(m): + for row in range(n): + if row == 0: + num_path[row] = 1 + continue + num_path[row] += num_path[row - 1] + return num_path[n - 1] + ``` + +- math.combの利用 + +```python + +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + return comb((m - 1) + (n - 1), n - 1) +``` + +mathモジュールの階乗のコードを見る(https://github.com/python/cpython/blob/main/Modules/mathmodule.c) + +おそらく掛け算がビットシフトより遅いのでこういう工夫がされてる? + +- factorial_partial_product + - startからstopまでの奇数の階乗の積を、64bitオーバーフローしない範囲まで分割統治で区切ってから計算。(オーバーフローの判定は最大値のbitcountにmultiply_countをかける、logの性質) +- factorial_odd_part + - 階乗の奇数パートを計算。innerに、nまでの数の中で2で最大k回割れる数の積の奇数パートを、factorial_partial_productを利用してkが大きい順に保存。それを順にouterにかけていく。 + - 1, 1, 1*3, 1*3*5、、、みたいにだんだん掛け算範囲が増加して計算されるので、ループごとに前の値を保存して節約 +- math_factorial + - factorial_odd_partで奇数パート計算したあと、2で割り切れる数ぶんbit shiftする(これはn//2 + n//4 + ,,, で計算) + +mathモジュールのcombのコード + +- perm_comb_small C(n, k)のnが64bitに収まるとき かつkが1ではないとき + - 結果が64bitに収まる、かつk ≤ 34 + - 階乗とその逆数(mod 2^64)について、奇数パートと、2で何回割り切れるかの配列が、ある程度小さいのなら作ってあるので、そこから頑張って計算 + - 結果がlong longに収まる、かつk≤13 + - *n/1 *(n-1)/2 *(n-2)/3 を繰り返す(確かにこれなら途中で非整数にならない) + - その他 + - C(n, k) = C(n, j) * C(n-j, k-j) // C(k, j) を利用して再帰的に +- perm_comb nが64bitに収まらないがkは収まる、もしくはk=1の時 + - kが0→1を返す  kが1→nを返す + - C(n, k) = C(n, j) * C(n-j, k-j) // C(k, j) を利用して再帰的に + - kはoverflowしてはいけないので、分母のC(k, j)はperm_comb_smallを利用 +- math_comb_impl 多分本体 条件に従って上のどちらかに渡してるだけだが、以下の操作もしている + - nとkが負でないかのチェック + - k = min(n-k, k)にする。そのあとkがoverflowしないかもチェック + - n < kなら0を返す + +こういうのの実装、見ると楽しいことに気づいたが、あんまりハマりすぎても問題が進まないので程々に