Skip to content
Open
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
97 changes: 97 additions & 0 deletions 198. House Robber.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
### Step1

- 大学受験の漸化式っぽい問題

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

分かります。まあDP全般そうですよね〜。

- 配列やresult_so_farあたりの命名は迷った

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resultだとあまり意味を含んでいないので他の命名が良いかなと思いました。max_amount_so_farとか、のようにresultの部分をもう少しカラフルにすると良いかなと思います。


```python

class Solution:
IS_ROBBING = 1
NOT_ROBBING = 0

def rob(self, nums: List[int]) -> int:
max_amount_can_rob = [[0] * 2 for _ in range(len(nums) + 1)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分なら二次元配列にしないで、インデックスで表される家に盗みに入るのと入らないのと2つの配列を作るかなと思いました。

Copy link

@thonda28 thonda28 Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_amount_can_rob という変数名だと「max amount(主語)は奪うことができる」という風に読めるため、個人的には違和感がありました。(can が使われていると bool をイメージします)
max_robbed_amountmax_robbery_amount くらいで、can の要素を明示的に入れなくても伝わりそうです。

また2次元配列の部分ですが、クラス変数を使って index を扱うのは(少なくとも僕は)見たことがないです。初見で何をしているかがわかりづらかったので、避けたほうがよさそうです。hayashi-ay さんが書いているように2つに分けるか、クラスを定義してあげるとわかりやすくなりそうです。(クラス定義はやり過ぎ感があるかもですが)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max_amount_can_rob という変数名だと「max amount(主語)は奪うことができる」という風に読めるため、個人的には違和感がありました。(can が使われていると bool をイメージします) max_robbed_amountmax_robbery_amount くらいで、can の要素を明示的に入れなくても伝わりそうです。

なるほど、確かにそう読めますね。
なかなかまだ読む人の心のモデルを構築できないです(英語力の問題もあるかも)

また2次元配列の部分ですが、クラス変数を使って index を扱うのは(少なくとも僕は)見たことがないです。初見で何をしているかがわかりづらかったので、避けたほうがよさそうです。hayashi-ay さんが書いているように2つに分けるか、クラスを定義してあげるとわかりやすくなりそうです。(クラス定義はやり過ぎ感があるかもですが)

なるほど、「見たことない」のはかなり判断の基準になります。ありがとうございます。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

配列に違和感を感じる理由を考えていたんですけど、順序に意味がないからなのかなと思いました。
例えば今回の場合numsは泥棒に入ろうとしている順に家のお金が並んでいますが、max_amount_can_robの要素の配列の中身の順序は、特に意味がないのかなと思います。

もっと極端な例を考えると、順序のない状態をn個考えて、それらを配列で管理すると、配列のインデックスとの対応が任意(書く人の決めの問題)なので、かなり分かりにくくなるんじゃないでしょうか。
要は、マジックナンバーを避けるために変数を定義するくらいなら、最初から辞書やデータクラスでキーアクセスする方が自然なんじゃないかなと。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ納得感がありすっきりしました。ありがとうございます。
0, 1のDPというのはAtcoderとかだとテクニックとしてあるんですが、実用コード書く時は少し慎重にしてもいいかもですね。

result_so_far = 0
for passed_house_count in range(1, len(nums) + 1):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passed_house_count は変数名の長さに対してわかりやすくはなっていない(むしろ読みにくくなっている)ように思いました。「通過した軒数」≒「何番目の軒数」と言えそうなので、個人的には i で十分な気がします。(for ループの時点で nums を順番に見ていくことはわかりそうです)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
そうですねえ、ただ for i in range(nums) は、問題文の意味からちょっとわかりにくい気が個人的にしてます。
LeetCode側の問題ですが、numsが良くない気がします(house_to_money_amountとかならiでわかる気がします。

max_amount_can_rob[passed_house_count][self.IS_ROBBING] = (
max_amount_can_rob[passed_house_count - 1][self.NOT_ROBBING]
+ nums[passed_house_count - 1]
)
max_amount_can_rob[passed_house_count][self.NOT_ROBBING] = result_so_far
result_so_far = max(
max_amount_can_rob[passed_house_count][self.IS_ROBBING],
max_amount_can_rob[passed_house_count][self.NOT_ROBBING],
)
return result_so_far
```

### Step2

- よく考えたら、いくつ家を通過したかを表すindexはいらない(こういうのつけちゃう癖がある)
- 前の状態をたくさん持ってる必要がある時以外はいらないということか
- ただ、IS_ROBBINGの変数が何を表しているかわからなくなるかも(一応コメントをつけた)
- max_amount_can_rob[self.RECENT_PASSED]の値が、for文の1回目で出てくる時と2回め以降で意味が違ってくる(しかも1行目でそれ使って代入してる)のはわかりにくい?

```python

class Solution:
# 最新の家から盗んだかどうか
IS_ROBBING = 1
RECENT_PASSED = 0

def rob(self, nums: List[int]) -> int:
max_amount_can_rob = [0] * 2
result_so_far = 0
for house_count in range(1, len(nums) + 1):
max_amount_can_rob[self.IS_ROBBING] = (
max_amount_can_rob[self.RECENT_PASSED] + nums[house_count - 1]
)
max_amount_can_rob[self.RECENT_PASSED] = result_so_far
result_so_far = max(
max_amount_can_rob[self.IS_ROBBING],
max_amount_can_rob[self.RECENT_PASSED],
)
return result_so_far
```

