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
217 changes: 217 additions & 0 deletions problem30/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
## 取り組み方
- step1: 5分以内に空で書いてAcceptedされるまで解く + テストケースと関連する知識を連想してみる
- step2: 他の方の記録を読んで連想すべき知識や実装を把握した上で、前提を置いた状態で最適な手法を選択し実装する
- step3: 10分以内に1回もエラーを出さずに3回連続で解く
- step4: LRU cacheを実装した上で解く

## step1
### 考えたこと
前と同じ色であることをo、違う色であることをxとしたときに、連続する3つの(i-2, i-1, i)番のフェンスについて考える。
i番目がi-1番目と同じ色で塗りたい時のパターンは

- o - o : これはルール違反
- x - o : i番目にi-1番目と同じ色を使う

i番目がi-1番目と違う色で塗りたい時のパターンは

- o - x : i番目にk-1色使える
- x - x : i番目にk-1色使える

と整理できるので、動的計画法を用いて求められる。
時間計算量は1~n番目まで順番に更新していくので、O(n)。空間計算量も1~n番目用に箱を作るのでO(n)。

動的計画法用の配列名って`dp`で問題ないのだろうか。一旦、`ways_is_same_before`あたりにしておく。
Copy link

@olsen-blue olsen-blue Apr 12, 2025

Choose a reason for hiding this comment

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

動的計画法用の配列名って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.

コメントありがとうございます。
ですよね。

この練習会の感覚だと何が入っているか分かる命名が良いだろうと判断しましたが、
検索してみるとdpと付けている例が多かったので揺らぎました。

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/1217439924333187244/1310104727257874432

上から読んでいくと何が入っているか分からないですね。

Choose a reason for hiding this comment

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

以前、dpという選択肢が浮かんだ際の感情はこんな感じでした。
https://github.com/olsen-blue/Arai60/pull/31/files#diff-b7fbb0dce1473afc0264185268f1a1ef6d682a3a8c997d43bc8bdd636a66ce4aR7

トップダウン型でも解く。

#### ボトムアップ
```python
class Solution:
def numWays(self, n: int, k: int) -> int:
if n == 1:
return k
ways_is_same_before = [{True: 0, False: 0} for _ in range(n)]
ways_is_same_before[1][True] = k
ways_is_same_before[1][False] = k * (k - 1)

def get_total_ways(i: int) -> int:
return ways_is_same_before[i][True] + ways_is_same_before[i][False]

for i in range(2, n):
ways_is_same_before[i][True] = ways_is_same_before[i - 1][False]
ways_is_same_before[i][False] = get_total_ways(i - 1) * (k - 1)

return get_total_ways(n - 1)
```

#### トップダウン
途中で気づいたが、

- 前のフェンスと違う色を選ぶ場合
- 前の柱の塗り方のパターン * (k - 1)
- 前のフェンスと同じ色を選ぶ場合
- 3本連続で同じ色にならない制約から、前々のフェンスと違う色を選ぶ必要があり
- 前々のフェンスの塗り方のパターン * (k - 1)

でも解ける。こちらの方が1次元配列で良く、変数名も短くなって可読性も高そう。

```python
class Solution:
def numWays(self, n: int, k: int) -> int:
index_to_ways = {}

def num_ways_helper(i: int) -> int:
if i == 0:
return k
if i == 1:
return k * k
if i in index_to_ways:
return index_to_ways[i]
index_to_ways[i] = (k - 1) * (num_ways_helper(i - 2) + num_ways_helper(i - 1))
return index_to_ways[i]

return num_ways_helper(n - 1)
```

## step2
### 読んだコード
- https://github.com/hayashi-ay/leetcode/pull/17/files
- https://github.com/TORUS0818/leetcode/pull/32/files
-

