diff --git a/213/memo.md b/213/memo.md new file mode 100644 index 0000000..546defa --- /dev/null +++ b/213/memo.md @@ -0,0 +1,73 @@ +# 213. House Robber II + +- sol1.py: 全問と同じ操作を2回やれば答えが出るというパズルを解けたので満足。同じ考えを使う問題を見たことある気がする。 + +https://github.com/TakayaShirai/leetcode_practice/pull/35#pullrequestreview-3943529697 + +> ちょっととんち扱いになっているのよくないと思うので、私の考え方を書きますね。 + +> 極端な話、まず、2^n 通り列挙して、そこから条件を満たしているものだけ取り出して、それぞれ和を取って最大値を取ればいけますよね。この 2^n 枚の紙をバインダーに挟んで、このパターンで盗んだときに、いくらになるかを計算し、最大にならない紙を捨てて、一番大きい紙を見つける方法でやってみましょう。 + +> 各家の前に、泥棒が立って、バインダーが回ってきます。2^n 枚の紙の自分の担当の情報を埋めていくわけです。 + +> 泥棒文句言うと思うんですね。ほとんどの紙は条件を満たしていません。先に捨てておけよ。 +> あと、最大になりようがないのも回ってきますね。自分は10人目なのにまだ1件も入っていない。先に捨てておけよ。 + +> え、でも全部捨てたら怒られますよね。じゃあ、何は捨てたらだめですか。 + +> これすると同じようなことが書かれている紙だけが残ります。 +> これ、1件目の家で盗んだか盗んでいないか、一つ前の家で盗んだか盗んでいないか、それぞれの最大の4種類だけですね。 +> 「一つ前の家で盗んだか盗んでいないか」は、自分が i 軒目の家の前にいる泥棒になったとき、自分の担当として埋めるか、埋めずに隣へ回すか、という感じで想像できました。 +> 「1軒目の家で盗んだか盗んでいないか」は、最後の家の前にいる泥棒になったとき、1軒目で盗んでいるなら自分は盗めないな=自分の担当の金額は埋められないな、と思うだろう、という感じでしょうか。 + +https://github.com/TakayaShirai/leetcode_practice/pull/35/changes +> 言われればそうだが、どうやって思いつくかの思考回路が知りたかったので、Claude に聞いてみた。 +> 1. まず円環の厄介さを特定する: 直線の House Robber は解ける。円環で何が変わるかというと、「最初と最後が隣り合っている」という制約が1つ増えただけ。 +> 2. その制約を消せないか考える: 厄介な制約が1つだけなら、場合分けで消せることが多い。最初の家を「盗む」か「盗まない」かで分ければ、最後の家との関係が確定する。 +> 3. 場合分けしたら既知の問題に帰着するか確認する: どちらの場合も、最初と最後のつながりが消えて直線になる。直線版はもう解けるので、それを2回使えば終わり。 +> 問題として、未知のものは、「最大の盗める量」、与えられているものは、「それぞれの家の盗める量」と、House Robber でも House Robber II でも、これらは変わらない。 +> 異なるのは、条件の「最初と最後が隣り合っている」という部分。これを取り除けば、全く同じ問題に帰着するから、「この条件をどうやって取り除けば良いか」を考えるのが肝だった。 +> 最近意識するのを忘れていたが、「未知のものは何か」、「与えられているものは何か」、「条件は何か」を意識するのは、どんな問題を解く上でもやはり重要。 + +「未知のものは何か」、「与えられているものは何か」、「条件は何か」を意識する。条件を取り除く方法を考える。 + + +https://github.com/mamo3gr/arai60/blob/213_house-robber-ii/213_house-robber-ii/memo.md + +tabulationという言葉を知らなかったので、整理 + +```markdown +- DP(動的計画法)とは? + +DPは、以下の2つの特徴を持つ問題を解くためのアルゴリズムの設計指針です。 + +部分構造最適性: 大きな問題の答えが、小さな問題の答えを組み合わせて作れる。 + +部分問題の重複: 同じ計算が何度も出てくる。 + +この「一度計算した小さな問題を二度と計算しないように保存しておく」という戦略全体をDPと呼びます。 + +- DPを実現する方法は、大きく分けて2つあります。 + - Tabulation (タビュレーション) ボトムアップ DPを実現する「反復型」の手法 + - Memoization (メモ化再帰) トップダウン DPを実現する「再帰型」の手法 +``` +sol1.pyはtabulationの解法である(DPテーブルを省略している) + +https://github.com/mamo3gr/arai60/blob/213_house-robber-ii/213_house-robber-ii/step1.py +メモ化再帰の方法 + +https://github.com/mamo3gr/arai60/blob/213_house-robber-ii/213_house-robber-ii/step3.py +同じ解法でもインデックスだけ持つ方が効率が良いのでこれを採用しよう。 + +list sliceの効率 +https://wiki.python.org/moin/TimeComplexity + +https://github.com/naoto-iwase/leetcode/pull/41 +多重代入でswap変数を避ける。これをさらにfunctools.reduceを使って関数型に置き換えている。可読性は下がるが副作用は避けられる。 +https://docs.python.org/ja/3.13/library/functools.html#functools.reduce +なるほど、fold_leftと同じ関数のようだ。Pythonにも関数言語チックなこんな関数があったとは知らなかった。 +itertools.isliceを使う点も勉強になる +https://docs.python.org/ja/3/library/itertools.html#itertools.islice +等価なプログラムでyieldが使われているように、配列のコピーを作成しないので、オーバーヘッドは発生しないのだろう +真似して書いてみる sol2.py + diff --git a/213/sol1.py b/213/sol1.py new file mode 100644 index 0000000..26e45f3 --- /dev/null +++ b/213/sol1.py @@ -0,0 +1,21 @@ +class Solution: + def rob(self, nums: List[int]) -> int: + if len(nums) < 2: + return max(nums) + + def rob_without_circle(nums_without_circle): + max_with_last = nums_without_circle[0] + max_without_last = 0 + + for i in range(1, len(nums_without_circle)): + next_max_without_last = max_with_last + max_with_last = max( + max_with_last, max_without_last + nums_without_circle[i] + ) + max_without_last = next_max_without_last + + return max(max_with_last, max_without_last) + + max_value_without_first = rob_without_circle(nums[1:]) + max_value_without_last = rob_without_circle(nums[:-1]) + return max(max_value_without_first, max_value_without_last) diff --git a/213/sol2.py b/213/sol2.py new file mode 100644 index 0000000..aec9e4c --- /dev/null +++ b/213/sol2.py @@ -0,0 +1,22 @@ +import functools +import itertools + + +class Solution: + def rob(self, nums: List[int]) -> int: + if len(nums) < 2: + return max(nums) + + def rob_linearly(sequence): + def visit_next(state: tuple[int, int], money: int) -> tuple[int, int]: + max_with_last, max_without_last = state + next_max_without_last = max_with_last + max_with_last = max(max_with_last, max_without_last + money) + return max_with_last, next_max_without_last + + return max(functools.reduce(visit_next, sequence, (0, 0))) + + return max( + rob_linearly(itertools.islice(nums, 0, len(nums) - 1)), + rob_linearly(itertools.islice(nums, 1, len(nums))), + )