Skip to content

Commit df4b6aa

Browse files
committed
Solve 276_paint_fence_medium
1 parent 3dda080 commit df4b6aa

7 files changed

Lines changed: 428 additions & 0 deletions

File tree

276_paint_fence_medium/README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# 問題へのリンク
2+
[Paint Fence - LeetCode](https://leetcode.com/problems/paint-fence/)
3+
4+
# 言語
5+
Python
6+
7+
# 問題の概要
8+
- `n` 本のフェンス(のポスト)があり、`k` 色のペンキを使って塗る
9+
- 3 本以上の隣接するフェンスは同じ色に塗れない
10+
- `n` 本のフェンスを塗る方法の数を求めよ
11+
12+
# 自分の解法: Bottom-Up DP
13+
`n` 本のフェンスを塗る方法の数を`n`についての漸化式で求める。`k`は固定して考える。
14+
15+
`n` 本のフェンスを塗る方法の数を求めるために、以下のように考える。
16+
- `n` 本目と `n-1` 本目のフェンスが同じ色でない場合
17+
- `n` 本目のフェンスは `k-1` 色から選べる。
18+
- `1` 本目から `n-1` 本目のフェンスを塗る方法の数は `count[n-1]` となる。
19+
- `n` 本目と `n-1` 本目のフェンスが同じ色である場合、`n`, `n-1` 本目のフェンスは `k-1` 色から選べる。
20+
- `1` 本目から `n-2` 本目のフェンスを塗る方法の数は `count[n-2]` となる。
21+
22+
23+
したがって、`count[n]``n` 本のフェンスを塗る方法の数とすると、
24+
- `count[n] = (k - 1) * count[n - 1] + (k - 1) * count[n - 2]`
25+
26+
- `count[1] = k`, `count[2] = k * k` (`n = 2` の場合は、どの色を選んでも隣接するフェンスと同じ色にならないため)とを併せて考えると、`count[n]``n`回のループで求まる。
27+
28+
動的計画法の配列は使わず、`count[n-1]``count[n-2]` の値を保持する変数を用いて計算することで空間計算量を`O(1)`に抑える。
29+
30+
- 時間計算量:`O(n)`
31+
- 空間計算量:`O(1)`
32+
33+
34+
## step1
35+
36+
```python
37+
class Solution:
38+
def numWays(self, n: int, k: int) -> int:
39+
if n == 1:
40+
return k
41+
# Suppose k is given.
42+
# Use dynamic programming
43+
# count[n]: number of possibilities using n posts with k different colors
44+
# count[1] = k, count[2] = k*k
45+
# count[n] = (k-1) * count[n-1] + (k-1) * count[n-2], for all n = 3, 4, ...
46+
47+
previous_count = k
48+
count = k * k
49+
for _ in range(3, n + 1):
50+
tmp_count = count
51+
count = (k - 1) * count + (k - 1) * previous_count
52+
previous_count = tmp_count
53+
return count
54+
```
55+
56+
57+
## step2
58+
59+
60+
- `count`という変数名は曖昧すぎたので`total_ways`などに変更した。
61+
- docstringを追加した。
62+
- 本問は線形の二項間漸化式であるため、行列累乗を用いて`O(log n)`時間 で解くこともできる。
63+
- [行列累乗まとめ (競プロ)](https://zenn.dev/shibak3n/articles/f08a8ad67a7d14#fnref-89e9-2)
64+
- ただし、記事でも言及されているように、`O(log n)`という計算量解析には注意が必要。そのまま答えの値を計算するだけで考えると桁数が大きくなるため、足し算や掛け算が定数時間とみなせなくなる(大きい数ほど一度の計算に時間がかかる)可能性がある。ある数で割った余りを求めるなどの処理を行う場合は、`O(log n)`で計算できると考えられる。
65+
66+
67+
## step3
68+
```python
69+
class Solution:
70+
def numWays(self, n: int, k: int) -> int:
71+
if n <= 0:
72+
return 0
73+
elif n == 1:
74+
return k
75+
previous_total_ways = k
76+
total_ways = k * k
77+
for _ in range(3, n + 1):
78+
total_ways, previous_total_ways = (k - 1) * (
79+
total_ways + previous_total_ways
80+
), total_ways
81+
82+
return total_ways
83+
```
84+
いつも1次元DPで、いくつかの変数を逐一、更新していく際には`tmp_foo`という一時変数を使って、元の値を保持しておくことが多かった。が、unpackして更新したほうが、コードが短くなり、可読性も上がるので、今後はできるだけこの書き方を使うことにしたい。ただし、上の例のように少し処理が複雑になると、可読性が下がるので難しいところもある。
85+
86+
## step4 (FB)
87+
88+
# 別解・模範解答
89+
## メモ化再帰(hayashi-ayさんの解答)
90+
- [276. Paint Fence by hayashi-ay · Pull Request #17 · hayashi-ay/leetcode · GitHub](https://github.com/hayashi-ay/leetcode/pull/17/files?short_path=50f7b63#diff-50f7b6331a7f8355594f718601d9bd00080e614a343f52dd67abdc08da922eba)
91+
- メモ化再帰を用いて解く方法。
92+
- `@cache`デコレータを用いて、計算済みの値をキャッシュすることで、再帰的な計算を効率化している。
93+
94+
- 時間計算量:`O(n)`
95+
- 空間計算量:`O(n)`(スタックの深さが最大`n`になるため)
96+
97+
```python
98+
99+
def cache(function):
100+
"""
101+
my implementation of functools.cache
102+
"""
103+
results_cache = {}
104+
105+
def wrapper(*args, **kwargs):
106+
key = (args, tuple(sorted(kwargs)))
107+
if key in results_cache:
108+
return results_cache[key]
109+
result = function(*args, **kwargs)
110+
results_cache[key] = result
111+
return result
112+
113+
return wrapper
114+
115+
116+
class Solution:
117+
@cache
118+
def numWays(self, n: int, k: int) -> int:
119+
if n <= 0:
120+
return 0
121+
if n == 1:
122+
return k
123+
if n == 2:
124+
return k * k
125+
126+
return (k - 1) * (self.numWays(n - 1, k) + self.numWays(n - 2, k))
127+
```
128+
129+
### cacheについて
130+
- `@cache`デコレータも自作してみて、自分なりに調べた内容をまとめて技術記事にした。[Python functools.cacheを自作してみた。キーワード引数のキャッシュの仕様には注意 - Qiita](https://qiita.com/garudakai/items/ca35a0c9b399d875f491)
131+
- LRUキャッシュも実装してみたい
132+
- [LRU Cache - LeetCode](https://leetcode.com/problems/lru-cache/description/)
133+
- [\[競プロ\]\[Python\] LRUキャッシュを実装する \| DevelopersIO](https://dev.classmethod.jp/articles/lru-cache-leetcode/)
134+
- [LRUキャッシュとLFUキャッシュをけっこう丁寧に実装します(Python) - Qiita](https://qiita.com/grouse324/items/8c7c48b17c4fbf246f44)
135+
136+
137+
## 組み合わせの数を分けて数えて、最後に足す方法
138+
つぎの2つのケースに分けて考える。
139+
1. 末尾の二つのフェンスが同じ色の場合
140+
2. 末尾の二つのフェンスが異なる色の場合
141+
142+
それぞれを別に計算して、最後に足す。
143+
144+
```python
145+
class Solution:
146+
def numWays(self, n: int, k: int) -> int:
147+
one_consecutive = k
148+
two_consecutive = 0
149+
150+
for _ in range(n - 1):
151+
new_one_consecutive = (k - 1) * (one_consecutive + two_consecutive)
152+
two_consecutive = one_consecutive
153+
one_consecutive = new_one_consecutive
154+
print(f"{one_consecutive=}, {two_consecutive=}")
155+
return one_consecutive + two_consecutive
156+
```
157+
158+
# 次に解く問題の予告
159+
- [LRU Cache - LeetCode](https://leetcode.com/problems/lru-cache/description/)

276_paint_fence_medium/memo_dp.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# @lc app=leetcode id=276 lang=python3
3+
#
4+
# [276] Paint Fence
5+
#
6+
7+
8+
# @lc code=start
9+
10+
11+
def cache(function):
12+
"""
13+
my implementation of functools.cache
14+
"""
15+
results_cache = {}
16+
17+
def wrapper(*args, **kwargs):
18+
key = (args, tuple(sorted(kwargs)))
19+
if key in results_cache:
20+
return results_cache[key]
21+
result = function(*args, **kwargs)
22+
results_cache[key] = result
23+
return result
24+
25+
return wrapper
26+
27+
28+
class Solution:
29+
@cache
30+
def numWays(self, n: int, k: int) -> int:
31+
if n <= 0:
32+
return 0
33+
if n == 1:
34+
return k
35+
if n == 2:
36+
return k * k
37+
return (k - 1) * (self.numWays(n - 1, k) + self.numWays(n - 2, k))
38+
39+
40+
# @lc code=end

276_paint_fence_medium/my_cache.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# %%
2+
import functools
3+
4+
5+
def cache(function):
6+
results_cache = {}
7+
8+
@functools.wraps(function)
9+
def wrapper(*args, **kwargs):
10+
11+
# The key must be the same, regardless of the order in which the keyword arguments are given.
12+
key = (args, tuple(sorted(kwargs.items())))
13+
print(f"{key=}")
14+
if key in results_cache:
15+
print("cache is used")
16+
return results_cache[key]
17+
result = function(*args, **kwargs)
18+
results_cache[key] = result
19+
return result
20+
21+
return wrapper
22+
23+
24+
# %%
25+
import time
26+
27+
28+
@cache
29+
def my_func(
30+
i: int,
31+
kw_x: str = "keyword X",
32+
kw_y: str = "keyword Y",
33+
) -> str:
34+
return f"{time.time()}, {kw_x=}, {kw_y=}"
35+
36+
37+
# %%
38+
39+
40+
print(my_func(1, kw_x="x", kw_y="y"))
41+
# %%
42+
print(my_func(1, kw_y="y", kw_x="x")) # cache should be used
43+
44+
45+
print(my_func(1))
46+
47+
# %%
48+
print(my_func(1))
49+
50+
# %%
51+
print(my_func(1))
52+
53+
# %%
54+
tp = (1, 2, 3)
55+
tp_ = (4, 5)
56+
tp += tp_
57+
print(tp) # (1, 2, 3, 4, 5)
58+
59+
# %%
60+
import functools
61+
62+
63+
@functools.cache
64+
def myfunc(a=-1, b=-1, c=-1):
65+
print("my func is called")
66+
return 0
67+
68+
69+
# %%
70+
myfunc(a=1, b=2) # my func is called
71+
myfunc(b=2, a=1) # my func is called
72+
73+
# %%
74+
myfunc(a=1, b=2) # nothing
75+
# %%
76+
myfunc(b=1) # myfunc(b=1, a=-1)と同じ扱い
77+
myfunc(a=-1, b=1)
78+
79+
80+
# %%
81+
import timeit
82+
83+
# キーワード引数が5個の場合
84+
kwargs_small = {f"k{i}": i for i in range(5)}
85+
# キーワード引数が20個の場合
86+
kwargs_large = {f"k{i}": i for i in range(20)}
87+
88+
89+
# ソートしない場合 (現在の functools.cache の方式)
90+
def no_sort(kwargs):
91+
return tuple(kwargs.items())
92+
93+
94+
# ソートする場合 (ご自身の提案、および古いPythonの方式)
95+
def do_sort(kwargs):
96+
return tuple(sorted(kwargs.items()))
97+
98+
99+
# 計測
100+
time_no_sort_small = timeit.timeit(lambda: no_sort(kwargs_small), number=1_000_000)
101+
time_do_sort_small = timeit.timeit(lambda: do_sort(kwargs_small), number=1_000_000)
102+
103+
time_no_sort_large = timeit.timeit(lambda: no_sort(kwargs_large), number=1_000_000)
104+
time_do_sort_large = timeit.timeit(lambda: do_sort(kwargs_large), number=1_000_000)
105+
106+
print(f"--- キーワード引数: 5個 (100万回実行) ---")
107+
print(f"ソートなし: {time_no_sort_small:.4f} 秒")
108+
print(f"ソートあり: {time_do_sort_small:.4f} 秒")
109+
print(f"パフォーマンス差: {time_do_sort_small / time_no_sort_small:.2f} 倍遅い")
110+
print("\n")
111+
print(f"--- キーワード引数: 20個 (100万回実行) ---")
112+
print(f"ソートなし: {time_no_sort_large:.4f} 秒")
113+
print(f"ソートあり: {time_do_sort_large:.4f} 秒")
114+
print(f"パフォーマンス差: {time_do_sort_large / time_no_sort_large:.2f} 倍遅い")
115+
116+
# %%
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# @lc app=leetcode id=276 lang=python3
3+
#
4+
# [276] Paint Fence
5+
#
6+
7+
8+
# @lc code=start
9+
class Solution:
10+
def numWays(self, n: int, k: int) -> int:
11+
one_consecutive = k
12+
two_consecutive = 0
13+
14+
for _ in range(n - 1):
15+
new_one_consecutive = (k - 1) * (one_consecutive + two_consecutive)
16+
two_consecutive = one_consecutive
17+
one_consecutive = new_one_consecutive
18+
print(f"{one_consecutive=}, {two_consecutive=}")
19+
return one_consecutive + two_consecutive
20+
21+
22+
# @lc code=end

276_paint_fence_medium/step1.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#
2+
# @lc app=leetcode id=276 lang=python3
3+
#
4+
# [276] Paint Fence
5+
#
6+
7+
8+
# @lc code=start
9+
class Solution:
10+
def numWays(self, n: int, k: int) -> int:
11+
if n == 1:
12+
return k
13+
# Suppose k is given.
14+
# Use dynamic programming
15+
# count[n]: number of possibilities using n posts with k different colors
16+
# count[1] = k, count[2] = k*k
17+
# count[n] = (k-1) * count[n-1] + (k-1) * count[n-2], for all n = 3, 4, ...
18+
19+
previous_count = k
20+
count = k * k
21+
for _ in range(3, n + 1):
22+
tmp_count = count
23+
count = (k - 1) * count + (k - 1) * previous_count
24+
previous_count = tmp_count
25+
return count
26+
27+
28+
# @lc code=end

0 commit comments

Comments
 (0)