### 感想
- 0-indexで考えていたが、1-indexにしても分かりやすくて良さそう
- トップダウンで`@cache`を使う実装が読みやすく、編集もしやすそうな気がする
- 配列にして管理せず、2つ前と1つ前の状態をとっておく方法もあった
- 「nが1の時」といったように`n`を条件につけると、`0-index`としてインデックスを使うときに混同しそうで避けたいかも
- 再帰にしなくてもシンプルに書けそうなので、ボトムアップ型で2つ前、1つ前の状態を保持しながら進んでいく実装方針を選ぶ

```python
class Solution:
def numWays(self, n: int, k: int) -> int:
if n == 1:
return k
if n == 2:
return k * k
prev_prev_num_ways = k
prev_num_ways = k * k
num_ways = 0
loop_count = 1
Copy link

Choose a reason for hiding this comment

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

変数に格納される値を表す編す名を付けるとよいと思いました。 fench_index などはいかがでしょうか?ただし、変数名と実際に格納される値を一致されるため、値を 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.

レビューありがとうございます。
おっしゃる通り、意味のあるループ用の変数を使うべきでした。
以下のようになりますね。

class Solution:
    def numWays(self, n: int, k: int) -> int:
        if n == 1:
            return k
        if n == 2:
            return k * k
        prev_prev_num_ways = k
        prev_num_ways = k * k
        num_ways = 0
        fence_index = 2
        while fence_index < n:
            num_ways = (k - 1) * (prev_prev_num_ways + prev_num_ways)
            prev_prev_num_ways = prev_num_ways
            prev_num_ways = num_ways
            fence_index += 1
        return num_ways

while 2 + loop_count <= n:
Copy link

@olsen-blue olsen-blue Apr 12, 2025

Choose a reason for hiding this comment

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

おそらく時系列順で、数えたカウントを並べているのだと思うのですが、パッと見2という定数が先に来ているのは、見慣れない感覚がありました。
条件式は、if (変数) < (定数): みたいな形で書くのが個人的には好みです。

思考の中心にあるものを先に書いて、それを評価する、という流れが自然言語的にはしっくりくる気がしました。
例えば、天気予報見てるとして、「今日の気温は・・・20度か、もう春用の服で良いかもな。」みたいなイメージですかね。

Choose a reason for hiding this comment

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

これに限らずですが、基本的に、主役を最も優先・主張して書きたい、みたいな気持ちが個人的にはあります。
ichika0615/arai60#17 (comment)

Copy link
Owner Author

Choose a reason for hiding this comment

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

主役を最も優先・主張して書きたい

かなり腑に落ちました。ありがとうございます。
自分は a < bのように右に大きいものがくるといった流れで書きたい癖があるのかなとも思いました。

Choose a reason for hiding this comment

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

右に大きいものがくるといった流れで書きたい

そうですね。これは同意できます。
https://github.com/Mike0121/LeetCode/pull/50/files#diff-43981f79e9645568541fbbe2e5cbe8f3e8f41c1f5c09f3fc70b454be886545caR65
Mike0121/LeetCode#50 (comment)

num_ways = (k - 1) * (prev_prev_num_ways + prev_num_ways)
prev_prev_num_ways = prev_num_ways
prev_num_ways = num_ways
loop_count += 1
return num_ways
```

## step3
#### ボトムアップ

```python
class Solution:
def numWays(self, n: int, k: int) -> int:
"""Using 1-index"""
if n == 1:
return k
prev_prev_ways = k
prev_ways = k * k
ways = k * k

Choose a reason for hiding this comment

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

ここはループ内ですぐに上書きされてしまうので、n == 2 の早期returnを加えた上で削ってもいいかもしれません。

for _ in range(2, n):
ways = (k - 1) * prev_prev_ways + (k - 1) * prev_ways

Choose a reason for hiding this comment

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

少し冗長な気がするので以下のような、書き方でもいいかなと思いました。

(k - 1) * (prev_prev_ways + prev_ways)

Copy link
Owner Author

@Fuminiton Fuminiton Apr 11, 2025

Choose a reason for hiding this comment

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

レビューありがとうございます。
意図としては、あえて式をまとめずに書いたほうが、
「前の色と違う色で塗るパターンと前の色と同じ色で塗るパターンの和を出しているのだ」という意図が伝わりやすいだろうといった具合です。

一方で、ご指摘の通り式をまとめたうえで、式の背景をコメントで残す方が適切な気がしてきました。

prev_prev_ways, prev_ways = prev_ways, ways
return ways
```

