diff --git a/213. House Robber II/0_213. House Robber II.md b/213. House Robber II/0_213. House Robber II.md new file mode 100644 index 0000000..b210090 --- /dev/null +++ b/213. House Robber II/0_213. House Robber II.md @@ -0,0 +1,328 @@ +# 213. House Robber II +https://leetcode.com/problems/house-robber-ii/description/ + +## STEP1 +- 何も見ずに解いてみる + +#### 考えたこと +- 初めの家から盗む、盗まないによって最後の家から盗めるかが変わってしまう +- 同じ条件でDPができないので、条件を変えて2回DPをする + - 上手に処理をまとめることはできそうだけど、思い浮かばなかった + + +計算量 +- 時間計算量 O(N) DPを2回行うので、2Nステップ N = 100 の時に200ステップ 200ナノ秒 +- 空間計算量 O(N) 実際は2N + +```cpp +#include +#include + +class Solution { +public: + int rob(const std::vector& nums) { + if (nums.empty()) { + return 0; + } + int num_houses = nums.size(); + if (num_houses <= 2) { + return *std::max_element(nums.begin(), nums.end()); + } + + // house_to_gain[i] = i番目までの家までを盗んだ時、得られる利得の最大値 + + // 初めの家から盗む(代わりに最後の家から盗めない) + std::vector house_to_gain(num_houses); + house_to_gain[0] = nums[0]; + house_to_gain[1] = nums[0]; + for (int i = 2; i < num_houses; i++) { + // 最後の家からは盗めない + if (i == num_houses - 1) { + house_to_gain[i] = house_to_gain[i - 1]; + continue; + } + house_to_gain[i] = std::max(house_to_gain[i - 1], + house_to_gain[i - 2] + nums[i]); + } + int candidate_1 = std::max(house_to_gain[num_houses - 1], + house_to_gain[num_houses - 2]); + + // 初めの家から盗まない(代わりに最後の家から盗める) + std::vector house_to_gain_2(num_houses); + house_to_gain_2[0] = 0; + house_to_gain_2[1] = nums[1]; + for (int i = 2; i < num_houses; i++) { + house_to_gain_2[i] = std::max(house_to_gain_2[i - 1], + house_to_gain_2[i - 2] + nums[i]); + } + int candidate_2 = std::max(house_to_gain_2[num_houses - 1], + house_to_gain_2[num_houses - 2]); + + return std::max(candidate_1, candidate_2); + } +}; +``` + + + +## STEP2 +### プルリクやドキュメントを参照 +#### 問題が解けるより他人のコードを読んだりコメントするほうがよっぽど大事 +#### 参照したもの + +- https://github.com/Fuminiton/LeetCode/pull/36/files +- https://github.com/fuga-98/arai60/pull/36/files +- https://github.com/olsen-blue/Arai60/pull/36/files +- + + +- ドキュメント系 + +teachers' eye +- 使用する前に宣言する https://github.com/Fuminiton/LeetCode/pull/36/files#r2067665111 +- 脳内シミュレーションと例外処理 https://github.com/olsen-blue/Arai60/pull/36/files#r1973945286 + + + +#### 感想 +- 2つ同様なものを書かないためのコツは、「初めと終わりを指定すればいい」ということか + - 1からN-1までの配列しか頭になかった。 + +#### STEP1以外の手法と感想 +- 同じ処理をを2つ書くよりは、関数化して処理する方が見やすそう + - ただ、答えを返す関数を書く場合は、2つの場合分けの途中経過が見えなくなるという問題はある + - 途中経過が欲しい場合は、関数を返り値を配列にすれば良い + - ただ、関数の中で配列を作って返す場合、コピーが発生する可能性がある(NRVOは保証はされていない) + - 配列も欲しいなら、素直に2回書いてもいいかもしれない + +- 配列を2つ用意する方法は、さらに2Nステップ追加することになるので、Nが大きいと避けた方がいいか。 + - ステップが増えることを除けば、2つのほぼ同じ処理をすることになり、わかりやすくて好き + - 左右の範囲を指定してDPする方法は、indexの添え字がどこなのか少しわかりにくくなりそうだ、 + - 配列のコピーは作らないので、読みやすさとトレードオフか + +- 前問(House Robber I)と同様に、メモ化再帰をする方法や、DP配列を作らず2変数で管理する方法 +- メモリ制限が厳しいなら2変数が良い。 + +- 総合的な今回の好みは、左右の範囲を指定してDPする方法。 + + +- 2つ配列を作って、共通のDPの関数を作る +```cpp +#include +#include + +class Solution { +public: + int rob(const std::vector& nums) { + if (nums.empty()) { + return 0; + } + int num_houses = nums.size(); + if (num_houses <= 2) { + return *std::max_element(nums.begin(), nums.end()); + } + + std::vector nums_excluding_first(nums.begin() + 1, nums.end()); + std::vector nums_excluding_last(nums.begin(), nums.end() - 1); + + return std::max(max_gain_from_houses(nums_excluding_first), + max_gain_from_houses(nums_excluding_last)); + } +private: + int max_gain_from_houses(const std::vector& nums) { + int num_houses = nums.size(); + std::vector house_to_gain(num_houses); + house_to_gain[0] = nums[0]; + house_to_gain[1] = std::max(nums[0], nums[1]); + for (int i = 2; i < num_houses; i++) { + house_to_gain[i] = std::max(house_to_gain[i - 1], + house_to_gain[i - 2] + nums[i]); + } + return std::max(house_to_gain[num_houses - 1], + house_to_gain[num_houses - 2]); + } +}; +``` + +- 配列は作らずに、DP計算をする範囲を指定する方法 +```cpp +#include +#include + +class Solution { +public: + int rob(const std::vector& nums) { + if (nums.empty()) { + return 0; + } + int num_houses = nums.size(); + if (num_houses <= 2) { + return *std::max_element(nums.begin(), nums.end()); + } + + return std::max(max_gain_in_range(nums, 0, num_houses - 2), + max_gain_in_range(nums, 1, num_houses - 1)); + } +private: + int max_gain_in_range(const std::vector& nums, int start, int end) { + std::vector house_to_gain(nums.size()); + house_to_gain[start] = nums[start]; + house_to_gain[start + 1] = std::max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + house_to_gain[i] = std::max(house_to_gain[i - 1], + house_to_gain[i - 2] + nums[i]); + } + return std::max(house_to_gain[end], + house_to_gain[end - 1]); + } +}; +``` + +- 配列は作らずに、DPの代わりに2変数を管理する方法 +```cpp +#include +#include + +class Solution { +public: + int rob(const std::vector& nums) { + if (nums.empty()) { + return 0; + } + int num_houses = nums.size(); + if (num_houses <= 2) { + return *std::max_element(nums.begin(), nums.end()); + } + + return std::max(max_gain_in_range(nums, 0, num_houses - 2), + max_gain_in_range(nums, 1, num_houses - 1)); + } +private: + int max_gain_in_range(const std::vector& nums, int start, int end) { + int max_two_before = nums[start]; + int max_one_before = std::max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + int temp = max_one_before; + max_one_before = std::max(max_one_before, max_two_before + nums[i]); + max_two_before = temp; + } + return std::max(max_two_before, max_one_before); + } +}; +``` + + +- pythonでメモ化再帰をデコレータを書いている方がいらっしゃるので、自分もトライしてみる + - 高階関数("関数を受け取って関数を返す"関数)なのか、C++には なさそうだ + - 関数を受け取ると、関数の引数も情報として受け取れるのね + - "args"や"kargs"を引数にとれるのが便利だけど、具体的に指定することもできるはず + +```python +class Solution: + def rob(self, nums: list[int]) -> int: + if len(nums) <= 2: + return max(nums, default = 0) + + def max_gain_cache(max_gain): + cache = {} + # cacheに引数のtupleに対応する値があれば返し、ないならその関数の値の辞書のページを作り値を返す + def wrapper(begin ,end): + pair = (begin, end) + if (pair in cache): + return cache[pair] + cache[pair] = max_gain(begin, end) + return cache[pair] + return wrapper + + @max_gain_cache + def max_gain(begin: int, end: int)-> int: + length = end - begin + 1 + if length == 1: + return nums[begin] + if length == 2: + return max(nums[begin], nums[begin + 1]) + return max(max_gain(begin, end - 1), + max_gain(begin, end - 2) + nums[end]) + + return max(max_gain(0, len(nums) - 2), + max_gain(1, len(nums) - 1)) + +``` +- これだと、仮にキーワード引数と通常の引数の指定が入り乱れていても対応できるはず + - ただ、argsやkargsn代わりに、max_gainの引数を具体的に指定すると、max_gainを修飾するためだけのデコレータという感じになってあまり意味がないなと気づいた + + +- これが素直な気がする + +```python +def my_cache(func): + cache = {} + # cacheに引数のtupleに対応する値があれば返し、ないならその関数の値の辞書のページを作り値を返す + def wrapper(*args): + if args in cache: + return cache[args] + cache[args] = func(*args) + return cache[args] + return wrapper + +class Solution: + def rob(self, nums: list[int]) -> int: + if len(nums) <= 1: + return max(nums, default = 0) + + @my_cache + def max_gain(begin: int, end: int)-> int: + length = end - begin + 1 + if length == 1: + return nums[begin] + if length == 2: + return max(nums[begin], nums[begin + 1]) + return max(max_gain(begin, end - 1), + max_gain(begin, end - 2) + nums[end]) + + return max(max_gain(0, len(nums) - 2), + max_gain(1, len(nums) - 1)) +``` + + +## STEP3 +### 3回ミスなく書く +- 配列は作らずに、DP計算をする範囲を指定する方法 + +```cpp +#include +#include + +class Solution { +public: + int rob(const std::vector& nums) { + int num_houses = nums.size(); + if (num_houses == 0) { + return 0; + } + if (num_houses <= 2) { + return *std::max_element(nums.begin(), nums.end()); + } + + return std::max(max_gain_in_range(nums, 0, num_houses - 2), + max_gain_in_range(nums, 1, num_houses - 1)); + } + +private: + int max_gain_in_range(const std::vector& nums, int start, int end) { + std::vector house_to_gain(nums.size()); + house_to_gain[start] = nums[start]; + house_to_gain[start + 1] = std::max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + house_to_gain[i] = std::max(house_to_gain[i - 1], + house_to_gain[i - 2] + nums[i]); + } + return std::max(house_to_gain[end], house_to_gain[end - 1]); + } +}; +``` + +6分,6分,5分で3回Accept + +#### 2周目の宿題 +- lru_cacheを実装する(pythonでも)