|
| 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/) |
0 commit comments