#### トップダウン

```python
class Solution:
def numWays(self, n: int, k: int) -> int:
"""Using 1-index"""
@cache
def get_ways(index: int):

Choose a reason for hiding this comment

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

getと表現すると、すでにあるものを取得するみたいなイメージが近いかもしれないと感じました。

if index == 1:
return k
if index == 2:
return k * k
ways = (k - 1) * get_ways(index - 2)
ways += (k - 1) * get_ways(index - 1)
return ways

return get_ways(n)
```

## step4
LRU cacheを実装する。
そもそも、LRU cacheとは、格納できる領域が限られる場合に使えるキャッシュ。

結局、実現したいのは、

1. 関数呼び出しの引数と結果を辞書に保存する
2. 最古の1.が先頭、最新が末尾にくるようなデータ構造を用意する
3. 関数が呼び出されたとき、辞書にアクセスして保存してあれば返す
4. 関数の呼び出しの引数と結果を辞書に保存する
- すでに辞書に保存されている場合は削除
- また、辞書が制限サイズを超えていれば最古のデータを削除
- 辞書に新たに引数と結果を登録する
5. 関数の呼び出しの引数と結果を2.のデータ構造に追加する
- すでに登録されていれば削除
- 制限サイズを超えていれば、先頭のデータを削除
- データを末尾に追加

ということ。
データ構造は、Doubly-Linked List or OrderedDict を使う方針がありそう。
Copy link

Choose a reason for hiding this comment

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

これ、OrderedDict の中身は Doubly-Linked List なので、まあ、練習としては、Doubly-Linked List 自体を書いて欲しいところではありますね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

コメントありがとうございます。

これ、OrderedDict の中身は Doubly-Linked List なので、

おっしゃる通りでした。。
https://github.com/python/cpython/blob/main/Lib/collections/__init__.py#L89

まあ、練習としては、Doubly-Linked List 自体を書いて欲しいところではありますね。

そうですよね。練習します。

前者の方がカスタマイズ性などが優れていそうだが、後者の方がシンプルに書けそうなので、今回は後者で書く。
デコレータにするのは一旦、保留する。

```python
from collections import OrderedDict

class MyLRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity

def get(self, key) -> int | None:
if not key in self.cache:
return None
value = self.cache.pop(key)
self.cahce[key] = value
return value

def put(self, key, value) -> None:
if key in self.cache:
self.cache.pop(key)
if not len(self.cache) < self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value


class Solution:
def numWays(self, n: int, k: int) -> int:
"""Using 1-index"""
cache = MyLRUCache(capacity=1000)

def get_ways(index: int):
if index == 1:
return k
if index == 2:
return k * k
ways = cache.get(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.

PRしてから気づきましたが、ここways = cache.get(index)の間違いです。

if ways is not None:
return ways
ways = (k - 1) * get_ways(index - 2) + (k - 1) * get_ways(index - 1)
cache.put(index, ways)
return ways

return get_ways(n)
```

### 感想
- 時間をかけすぎた感があるのでやらなかったが、後日、doubly-linked listを使った実装とcpythonの実装箇所の読み込みはやる
- 公式ドキュメントのOrderedDictの使用例に、lru_cacheの亜種の実装があって興味深かった

https://docs.python.org/3.13/library/collections.html#collections.OrderedDict
> class LastUpdatedOrderedDict(OrderedDict):
> class TimeBoundedLRU:
> class MultiHitLRUCache: