Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
start_new_problem.sh
main.go
go.mod
go.sum
*.go
146 changes: 146 additions & 0 deletions 198HouseRobber.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
問題: https://leetcode.com/problems/house-robber/description/

### Step 1
- 紙に書いてアルゴリズムを考える
- 考えたテストケース: [1], [1,1], [1,1,1]
- nums[i]が尻になる配列での最大値 =
max(nums[i-2]が尻になる配列での最大値 + nums[i], nums[i-1]が尻になる配列での最大値)
と思った
- しかし、入力[2,1,1,2]でWA
- 2個飛ばしの場合を想定できていなかった
- よく考えたらテストケースが少なすぎる。手抜きしない
- テストケースを考える時にパターンをすべて網羅できているかよく考えたほうがいい
Copy link

Choose a reason for hiding this comment

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

手下を各々の家の前に立たせて伝言をさせたら、自分の前を盗む場合と盗まない場合の話をすると思うんですよね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

なるほどです。ちなみにodaさんはよく例えを使われますがそれは人に説明をするために実世界の例を作り出されていますか?それとも普段のコーディングのときから何かに例えていらっしゃるんですか?

Copy link

Choose a reason for hiding this comment

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

喩えていないですね。ただ、普段から、もうちょっと抽象的なものに人格を感じているので、あだ名をつけているくらいの感じです。


- 以下でWA
```Go
func rob(nums []int) int {
if len(nums) == 1 {
return nums[0]
}
if len(nums) == 2 {
return max(nums[0], nums[1])
}
maxTwoBefore := nums[0]
maxOneBefore := nums[1]
for i := 2; i < len(nums); i++ {
currentMax := max(maxTwoBefore+nums[i], maxOneBefore)
maxOneBefore, maxTwoBefore = currentMax, maxOneBefore
}
return maxOneBefore
}
```

- 修正後
- 最初の3つの条件分岐をループにまとめる方法もあるが、
個人的には最初に特殊ケースを弾いた方が直感的にわかりやすかった
- n = len(nums)として、時間計算量はO(n), 空間計算量はO(1)
```Go
func rob(nums []int) int {
if len(nums) == 1 {
return nums[0]
}
if len(nums) == 2 {
return max(nums[0], nums[1])
}
if len(nums) == 3 {
return max(nums[0]+nums[2], nums[1])
}
maxThreeBefore := nums[0]
maxTwoBefore := nums[1]
maxOneBefore := max(nums[0]+nums[2], nums[1])
for i := 3; i < len(nums); i++ {
n := nums[i]
currentMax := max(maxThreeBefore+n, maxTwoBefore+n, maxOneBefore)
maxThreeBefore, maxTwoBefore, maxOneBefore = maxTwoBefore, maxOneBefore, currentMax
}
return maxOneBefore
}
```

### Step 2

#### 2a
- step1の修正
- 他の方のコードを見てstep1のように3つ先まで参照する必要はないことに気づいた
- numsの長さ1,2,3の場合もループの中に入れた
- 書いてみたら思ったよりすっきりになった

```Go
func rob(nums []int) int {
twoBefore := 0
oneBefore := 0
for i, n := range nums {
if i == 0 {
oneBefore = n
continue
}
if i == 1 {
twoBefore, oneBefore = oneBefore, max(oneBefore, n)
Copy link

Choose a reason for hiding this comment

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

代入を一気にするのは、少し見にくく感じます
https://discord.com/channels/1084280443945353267/1195700948786491403/1195724750895452220

Copy link
Owner Author

Choose a reason for hiding this comment

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

tmpTwoBefore := twoBefore

みたいに値を一時的に記憶するための変数が必要になると勘違いしていました。単に以下のように2行に分けて全く問題なかったですね。ありがとうございます

twoBefore = oneBefore
oneBefore = max(oneBefore, n)

continue
}
currentMax := max(twoBefore+n, oneBefore)
twoBefore, oneBefore = oneBefore, currentMax
Copy link

Choose a reason for hiding this comment

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

maxtwoBeforeや、twoBeforeなどの変数名から、「2つ前を最後に取った場合の最大値」「もし配列が2つ前までだった場合の最大値」などの細かい違いを読み取るのは難しいですね。
ただ、この辺ちゃんと説明しようと思うと長くないそうで難しいです。
コメントに書くか、もしくはStep3のrobbedLastなどの変数名は意味的にわかりやすく感じます。

}
return oneBefore
}
```

#### 2b
- メモ付き再帰
- 時間・空間計算量はともにO(n)

Choose a reason for hiding this comment

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

たまに再帰上限について呟いておくといいかもしれません。

- ??メモがないとO(2^n)時間になる??
Copy link

Choose a reason for hiding this comment

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

フィボナッチになりそうですね。黄金比の n 乗なので 1.6^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.

すみません、ピンと来てないです、、

Copy link

Choose a reason for hiding this comment

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

memo がないとしましょう。

robHelper(100) と呼ぶと、robHelper(99) と robHelper(98) が呼ばれて、再帰的に木ができあがりますね。一番下の葉は robHelper(0) と robHelper(1) です。

robHelper(0) と robHelper(1) は、かかった計算コストの請求書を呼び出した人たちに提出します。呼び出した人たちは請求書をホチキスで止めて呼び出し元に送ります。

最後、請求書の束ができあがりますね。請求書は何枚あって、ホチキスは何回止められましたか。

Choose a reason for hiding this comment

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

もう調べられているかもしれませんが、念の為。
https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%9C%E3%83%8A%E3%83%83%E3%83%81%E6%95%B0#cite_note-algo-3

1.6というのは、上のφのことですね。
フィボナッチ数列の一般項は、Fn = Fn-1 + Fn-2という漸化式を解くと出てきます。
(φ, 1-φを解に持つ特性方程式x^2 = x + 1を考えて、Fn-φFn-1 = (1-φ)(Fn-1-φFn-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.

すみません、返信遅くなりました

@oda
robHelper(n)における請求書の数をbill(n)という関数で表すと、bill(100) = bill(99) + bill(98) になり、bill(1) = bill(0) = 1 より、フィボナッチ数列からbill(100) ≒ 1.6^100 となるということですね。
ホチキスについてもほぼ同様でstapler(n) = stapler(n-1) + stapler(n-2), stapler(0) = stapler(1) = 0, stapler(2) = stapler(3) = 1 よりstapler(100) ≒ 1.6^(100-2) となります
なのでrobHelper関数が1.6^n回呼ばれ、return max(twoBefore+nums[tailIndex], oneBefore)が1.6^(n-2)回計算されるということですね

図示した時に二分木に見えることに囚われてしまい、bill(100) = bill(99) + bill(98)に気づくのに時間がかかりました

@TORUS0818
数学の解説ありがとうございます!

Copy link

Choose a reason for hiding this comment

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

はい。stapler(3)は2でしょうか。まあ、ホチキスの数は、請求書-1ですね。

- ↑ここ自信ないです!!
- 紙に書いていくと高さ2nの二分木が出来上がりそうな感じになった

```Go
func rob(nums []int) int {
memo := make(map[int]int, len(nums))

var robHelper func(tailIndex int) int
robHelper = func(tailIndex int) int {
if v, found := memo[tailIndex]; found {
return v
}
if tailIndex == 0 {
return nums[0]
}
if tailIndex == 1 {
return max(nums[0], nums[1])
}
twoBefore := robHelper(tailIndex - 2)
memo[tailIndex-2] = twoBefore
oneBefore := robHelper(tailIndex - 1)
memo[tailIndex-1] = oneBefore
return max(twoBefore+nums[tailIndex], oneBefore)
}

return robHelper(len(nums) - 1)
}
```

#### 2c
- 末尾要素を盗んだか盗まなかったかの2パターンの値を保持する方法
- 参考: https://github.com/TORUS0818/leetcode/pull/37/files#diff-83c62c4ad09009d4bd113ed1c717a697821ba57360e7b4fc2cfcdc8848add9a3R139

```Go
func rob(nums []int) int {
skippedLast := 0
robbedLast := nums[0]
for i := 1; i < len(nums); i++ {
skippedLast, robbedLast = max(skippedLast, robbedLast), skippedLast+nums[i]
}
return max(skippedLast, robbedLast)
}
```

### Step 3
```Go
func rob(nums []int) int {
skippedLast := 0
robbedLast := nums[0]
for i := 1; i < len(nums); i++ {
skippedLast, robbedLast = max(skippedLast, robbedLast), skippedLast+nums[i]
}
return max(skippedLast, robbedLast)
}
```