From df4b6aaa4e5bb78ad5ea20f81247331d2c32f940 Mon Sep 17 00:00:00 2001 From: Kaichi-Irie Date: Thu, 26 Jun 2025 18:16:48 +0900 Subject: [PATCH] Solve 276_paint_fence_medium --- 276_paint_fence_medium/README.md | 159 ++++++++++++++++++ 276_paint_fence_medium/memo_dp.py | 40 +++++ 276_paint_fence_medium/my_cache.py | 116 +++++++++++++ .../separate_color_pattern.py | 22 +++ 276_paint_fence_medium/step1.py | 28 +++ 276_paint_fence_medium/step2.py | 38 +++++ 276_paint_fence_medium/step3.py | 25 +++ 7 files changed, 428 insertions(+) create mode 100644 276_paint_fence_medium/README.md create mode 100644 276_paint_fence_medium/memo_dp.py create mode 100644 276_paint_fence_medium/my_cache.py create mode 100644 276_paint_fence_medium/separate_color_pattern.py create mode 100644 276_paint_fence_medium/step1.py create mode 100644 276_paint_fence_medium/step2.py create mode 100644 276_paint_fence_medium/step3.py diff --git a/276_paint_fence_medium/README.md b/276_paint_fence_medium/README.md new file mode 100644 index 0000000..d6c42af --- /dev/null +++ b/276_paint_fence_medium/README.md @@ -0,0 +1,159 @@ +# 問題へのリンク +[Paint Fence - LeetCode](https://leetcode.com/problems/paint-fence/) + +# 言語 +Python + +# 問題の概要 +- `n` 本のフェンス(のポスト)があり、`k` 色のペンキを使って塗る +- 3 本以上の隣接するフェンスは同じ色に塗れない +- `n` 本のフェンスを塗る方法の数を求めよ + +# 自分の解法: Bottom-Up DP +`n` 本のフェンスを塗る方法の数を`n`についての漸化式で求める。`k`は固定して考える。 + +`n` 本のフェンスを塗る方法の数を求めるために、以下のように考える。 +- `n` 本目と `n-1` 本目のフェンスが同じ色でない場合 + - `n` 本目のフェンスは `k-1` 色から選べる。 + - `1` 本目から `n-1` 本目のフェンスを塗る方法の数は `count[n-1]` となる。 +- `n` 本目と `n-1` 本目のフェンスが同じ色である場合、`n`, `n-1` 本目のフェンスは `k-1` 色から選べる。 + - `1` 本目から `n-2` 本目のフェンスを塗る方法の数は `count[n-2]` となる。 + + +したがって、`count[n]` を `n` 本のフェンスを塗る方法の数とすると、 +- `count[n] = (k - 1) * count[n - 1] + (k - 1) * count[n - 2]` + +- `count[1] = k`, `count[2] = k * k` (`n = 2` の場合は、どの色を選んでも隣接するフェンスと同じ色にならないため)とを併せて考えると、`count[n]` が`n`回のループで求まる。 + +動的計画法の配列は使わず、`count[n-1]` と `count[n-2]` の値を保持する変数を用いて計算することで空間計算量を`O(1)`に抑える。 + +- 時間計算量:`O(n)` +- 空間計算量:`O(1)` + + +## step1 + +```python +class Solution: + def numWays(self, n: int, k: int) -> int: + if n == 1: + return k + # Suppose k is given. + # Use dynamic programming + # count[n]: number of possibilities using n posts with k different colors + # count[1] = k, count[2] = k*k + # count[n] = (k-1) * count[n-1] + (k-1) * count[n-2], for all n = 3, 4, ... + + previous_count = k + count = k * k + for _ in range(3, n + 1): + tmp_count = count + count = (k - 1) * count + (k - 1) * previous_count + previous_count = tmp_count + return count +``` + + +## step2 + + +- `count`という変数名は曖昧すぎたので`total_ways`などに変更した。 +- docstringを追加した。 +- 本問は線形の二項間漸化式であるため、行列累乗を用いて`O(log n)`時間 で解くこともできる。 + - [行列累乗まとめ (競プロ)](https://zenn.dev/shibak3n/articles/f08a8ad67a7d14#fnref-89e9-2) + - ただし、記事でも言及されているように、`O(log n)`という計算量解析には注意が必要。そのまま答えの値を計算するだけで考えると桁数が大きくなるため、足し算や掛け算が定数時間とみなせなくなる(大きい数ほど一度の計算に時間がかかる)可能性がある。ある数で割った余りを求めるなどの処理を行う場合は、`O(log n)`で計算できると考えられる。 + + +## step3 +```python +class Solution: + def numWays(self, n: int, k: int) -> int: + if n <= 0: + return 0 + elif n == 1: + return k + previous_total_ways = k + total_ways = k * k + for _ in range(3, n + 1): + total_ways, previous_total_ways = (k - 1) * ( + total_ways + previous_total_ways + ), total_ways + + return total_ways +``` +いつも1次元DPで、いくつかの変数を逐一、更新していく際には`tmp_foo`という一時変数を使って、元の値を保持しておくことが多かった。が、unpackして更新したほうが、コードが短くなり、可読性も上がるので、今後はできるだけこの書き方を使うことにしたい。ただし、上の例のように少し処理が複雑になると、可読性が下がるので難しいところもある。 + +## step4 (FB) + +# 別解・模範解答 +## メモ化再帰(hayashi-ayさんの解答) +- [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) +- メモ化再帰を用いて解く方法。 + - `@cache`デコレータを用いて、計算済みの値をキャッシュすることで、再帰的な計算を効率化している。 + +- 時間計算量:`O(n)` +- 空間計算量:`O(n)`(スタックの深さが最大`n`になるため) + +```python + +def cache(function): + """ + my implementation of functools.cache + """ + results_cache = {} + + def wrapper(*args, **kwargs): + key = (args, tuple(sorted(kwargs))) + if key in results_cache: + return results_cache[key] + result = function(*args, **kwargs) + results_cache[key] = result + return result + + return wrapper + + +class Solution: + @cache + def numWays(self, n: int, k: int) -> int: + if n <= 0: + return 0 + if n == 1: + return k + if n == 2: + return k * k + + return (k - 1) * (self.numWays(n - 1, k) + self.numWays(n - 2, k)) +``` + +### cacheについて +- `@cache`デコレータも自作してみて、自分なりに調べた内容をまとめて技術記事にした。[Python functools.cacheを自作してみた。キーワード引数のキャッシュの仕様には注意 - Qiita](https://qiita.com/garudakai/items/ca35a0c9b399d875f491) +- LRUキャッシュも実装してみたい + - [LRU Cache - LeetCode](https://leetcode.com/problems/lru-cache/description/) + - [\[競プロ\]\[Python\] LRUキャッシュを実装する \| DevelopersIO](https://dev.classmethod.jp/articles/lru-cache-leetcode/) + - [LRUキャッシュとLFUキャッシュをけっこう丁寧に実装します(Python) - Qiita](https://qiita.com/grouse324/items/8c7c48b17c4fbf246f44) + + +## 組み合わせの数を分けて数えて、最後に足す方法 +つぎの2つのケースに分けて考える。 +1. 末尾の二つのフェンスが同じ色の場合 +2. 末尾の二つのフェンスが異なる色の場合 + +それぞれを別に計算して、最後に足す。 + +```python +class Solution: + def numWays(self, n: int, k: int) -> int: + one_consecutive = k + two_consecutive = 0 + + for _ in range(n - 1): + new_one_consecutive = (k - 1) * (one_consecutive + two_consecutive) + two_consecutive = one_consecutive + one_consecutive = new_one_consecutive + print(f"{one_consecutive=}, {two_consecutive=}") + return one_consecutive + two_consecutive +``` + +# 次に解く問題の予告 +- [LRU Cache - LeetCode](https://leetcode.com/problems/lru-cache/description/) diff --git a/276_paint_fence_medium/memo_dp.py b/276_paint_fence_medium/memo_dp.py new file mode 100644 index 0000000..e33eae6 --- /dev/null +++ b/276_paint_fence_medium/memo_dp.py @@ -0,0 +1,40 @@ +# +# @lc app=leetcode id=276 lang=python3 +# +# [276] Paint Fence +# + + +# @lc code=start + + +def cache(function): + """ + my implementation of functools.cache + """ + results_cache = {} + + def wrapper(*args, **kwargs): + key = (args, tuple(sorted(kwargs))) + if key in results_cache: + return results_cache[key] + result = function(*args, **kwargs) + results_cache[key] = result + return result + + return wrapper + + +class Solution: + @cache + def numWays(self, n: int, k: int) -> int: + if n <= 0: + return 0 + if n == 1: + return k + if n == 2: + return k * k + return (k - 1) * (self.numWays(n - 1, k) + self.numWays(n - 2, k)) + + +# @lc code=end diff --git a/276_paint_fence_medium/my_cache.py b/276_paint_fence_medium/my_cache.py new file mode 100644 index 0000000..ff1a8bd --- /dev/null +++ b/276_paint_fence_medium/my_cache.py @@ -0,0 +1,116 @@ +# %% +import functools + + +def cache(function): + results_cache = {} + + @functools.wraps(function) + def wrapper(*args, **kwargs): + + # The key must be the same, regardless of the order in which the keyword arguments are given. + key = (args, tuple(sorted(kwargs.items()))) + print(f"{key=}") + if key in results_cache: + print("cache is used") + return results_cache[key] + result = function(*args, **kwargs) + results_cache[key] = result + return result + + return wrapper + + +# %% +import time + + +@cache +def my_func( + i: int, + kw_x: str = "keyword X", + kw_y: str = "keyword Y", +) -> str: + return f"{time.time()}, {kw_x=}, {kw_y=}" + + +# %% + + +print(my_func(1, kw_x="x", kw_y="y")) +# %% +print(my_func(1, kw_y="y", kw_x="x")) # cache should be used + + +print(my_func(1)) + +# %% +print(my_func(1)) + +# %% +print(my_func(1)) + +# %% +tp = (1, 2, 3) +tp_ = (4, 5) +tp += tp_ +print(tp) # (1, 2, 3, 4, 5) + +# %% +import functools + + +@functools.cache +def myfunc(a=-1, b=-1, c=-1): + print("my func is called") + return 0 + + +# %% +myfunc(a=1, b=2) # my func is called +myfunc(b=2, a=1) # my func is called + +# %% +myfunc(a=1, b=2) # nothing +# %% +myfunc(b=1) # myfunc(b=1, a=-1)と同じ扱い +myfunc(a=-1, b=1) + + +# %% +import timeit + +# キーワード引数が5個の場合 +kwargs_small = {f"k{i}": i for i in range(5)} +# キーワード引数が20個の場合 +kwargs_large = {f"k{i}": i for i in range(20)} + + +# ソートしない場合 (現在の functools.cache の方式) +def no_sort(kwargs): + return tuple(kwargs.items()) + + +# ソートする場合 (ご自身の提案、および古いPythonの方式) +def do_sort(kwargs): + return tuple(sorted(kwargs.items())) + + +# 計測 +time_no_sort_small = timeit.timeit(lambda: no_sort(kwargs_small), number=1_000_000) +time_do_sort_small = timeit.timeit(lambda: do_sort(kwargs_small), number=1_000_000) + +time_no_sort_large = timeit.timeit(lambda: no_sort(kwargs_large), number=1_000_000) +time_do_sort_large = timeit.timeit(lambda: do_sort(kwargs_large), number=1_000_000) + +print(f"--- キーワード引数: 5個 (100万回実行) ---") +print(f"ソートなし: {time_no_sort_small:.4f} 秒") +print(f"ソートあり: {time_do_sort_small:.4f} 秒") +print(f"パフォーマンス差: {time_do_sort_small / time_no_sort_small:.2f} 倍遅い") +print("\n") +print(f"--- キーワード引数: 20個 (100万回実行) ---") +print(f"ソートなし: {time_no_sort_large:.4f} 秒") +print(f"ソートあり: {time_do_sort_large:.4f} 秒") +print(f"パフォーマンス差: {time_do_sort_large / time_no_sort_large:.2f} 倍遅い") + +# %% diff --git a/276_paint_fence_medium/separate_color_pattern.py b/276_paint_fence_medium/separate_color_pattern.py new file mode 100644 index 0000000..e954f1c --- /dev/null +++ b/276_paint_fence_medium/separate_color_pattern.py @@ -0,0 +1,22 @@ +# +# @lc app=leetcode id=276 lang=python3 +# +# [276] Paint Fence +# + + +# @lc code=start +class Solution: + def numWays(self, n: int, k: int) -> int: + one_consecutive = k + two_consecutive = 0 + + for _ in range(n - 1): + new_one_consecutive = (k - 1) * (one_consecutive + two_consecutive) + two_consecutive = one_consecutive + one_consecutive = new_one_consecutive + print(f"{one_consecutive=}, {two_consecutive=}") + return one_consecutive + two_consecutive + + +# @lc code=end diff --git a/276_paint_fence_medium/step1.py b/276_paint_fence_medium/step1.py new file mode 100644 index 0000000..fa14581 --- /dev/null +++ b/276_paint_fence_medium/step1.py @@ -0,0 +1,28 @@ +# +# @lc app=leetcode id=276 lang=python3 +# +# [276] Paint Fence +# + + +# @lc code=start +class Solution: + def numWays(self, n: int, k: int) -> int: + if n == 1: + return k + # Suppose k is given. + # Use dynamic programming + # count[n]: number of possibilities using n posts with k different colors + # count[1] = k, count[2] = k*k + # count[n] = (k-1) * count[n-1] + (k-1) * count[n-2], for all n = 3, 4, ... + + previous_count = k + count = k * k + for _ in range(3, n + 1): + tmp_count = count + count = (k - 1) * count + (k - 1) * previous_count + previous_count = tmp_count + return count + + +# @lc code=end diff --git a/276_paint_fence_medium/step2.py b/276_paint_fence_medium/step2.py new file mode 100644 index 0000000..5f17270 --- /dev/null +++ b/276_paint_fence_medium/step2.py @@ -0,0 +1,38 @@ +# +# @lc app=leetcode id=276 lang=python3 +# +# [276] Paint Fence +# + + +# @lc code=start +class Solution: + def numWays(self, n: int, k: int) -> int: + """ + Find the number of ways using dynamic programming. + total_ways[n]: number of possibilities using n posts with k different colorss (supporse that k is given). + To compute total_ways[n], we can consider two exclusive cases: + 1. colors of fence `n` and `n-1` are different: (k-1) * total_ways[n-1] + 2. colors of fence `n` and `n-1` are same: (k-1) * total_ways[n-2] + + Then, following recursive holds for all n = 3, 4, ...; + - total_ways[1] = k + - total_ways[2] = k*k + - total_ways[n] = (k-1) * total_ways[n-1] + (k-1) * total_ways[n-2] + + For better performance, we use update two variables, total_ways and preivious_total_ways, iteratively instead of allocating an array. + """ + if n <= 0: + return 0 + if n == 1: + return k + + preivious_total_ways = k + total_ways = k * k + for _ in range(3, n + 1): + new_total_ways = (k - 1) * (total_ways + preivious_total_ways) + total_ways, preivious_total_ways = new_total_ways, total_ways + return total_ways + + +# @lc code=end diff --git a/276_paint_fence_medium/step3.py b/276_paint_fence_medium/step3.py new file mode 100644 index 0000000..fa802a0 --- /dev/null +++ b/276_paint_fence_medium/step3.py @@ -0,0 +1,25 @@ +# +# @lc app=leetcode id=276 lang=python3 +# +# [276] Paint Fence +# + + +# @lc code=start +class Solution: + def numWays(self, n: int, k: int) -> int: + if n <= 0: + return 0 + elif n == 1: + return k + previous_total_ways = k + total_ways = k * k + for _ in range(3, n + 1): + total_ways, previous_total_ways = (k - 1) * ( + total_ways + previous_total_ways + ), total_ways + + return total_ways + + +# @lc code=end