- https://github.com/fhiyo/leetcode/pull/36/files
- 結構読み解くのに時間かかった。思考回路が多分自分と違った。
- 多分、前2つのうちどちらが最後に盗んだかで分けよう!というのが思考回路として最初に出てる?ので、今の家で盗んだか盗んでないかで分けようというのが自分と思考回路が違ったのかな
- フィボナッチ数列のちょっと変えたやつみたいな(説明が下手)
- max_money_so_farの、[1]と[0]がそれぞれ何を表しているのかわからない、というのはあるかも
- https://github.com/seal-azarashi/leetcode/pull/33
- twoBackとあるので、やはり最後に盗んだのが2つ前か1つ前かという思考らしい
- どうせ保存する値は2つなので、わざわざ配列にする必要ないのは確かに
- numsが空の場合の例外処理をちゃんと考えてなかったが、上の自分の実装の場合は結果的に0になる(書かなくて良いにしてもちゃんと考えた方が良かった)
- https://github.com/Ryotaro25/leetcode_first60/pull/36
- https://github.com/rossy0213/leetcode/pull/20/files
- dpの長さをlen(nums) + 2になるのはマジックナンバー感がありあまり好みでない
- https://github.com/YukiMichishita/LeetCode/pull/16
- 今を含むかで場合わけしており、自分と思考が近め
- https://github.com/shining-ai/leetcode/pull/35
- currentではなくmax_amount_1_beforeを返すのはあんまり納得いかないかも
- maxのdefaultは勉強になったが、あまり好まないかも
- https://github.com/hayashi-ay/leetcode/pull/48/files
- 結構前2つのmaxという発想の人は多いみたい
- https://github.com/Yoshiki-Iwasa/Arai60/pull/50/files
- rob_3が自分の思考回路と近そうだが、命名がしっくりこなかった(コメントがあったのでスムーズに読めはした)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yoshiki-Iwasa/Arai60#50 (comment)

このodaさんのコメント、自分はしっくり来ました。
あとから他のDPを解き直すときもリアルな情景をイメージすると自然に解法までたどり着く印象があります

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。
そうですね。確かに、リアルな状況を思い浮かべると、変数名も付けやすい気がしてます。(自分も変数名なかなかうまくつけれてないですが…)


## Step3

- DPらしさが跡形もなくなったが、以下のようになった
- 自然な気がする
- 長さが0, 1の場合の例外処理がいらないのも好み

```python

class Solution:
def rob(self, nums: List[int]) -> int:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なんか変数名が全体的にふわっとしていて読むのが難しかったです。たぶんrecentlyというワードチョイスも良くないと思います、なにを指しているのかが曖昧なので、結局コードを全部読む必要がありました。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。recentlyは実態と違っており良くなかったと今改めて見て思いました。lastの方が適切でした。
あと2つはmax_amount_so_farと、max_amount_prevですかねえ。max_amount_prevが特に、これでわかりやすいかが迷います。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipped_previous, robbed_previous とかどうでしょうか。

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[1]を2変数で書き直すと[2]になります。@nittocoさんの方法と同じだと思います。多重代入を使わないと一時変数が必要になります。[3]は変数の意味が、元の@nittocoさんの方法とは少し異なります。
[1]

class Solution {
public:
    int rob(vector<int>& nums) {
        if (nums.size() == 1) {
            return nums[0];
        }
        nums[1] = max(nums[0], nums[1]);
        for (int i = 2; i < nums.size(); ++i) {
            nums[i] = max(nums[i - 1], nums[i] + nums[i - 2]);
        }
        return nums.back();
    }
};

[2]

class Solution {
public:
    int rob(vector<int>& nums) {
        int max_amount = 0, previous_max_amount = 0;
        for (int num : nums) {
            int next_previous_max_amount = max_amount;
            max_amount = max(max_amount, previous_max_amount + num),
            previous_max_amount = next_previous_max_amount;
        }
        return max_amount;
    }
};

[3]

class Solution {
public:
    int rob(vector<int>& nums) {
        int skipped_previous = 0;
        int robbed_previous = 0;
        for (int num : nums) {
            int next_skipped_previous = max(skipped_previous, robbed_previous);
            robbed_previous = skipped_previous + num;
            skipped_previous = next_skipped_previous;
        }
        return max(skipped_previous, robbed_previous);
    }
};

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

皆さんありがとうございます。
そうですよね、skipped_previous, robbed_previous という変数名だと、Step3で書いたコードだと違ってくるかなと思いました(Step1を2変数にしたのに近い)
max_amount, previous_max_amount, skipped_previousあたりですかね(難しい)

prev_result = 0
result_so_far = 0
recently_passed = 0
for money_amount_in_house in nums:
result_so_far = max(prev_result, recently_passed + money_amount_in_house)
recently_passed = prev_result
prev_result = result_so_far
return result_so_far
```