diff --git a/35.Search-Insert-Position/memo.md b/35.Search-Insert-Position/memo.md new file mode 100644 index 0000000..2e32802 --- /dev/null +++ b/35.Search-Insert-Position/memo.md @@ -0,0 +1,126 @@ +# 35. Search Insert Position + +sol1.py: bisect ライブラリを使うと秒殺 + +これでは何の練習にならないので自力で書く + +sol2.py:インデックスの1のずれで混乱 + +sol3.pyに改善。bisect_leftに関数を分けるのもやめる + + +left == rightのときこれらが同時になりたつ→答え +leftに+1があるので無限ループが発生しない + +https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562 + +> 二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか? +位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか? +範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? +用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? +用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? +用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? + + +```python +# 1. [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 +# false と true の境界の位置を求める問題と捉える。 +# ここでは、 target より小さい値は false、 target 以上の値は true とする。 +# 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。 +# 3. 範囲を考えるにあたり、半開区間を用いる。 +# 4. 初期値は 0, len(numbers) とする。 +def lower_bound(numbers, left, right, target): + # 5. ループの終了状態で false, false, ..., false, [true), true, ..., true となって欲しい。 + # この書き方は正式な書き方ではなく、自分が考えるときのイメージです。 + # [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。 + # ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。 + # よってループの不変条件は left < right とする。 + while left < right: + middle = (left + right) // 2 + # target より小さい値は false、 target 以上の値は true とする。 + if (numbers[middle] < target): + # 6. 右に狭める場合。 + # middle の位置の要素を、狭めたあとの区間に含めたくない。 + # 左側は閉区間である。 + # middle の位置の要素を区間に含めないようにするには、 + # left の位置を middle + 1 にすればよい。 + left = middle + 1 + else: + # 左に狭める場合。 + # middle の位置の要素を、狭めたあとの区間に含めたくない。 + # なぜならば、今回は境界の位置を求める問題と捉ええているためである。 + # 別の言い方をすると、境界の位置を middle より左側にしたい。 + # 右側は開区間である。 + # middle の位置の要素を区間に含めないようにするには、 + # right の位置を middle にすればよい。 + right = middle + return left + + +def upper_bound(numbers, left, right, target): + while left < right: + middle = (left + right) // 2 + if (numbers[middle] <= target): + left = middle + 1 + else: + right = middle + return left + + +if __name__ == '__main__': + print(lower_bound([0, 1, 2, 3, 4, 5], 0, 6, 3)) + print(upper_bound([0, 1, 2, 3, 4, 5], 0, 6, 3)) +``` + + +保証している条件 +nums[left-1] < target +nums[right] >= target or rightが右端 + +ループ不変条件: left < right + +つまり flag_nums = [n>=target for n in nums] としたとき +i < left→ flag_nums[i] == False +i >= right → flag_nums[i] == True +終了時 left = rightでは right: flag_nums[i] == Trueを満たす最小のi が保証される + +--- +## コメントをいただいて追記: + +### ループ不変式: + +i < left -> nums[i] < target (1) + +&& j >= right -> nums[j] >= target (2) + + +### 成り立つことの確認 + +#### 初期化時: left=0, right=len(nums) +i < left, j >= rightとなるi, jは存在しない。 + +#### 維持: ある反復で成り立つと仮定して次の反復で成り立つことを示す。場合分け。 +- nums[middle] < target の場合: numsがソートされているため left=middle+1とした場合、(1) は依然として成立 +- nums[middle] >= target の場合 numsがソートされているため right=middle とした場合(2) は依然として成立 + +#### 終了: left < rightがFalseとなる。更新式よりleft==rightである +(1),(2)と合わせて、 + +i nums[i] <= target && i >= left -> nums[i] > target + +これはleftがnums[i] > targetを満たす最小のiであることを意味する。 + + +--- + +https://github.com/seal-azarashi/leetcode/pull/38 + +しっかりと理解する必要があるな + +> 二分探索で「書き方」を固定することはできるのですが、「読む方法」を固定することは普通はできないです。書いている人がどういう考えで書くか普通は強制できないからです。 +> ジャッジシステムを相手にしているならば別にいいのですが、人間に技術面接で出題された場合は、多くの場合、どうしてそのコードが動くのかを聞きます。 +> 技術面接で見ているのが「一緒に働いたときに成果がより出るか」で、そのために必要な「簡単なコードが読めて書けて管理ができる」かを知りたいからです。(余計な絶対値があれば、分かっているかより気になるでしょう。) +> というわけで、書き方を固定してもいいけれども、幅のある表現を読めるようにしてくださいね、ということです。 + +https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0 + diff --git a/35.Search-Insert-Position/sol1.py b/35.Search-Insert-Position/sol1.py new file mode 100644 index 0000000..1a82352 --- /dev/null +++ b/35.Search-Insert-Position/sol1.py @@ -0,0 +1,6 @@ +import bisect + + +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + return bisect.bisect_left(nums, target) diff --git a/35.Search-Insert-Position/sol2.py b/35.Search-Insert-Position/sol2.py new file mode 100644 index 0000000..d549fc1 --- /dev/null +++ b/35.Search-Insert-Position/sol2.py @@ -0,0 +1,21 @@ +def bisect_left(nums, target): + left = 0 + right = len(nums) - 1 + while right - left > 1: + mid = left + (right - left) // 2 + if nums[mid] == target: + return mid + elif nums[mid] < target: + left = mid + else: + right = mid + if target <= nums[left]: + return left + if target <= nums[right]: + return right + return right + 1 + + +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + return bisect_left(nums, target) diff --git a/35.Search-Insert-Position/sol3.py b/35.Search-Insert-Position/sol3.py new file mode 100644 index 0000000..2789436 --- /dev/null +++ b/35.Search-Insert-Position/sol3.py @@ -0,0 +1,11 @@ +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) + while right > left: + mid = left + (right - left) // 2 + if nums[mid] < target: + left = mid + 1 + else: + right = mid + return left diff --git a/35.Search-Insert-Position/sol4.py b/35.Search-Insert-Position/sol4.py new file mode 100644 index 0000000..cb2a394 --- /dev/null +++ b/35.Search-Insert-Position/sol4.py @@ -0,0 +1,9 @@ +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + if not nums: + return 0 + right = len(nums) + mid = right // 2 + if nums[mid] < target: + return mid + 1 + self.searchInsert(nums[mid + 1 : right], target) + return self.searchInsert(nums[:mid], target) diff --git a/35.Search-Insert-Position/sol5_retry.py b/35.Search-Insert-Position/sol5_retry.py new file mode 100644 index 0000000..1a82352 --- /dev/null +++ b/35.Search-Insert-Position/sol5_retry.py @@ -0,0 +1,6 @@ +import bisect + + +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + return bisect.bisect_left(nums, target) diff --git a/35.Search-Insert-Position/sol6_retry.py b/35.Search-Insert-Position/sol6_retry.py new file mode 100644 index 0000000..3b424fb --- /dev/null +++ b/35.Search-Insert-Position/sol6_retry.py @@ -0,0 +1,11 @@ +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) + while left < right: + middle = left + (right - left) // 2 + if nums[middle] < target: + left = middle + 1 + else: + right = middle + return left diff --git a/README.md b/README.md index d63cee4..906ada2 100644 --- a/README.md +++ b/README.md @@ -1 +1,466 @@ # Coding + +
+0141.Linked List Cycle + +- `set`を使った解法 +- `set`と`dict`がどちらもhashtableで値を格納している +- そのためdictのkeyはhashableである必要がある +- `__eq__` が `True` なら、`__hash__` も等しくなければならない + +
+
+0142.Linked List Cycle II + +- Floydの循環検出法(有名だが常識ではない) +- `is`と`==`の違い: オブジェクトの同一性を比較する。同じインスタンスかどうか + - `is`: オブジェクトの同一性を比較する。同じインスタンスかどうかを`id()`で判定(CPythonではメモリアドレスとして実装されている)。 + - `==`: `__eq__`でオブジェクトの等価性を比較する。 + +
+ +
+83.Remove Duplicates from Sorted List + +- Linked Listの操作 +- 変数名には組み込み名と衝突する名前を使わない + +
+ +
+82.Remove Duplicates from Sorted List II + +- 変数名の意味を考える +- currentの名前は情報量がない +- dummyを番兵として用いる方法は自然に解釈できる + +
+ + +
+0002.Add Two Numbers + +- 多倍長整数の加算 + +
+ +
+20. Valid Parentheses + +- PEP-8 と Google Style Guide では strings, lists, tuples は implicit で真偽判定 +- 副作用のある式を条件のところに書かない方が良いかもしれない。読む側が頭の中で実行順序を追う必要が出るため。e.g. `stack.pop()` + +
+ +
+206. Reverse Linked List + +- 一度の走査で行うポインタの付け替え + +
+
+703. Kth Largest Element in a Stream + +- heapqライブラリの使い方 +- マージソート、クイックソート、クイックセレクトの確認 +- ヒープの構築がO(n)であることが抜けていた + +
+
+347. Top K Frequent Elements + +- ヒープの構築はO(n) (再) +- クイックソートの実装 +- 一行で行う操作を増やしすぎない + +
+ +
+98. Validate Binary Search Tree + +- 木の探索順(preorder, inorder, postorder) +- 再帰とループの変換 +- ロジックの流暢さ + +
+
+373.Find K Pairs with Smallest Sums + +- 全組み合わせを作らず、次に小さくなりうる候補だけをヒープで管理する +- 分からない場合には手作業で考える + +
+ +
+1. Two Sum + +- Hashmapを使うと、必要な相手の値を平均O(1)で確認できるため、全探索のO(n^2)をO(n)に改善できる +- Type HintsはPython3から導入され、Docstringの型指定が使われなくなった +- 実装から時間計算量を見積もる時の目安 + - C++: 約 1~10 億 (10^8~10^9) ステップ/秒、Python: 約 100 万~1000 万 (10^6~10^7) ステップ/秒 + +
+ +
+49. Group Anagrams + +- 理論上は文字頻度を使う方が速いが、Pythonの組み込みsortはC実装で高速なので、入力サイズによってはsortedを使う方が実測で速いことがある +- listはmutableでhashableではないため、dictのkeyにするにはtupleへ変換する必要がある + +
+ +
+349. Intersection of Two Arrays + +- setはhash tableで実装されており、構築は平均O(n)、membership checkは平均O(1) +- 短い方をsetにすると、空間計算量をO(min(l1, l2))に抑えられる +- 入力がソート済み、片方だけ巨大、メモリ制約が厳しいなど、追加条件によって適した解法は変わる +- ソート済み配列同士なら、マージソートのmergeに近いtwo pointersでO(l1 + l2) +- マージソート + - pros: 最悪計算量もO(n log n)で安定、stable sort、外部ソートに向く + - cons: 配列では追加メモリO(n)、コピーの定数倍が重い、実測ではquick sortより遅いことが多い + + +
+ + +
+929. Unique Email Addresses + +- `str.partition`: 区切り文字で「左、 +区切り文字、右」の3要素に分けるため、最初の1回だけ分割したい意図を表しやすい +- 正規表現、有限ステートマシン、文字列操作の複数方針 +- Pythonの文字列はimmutableなので、ループ内での文字列連結は避け、必要ならlistにappendして最後に`"".join()`する + +
+ +
+387. First Unique Character in a String + +- 頻度を先に数えてから再走査する解法はシンプルで堅いが、文字列を2回見る +- 文字が1回しか流れてこない設定では、現在ユニークな文字だけを順序付きで管理する発想が使える +- `OrderedDict`は挿入順を保てるため、重複した文字を削除していけば先頭が最初のユニーク文字になる +- `queue.Queue`はスレッド同期用の機能を持つため、単一スレッドのアルゴリズム用途では`collections.deque`の方が自然 +- `queue.Queue`には`peek`がないので、先頭を見ながら取り除く用途では`deque`の`q[0]`と`popleft()`が使いやすい +- `OrderedDict`の順序管理は、dictに加えてdoubly-linked listを持つことで実現できる +- doubly-linked listでは前後の参照を更新することで、ノード削除をO(1)で行える + +
+ +
+560. Subarray Sum Equals K + +- prefix sumの出現回数をhashmapで管理すると、全区間を列挙するO(n^2)ではなくO(n)で数えられる +- 累積和配列を作って後から処理するより、走査しながら過去のprefix sumを数える方がシンプルに書ける +- 和がKになる区間を列挙する問題に変えると、prefix sum同士の対応関係を理解しやすい +- `defaultdict(int)`を使うと、存在しないprefix sumの出現回数を0として扱えて実装が素直になる +- パフォーマンス不足はデータ量が増えたときに実行時間やメモリ使用量として現れるため、O(n^2)からO(n)への改善は実務でも効く + +
+
+200. Number of Islands + +- DFSでは再帰でも書けるが、探索が深い場合はコールスタックの上限に注意が必要 +- 明示的なstackを使うと再帰を避けられ、深い探索でも扱いやすい +- 訪問済みの印は、gridを書き換える方法と`seen`集合で管理する方法がある +- gridを書き換える場合は、帰りがけではなく行きがけで印をつけると無限ループを防ぎやすい +- gridを書き換えると破壊的になってしまう +- 境界判定や隣接マスの列挙は関数に分けると、グリッド探索の本体が読みやすくなる +- 添字の変数名は`i`, `j`よりも`row`, `col`にすると、行と列の意味が明確になる +- BFSは最短経路、DFSは連結成分・トポロジカルソート・サイクル検出などに応用しやすい +- UnionFindでも解けるが、問題側の操作に合った抽象を用意しないと使う側の負担が大きくなる + - 汎用的なUnionFindは自然数の添字同士をつなぐだけなので、呼び出し側が`row * num_cols + col`への変換や陸地判定を担当することになる + - 問題寄りのAPIを用意すると、呼び出し側が楽に書ける +- メソッドは呼び出し側に強い前提を押しつけず、多少雑に呼ばれても扱える設計が望ましい + +
+ +
+695. Max Area of Island + +- 島の最大面積も、グリッド上の連結成分をDFS/BFSで探索して各成分のサイズを数えればよい +- stackやqueueに同じマスを重複して積みたくない場合、`seen`は「処理済み」ではなく「発見済み」として、stack/queueに入れるタイミングで更新する +- `seen`をpop後に更新する書き方もできるが、その場合はpop直後に処理済みならskipしないと面積を二重に数えてしまう +- UnionFindでも解けるが、各マスは上下左右すべてではなく右と下だけを見れば隣接関係を重複なく結合できる +- どの解法でも全マスを確認する必要があるため、時間計算量はO(mn) + +
+ +
+127. Word Ladder + +- 単語をノード、1文字だけ違う関係を辺と見ればグラフの最短経路問題として扱える +- 全単語ペアを比較して隣接リストを作ると、前処理がO(N^2 L)になって遅い +- `subword`を使って`h*t`のような形から隣接候補を引くと、全ペア比較を避けられる +- 各位置を`a-z`に置き換えて候補を作り、`set`で存在確認する方法もある +- `set`は未訪問の有効単語集合としても使えるため、見つけた単語を削除すれば`seen`を別に持たずに済む +- `discard`は存在しない値を削除しようとしても何もしないが、`remove`は`KeyError`になる +- 双方向BFSでは常に小さいfrontierから広げることで探索量を減らせる + +
+ +
+104. Maximum Depth of Binary Tree + +- 二分木の最大深さは、左右の部分木の最大深さを求めて大きい方に1を足すボトムアップ再帰で自然に書ける +- トップダウン再帰では、現在の深さを引数で渡しながら全ノードをたどり、外側の`max_depth`を更新する +- 内側関数から外側の変数に再代入する場合は`nonlocal`が必要になる + - 外側の変数を参照するだけなら`nonlocal`は不要 + ```python + def outer(): + x = 1 + + def inner(): + return x + + return inner() + ``` + - しかし、同じ内側関数内で代入すると、Pythonはその名前を内側関数のローカル変数とみなす + ```python + def outer(): + x = 1 + + def inner(): + x = x + 1 # UnboundLocalError + return x + + return inner() + ``` + - 外側の変数を更新したい場合は`nonlocal`で明示する + ```python + def outer(): + x = 1 + + def inner(): + nonlocal x + x = x + 1 + return x + + return inner() + ``` + - `max_depth = max(max_depth, depth)`も同じで、`nonlocal`なしだと右辺の`max_depth`まで内側関数のローカル変数扱いになり、代入前参照で`UnboundLocalError`になる +- LeetCodeの指定シグネチャにある引数名は、Pythonではkeyword呼び出しのAPIにもなり得るため、安易に変えない方がよい + +
+ +
+111. Minimum Depth of Binary Tree + +- 最小深さは根から浅い順に見るBFSと相性がよく、最初に見つけた葉の深さをそのまま返せる +- DFSで解く場合は、全探索しながら最小値を更新するトップダウン再帰か、左右の部分木の答えを集約するボトムアップ再帰で考えられる + - トップダウンは、現在の深さや根からの累積情報を引数で子へ配る + - ボトムアップは、書きやすい +- 再帰DFSは木が深いとコールスタック上限に当たる可能性があるため、深い入力があり得るならBFSや明示的なstackも検討する +- `None`だけを判定したい場合は、PEP 8やGoogle Python Style Guideの考え方に沿って`if node.left is None:`のように書く + +
+ +
+617. Merge Two Binary Trees + +- `copy.copy`は浅いコピーなので、親ノードだけが新しくなり、`left`や`right`の子孫ノードは元の木と共有される +- `copy.deepcopy`は深いコピーなので、`left`や`right`からたどれる子孫ノードも再帰的に複製される +- immutableなオブジェクトでは浅いコピーと深いコピーの差はほぼ問題にならないが、listやTreeNodeのように内部に別オブジェクトへの参照を持つ複合オブジェクトでは差が出る + +
+ +
+112. Path Sum + +- 問題自体は再帰で素直に解ける +- パスを返す/数える発展 + - 経路自体を返したい場合は、各ノードで「親ノード」または「そこまでの累積和」を辞書/setに記録しておけば、目的の葉から遡って経路を再構成できる + - 経路の総数や全列挙が必要なら、early returnせずに最後まで探索し、累積和の出現回数を`defaultdict(int)`で集計するのが良い + +
+ +108. Convert Sorted Array to Binary Search Tree + +- ソート済み配列の中央を根にして、左右の半分から部分木を再帰的に作るのが一番素直 +- スライス `nums[:idx]`, `nums[idx+1:]` で書くとシンプルだが、コピーが発生するため空間計算量がO(n log n)になる + + + +
+102. Binary Tree Level Order Traversal + +複数の解法が考えられる。 + +- 帰りがけDFS再帰(部分木の結果をmergeする) + - 左右の部分木のlevel orderを再帰で求めてから、`itertools.zip_longest`で同レベル同士を連結する + - 時間計算量: 偏った木でO(n^2)(各レベルで`left + right`のリスト連結がかさむ) + - 空間計算量: 結果のO(n) + 再帰スタックのO(h) + - 計算量も悪化するので、積極的に選ぶ理由は薄い + +- 行きがけDFS再帰(レベルを引数で配る) + - `level`を引数で渡し、`level_order[level]`に値を直接appendする + - 時間計算量: O(n) + - 空間計算量: 結果のO(n) + 再帰スタックのO(h) + - 短く書けるが、深い木ではPythonの`sys.setrecursionlimit`に当たる/Cスタックを破壊するリスクがある + +- 行きがけDFS反復(明示stack) + - 上を明示スタックに書き直したもの。`deque.pop()`でDFS + - 左から処理したい場合、子は`(node.right, node.left)`の順で積む(後入れ先出しのため) + - 時間計算量: O(n) + - 空間計算量: 結果のO(n) + スタックのO(h) + - 再帰深さの上限を気にせず使え、ログも取りやすい + +- BFS(queue + level_size) + - 各反復で`len(queue)`を先に保存し、その回数だけ`popleft`して現レベルを取り出す + - 時間計算量: O(n) + - 空間計算量: 結果のO(n) + キューの最大幅(最悪O(n)) + - 「レベルごとの処理」が問題の構造そのものと一致するため最も読みやすい + - 最短深さや「最初に条件を満たす階層」を返したい派生問題にも素直に拡張できる + +- 例外処理は「if-elseで両方の場合を書く」より「ifで例外だけ早期returnし、本流のロジックを後ろにフラットに書く」のが読みやすいことがある +- 再帰の上限はPythonでは`sys.setrecursionlimit`が言語処理系側でガードする。Javaのスタックは~1MB、Cは~10MBが目安 +- 再帰は「ある条件のときだけログを出して呼び出し元を追う」のが難しい。ループに直すとデバッグしやすい +- クイックソートで小さい方を再帰、大きい方を末尾再帰で処理するのも、スタック深さ抑制のため + + +
+103. Binary Tree Zigzag Level Order Traversal + +- 102(level order)に「奇数段だけ向きを反転」を足した派生問題と捉えると、102のBFS解にreverseを1行足すだけで済む +- リストを「逆順」に扱う方法は用途で選ぶ + - `xs.reverse()`:破壊的(in-place)。リスト自体を反転して構わない場面で最有力 + - `reversed(xs)`:非破壊の逆順イテレータ。`for`で回すだけならコピー不要で軽い(返り値はイテレータ) + - `list(reversed(xs))` / `xs[::-1]`:非破壊で「反転した新しいリスト」が欲しいとき(どちらも O(n) コピー) + - 手動の`for i in range(len(xs)-1, -1, -1)`:特殊な制御が要るときだけ。可読性は落ちがち + +
+ +
+300. Longest Increasing Subsequence + +- 愚直DPだと計算量 O(n^2) +- 二分探索でも解ける O(n log n) +- セグメント木: 元の DP 漸化式 の `max` を区間 max クエリで O(log n) に高速化。全体 O(n log n) + - 「値の小ささ」は座標圧縮した index の範囲で、「並び順の前後」は `nums` を順に for で回すことで、別々に分担させる + - 1-indexed のセグメント木では「奇数 = 右の子、偶数 = 左の子」となり、区間 max のループ条件が綺麗に書ける +- Binary Indexed Tree (Fenwick Tree): セグメント木の特殊版で、「常に左端からの累積(prefix)」だけを扱うと決めれば配列サイズを半分(n+1)にできる + - `index & -index` で親/子に O(1) で移れる(lowbit)。実装は短いが、prefix 系の操作しかできないのが制約 +- bisect の挙動 + - `bisect_left(a, x)`: `a[:i]` がすべて `< x` となる最小の `i`(既存の `x` の左) + - `bisect_right(a, x)`: `a[:i]` がすべて `<= x` となる最小の `i`(既存の `x` の右) + - `a` に `x` がなければ両者は一致 + +
+ +
+53. Maximum Subarray + +- 「ある位置 `i` を末尾とする部分配列の最大和」を順に求めれば、その全体の最大が答えになる、という発想で O(n) になる +- 同じことを2通りの式で書ける(どちらも O(n)、空間 O(1)) + - prefix sum + - `min_cumulative_sum` の初期値を 0 にすることで、「途中から始める」と「先頭から始める」を統一して扱える + - Kadane版のアルゴリズム + - 直感的には「過去の合計がマイナスなら相続をやめる」 + +
+ +
+62. Unique Paths + +- メモ化再帰で`functools.cache` を使うと再帰関数のメモ化を1行で書ける +- Python のリスト掛け算 + - `[1] * 10` のように immutable な要素を掛け算で並べるのは安全 + - `[[1]] * 10` のように mutable オブジェクトを掛け算で並べると、全要素が同じオブジェクトを指すため一箇所の変更が全体に波及する + - 2次元配列を作るときは `[[1] for _ in range(n)]` のように内包表記で個別生成するのが安全 + - リストはオブジェクトへのポインタを持っており、immutable は再代入で別オブジェクトに差し替わるが mutable はその場で書き換わる、という違いが原因 + +
+ + +
+63. Unique Paths II + +- 配る DPと もらう DP + - もらう DP: `dp[r][c]` を「自分が上 / 左から受け取る値」で計算する(一般的) + - 配る DP: `dp[r][c]` の値を「右と下のマスへ加算する」形で書く +- コンパイラ言語とインタプリタ言語での `if` 分岐コストの違い + - C / Rust などのコンパイラ言語では、分岐予測失敗で命令パイプラインのやり直しが発生するため、`for` の中の `if` はなるべく減らす方が速い + - 「0 行目/0 列目だけの特殊処理」など事前に決まっている分岐は、ループの外に出して別ループで処理した方が、可読性も速度も向上する + - Python はインタプリタ実行で元から大量の分岐命令を踏むので、ここを気にしても差は出にくい(可読性の観点では分けてもよい) +
+ +
+198. House Robber + +- DP配列のメモリは O(n) → O(1) に落とせる +- 変数の意味はコメントで明示すると読みやすい +- メモ化再帰は `@functools.cache` を使うと1行で書ける +- `functools.lru_cache` / `functools.cache` のキャッシュはスレッドセーフ + - 内部でロックを取っているので、複数スレッドから同じ関数を実行してもキャッシュ自体の状態は壊れない +
+ +
+213. House Robber II + +- 円環の制約は「場合分けで消す」ことで直線版に帰着できる + - 思考の型: 「未知のものは何か」「与えられているものは何か」「条件は何か」を整理し、厄介な条件を場合分けで消せないか考える +- DP の用語の整理 + - DP は「部分構造最適性」と「部分問題の重複」を持つ問題に対し、「一度計算した部分問題は保存して使い回す」設計指針の総称 + - 実現方法は2つ + - **Tabulation (タビュレーション)**: ボトムアップ・反復型。DP 配列を小さい方から埋めていく + - **Memoization (メモ化再帰)**: トップダウン・再帰型。必要になった部分問題を再帰で計算しキャッシュする +- スワップを多重代入で書く(再掲) + - `a, b = new_a, new_b` の形にすれば、入れ替え用の一時変数を持たずに同時更新できる +- `functools.reduce` で副作用のない関数型スタイルに置き換えられる + - `reduce(func, iterable, initial)` は他言語でいう `fold_left` と同じで、状態を引数として持ち回り再代入を消せる + - 可読性は落ちる代わり、ループ内の代入による副作用が無くなり、状態遷移を関数として切り出せる +- `itertools.islice` でスライスのコピーを避ける(`sol2.py`) + - 新しいリストを作る場合 O(k) のコピーが発生する + - `itertools.islice(nums, 1, len(nums))` は元のリストをコピーせず遅延評価のイテレータを返すので、メモリも時間も節約できる + - 引数として一度走査するだけで済むようなケースでは、スライスより `islice` が向いている +- list slice の計算量 + - `nums[a:b]` は要素 `b - a` 個ぶんの新しいリストを作るため O(b - a) の時間と空間がかかる + - に基本演算の計算量がまとまっている +
+ +
+121. Best Time to Buy and Sell Stock + +- `itertools.accumulate(prices, min)` で「その日までの最小値」を遅延列として作れる(`sol3.py`) + - Haskell の `scanl1 min` 相当。関数型風に「最小列 → 利益列 → max」と書ける + - + - 同じ問題を「scanl で累積最小、zipWith (-) で利益、max で集約」と分解できる +- import スタイル(Google Python Style Guide) + - `from itertools import islice` のように個別の関数を import するのではなく、`import itertools` してから `itertools.islice(...)` と書く方が好まれる + - > Use import statements for packages and modules only, not for individual types, classes, or functions. +- 型ヒント + - Python 3.9+ では `typing.List[int]` より組み込みの `list[int]` が推奨 + - 引数はより抽象的な型(`Sequence`, `Iterable`)、返り値はより具体的な型(`list[int]`)にすると、呼び出し側の自由度を上げつつ、利用側に確かな情報を返せる +
+ +
+122. Best Time to Buy and Sell Stock II +- 答えは \( \sum_{k=1}^{n-1}\max(0, p_k - p_{k-1}) \) と書ける +
+ +
+139. Word Break + +- `s[start:].startswith(word)` のようなスライスは、毎回新しい文字列オブジェクトを生成する O(n−start) のコピー が発生する。代わりに `s.startswith(word, start)` を使えば追加メモリなしで比較できる。同様に `s[start:end] == word` よりも `s.startswith(word, start)` の方が速い。 +- `str.startswith` は tuple を受け取れる + +```python +s.startswith(("apple", "pen")) # どれかで始まれば True +``` + +- 正規表現的な見方 + - `((apple)|(pen))*` のように正規表現で書ける問題は、`wordDict` を定数とみなせば NFA/DFA 的に O(n) で解ける +- `dataclasses.field(default_factory=...)` + - `list` / `dict` / `set` のような mutable をフィールドのデフォルト値に直接書くと、`@dataclass` は `TypeError` を出す。インスタンスごとに独立した mutable を持たせたい場合は `default_factory` を使う + +
+ +
+322. Coin Change + +- `math.isinf`を用いた判定 +- `min` を取るときの工夫 + - 子の結果を generator で流し、`min(gen, default=-1)` で集約すると、無効値を一切流さずに書ける + - `min` / `max` は `default` 引数で「空イテラブルのときの値」を指定できる + +