문제
790. Domino and Tromino Tiling
You have two types of tiles: a 2 x 1 domino shape and a tromino shape. You may rotate these shapes.
Given an integer n, return the number of ways to tile an 2 x n board. Since the answer may be very large, return it modulo 109 + 7.
In a tiling, every square must be covered by a tile. Two tilings are different if and only if there are two 4-directionally adjacent cells on the board such that exactly one of the tilings has both squares occupied by a tile.
Example 1:
Input: n = 3
Output: 5
Explanation: The five different ways are show above.
Example 2:
Input: n = 1
Output: 1
Constraints:
- 1 <= n <= 1000
문제 설명 (GPT)
이 문제는 2 x n 크기의 보드를 도미노(2x1)와 트로미노(ㄱ 모양) 타일을 이용해서 채우는 방법의 수를 계산하는 문제입니다. 단, 결과가 매우 커질 수 있으므로 109+710^9 + 7로 나눈 나머지를 반환해야 합니다.
재귀와 메모이제이션을 사용하여 2×n2 \times n 보드를 도미노와 트로미노로 덮는 경우의 수를 계산합니다.
문제 이해
이 문제는 2×n2 \times n 크기의 보드를 도미노(2x1)와 트로미노(ㄱ 모양) 타일을 사용하여 덮는 방법의 수를 계산하는 것입니다.
- 도미노: 세로로 2×12 \times 1 또는 가로로 1×21 \times 2의 공간을 덮습니다.
- 트로미노: 2×32 \times 3의 공간 중 3칸만 덮는 타일로, 여러 방향으로 회전 가능합니다.
조건:
- 모든 칸은 완전히 덮어야 합니다.
- 타일은 겹치거나 보드 밖으로 나가면 안 됩니다.
결과:
- 보드를 덮는 가능한 방법의 수를 계산한 후, 결과를 109+710^9 + 7로 나눈 나머지를 반환합니다.
풀이 접근
이 문제는 **동적 프로그래밍(DP)**을 사용하여 해결합니다.
- 보드 크기가 커질수록 가능한 타일 배치 조합이 늘어나기 때문에, 이전 크기에서의 결과를 이용해 현재 크기의 경우를 계산할 수 있습니다.
- **"짝수 상태"와 "홀수 상태"**로 나누어 문제를 세분화하고 재귀적으로 계산합니다.
정의:
- 짝수 상태 (even):
- 2×n 보드를 완전히 덮는 경우를 계산합니다.
- 홀수 상태 (odd):
- 2×n 보드를 덮은 뒤, 1칸이 튀어나온 상태의 경우를 계산합니다.
아이디어:
- 짝수 상태는 기본 상태로, 모든 칸이 덮인 완전한 상태입니다.
- 홀수 상태는 트로미노 타일 일부가 덮여 "비대칭적" 상태를 만들어내는 것을 처리하기 위해 도입되었습니다.
- 두 상태가 서로 재귀적으로 호출되며, 결과를 계산합니다.
점화식
짝수 상태 (even)
짝수 상태를 계산하기 위한 점화식은 다음과 같습니다: even(n) = even(n−1) + even(n−2) + 2⋅odd(n−2)
- : 마지막 칸을 도미노로 세로로 채운 경우.
- even(n−2): 마지막 두 칸을 도미노로 가로로 채운 경우.
- 2⋅odd(n−2): 마지막 세 칸을 트로미노로 덮은 경우 (두 방향 포함).
홀수 상태 (odd)
홀수 상태를 계산하기 위한 점화식은 다음과 같습니다: odd(n) = even(n−1) + odd(n−1)
- even(n−1): 트로미노를 사용하여 마지막 칸을 홀수 상태로 만든 경우.
- odd(n−1): 기존 홀수 상태에서 도미노를 추가로 덮은 경우.
초기값
초기값은 작은 n에 대해 계산된 값입니다:
- even(0) = 1: 2×0 크기의 보드를 덮는 경우는 "아무것도 하지 않는 것" 한 가지입니다.
- even(1) = 1: 2×1 보드는 도미노 한 개로 덮을 수 있는 한 가지 방법만 존재합니다.
- odd(0) = 0: 2×0 보드에서는 홀수 상태가 불가능합니다.
복잡도 분석
- 시간 복잡도:
- 메모이제이션을 통해 각 n에 대해 한 번만 계산하므로 O(n)입니다.
- 공간 복잡도:
- 캐시 배열과 재귀 호출 스택으로 인해 O(n)의 공간이 필요합니다.
예제 실행복잡도 분석
n=4:
- even(4) = even(3) + even(2) + 2⋅odd(2)
- 계산:
- even(3) = 5, even(2) = 2, odd(2) = 2
- even(4)=5+2+4 = 11
n=5:
- even(5) = even(4) + even(3) + 2⋅odd(3)
- 계산:
- even(4) = 11, even(3) = 5, odd(3) = 4
- even(5) = 11+5+8 = 24
추가 정보
왜? 짝수 상태 (even) 에서 2⋅odd(n−2) 수행?
핵심 아이디어: 2⋅odd(n−2)란?
1. 짝수 상태 even(n) 정의
짝수 상태 even(n)은 2×n 보드를 완전히 덮는 경우의 수를 의미합니다.
2. 트로미노의 기여
트로미노는 2×3 크기의 영역을 덮습니다. 따라서 트로미노를 사용할 경우, 보드의 마지막 세 칸에 영향을 미칩니다.
3. 2⋅odd(n−2)의 의미
트로미노를 보드의 마지막 세 칸에 배치하면, 보드의 나머지 부분(2×(n−2))은 홀수 상태 odd(n−2) 로 남습니다.
- 트로미노는 2×3의 공간을 덮으며, 회전 가능성이 있기 때문에 두 가지 방향으로 배치할 수 있습니다:
- 트로미노가 오른쪽 위로 "ㄱ" 형태.
- 트로미노가 오른쪽 아래로 "ㄴ" 형태.
따라서, 각각의 트로미노 방향에 대해 odd(n−2) 경우의 수만큼 새로운 타일링 방법이 생깁니다.
결론적으로 2⋅odd(n−2)는 이 두 가지 방향에서 만들어지는 모든 가능한 경우의 수를 더한 것입니다.
2⋅odd(n−2) 시각적 이해
예: n=4
- 현재 보드 크기는 2×4.
- 트로미노를 마지막 세 칸 (n=4에서 오른쪽 끝) 부분에 배치한다고 가정합니다.
왜 "짝수 상태에서 홀수 상태로 연결"이 중요한가?
트로미노는 "짝수 상태"를 "홀수 상태"로 변환하는 역할을 합니다.
- 트로미노는 2×3 공간을 덮으면서 남은 공간을 홀수 상태로 만들고, 남은 부분을 다시 odd(n−2)를 기반으로 계산해야 합니다.
이런 이유로, 짝수 상태 even(n) 계산에는 홀수 상태 odd(n−2)가 포함됩니다.
홀수 상태 이해~
Code (GPT)
class Solution:
def numTilings(self, n: int) -> int:
MOD = 10 ** 9 + 7 # 결과가 매우 커질 수 있으므로 나머지 연산을 적용할 값
# 캐시 배열 생성: "even" 상태에 대한 메모이제이션
hasEvenCashe = [False] * (n + 1) # 이미 계산한 "even" 상태인지 여부를 저장
evenCashe = [None] * (n + 1) # "even" 상태에 대한 결과를 저장
# "even" 상태 계산 함수: 2xN 보드를 완전히 덮는 경우의 수
def even(N):
if N == 0: # 2x0 보드는 완전히 비어 있는 상태로, 경우의 수는 1가지
return 1
if hasEvenCashe[N]: # 이미 계산된 값이 있으면 캐시에서 반환
return evenCashe[N]
count = 0 # 경우의 수를 저장할 변수
count += even(N - 1) # 마지막 칸을 도미노로 세로로 채운 경우
if N - 2 >= 0:
count += even(N - 2) # 마지막 두 칸을 도미노로 가로로 채운 경우
count += 2 * odd(N - 2) # 마지막 세 칸에 트로미노를 사용한 경우
hasEvenCashe[N] = True # 현재 상태를 계산했음을 표시
evenCashe[N] = count % MOD # 결과를 캐시에 저장하며 MOD 연산 적용
return evenCashe[N]
# 캐시 배열 생성: "odd" 상태에 대한 메모이제이션
hasOddCashe = [False] * (n + 1) # 이미 계산한 "odd" 상태인지 여부를 저장
oddCashe = [None] * (n + 1) # "odd" 상태에 대한 결과를 저장
# "odd" 상태 계산 함수: 2xN 보드에서 1개의 타일이 튀어나온 상태의 경우의 수
def odd(N):
if N == 0: # 2x0 보드는 "odd" 상태가 불가능하므로 경우의 수는 0
return 0
if hasOddCashe[N]: # 이미 계산된 값이 있으면 캐시에서 반환
return oddCashe[N]
count = 0 # 경우의 수를 저장할 변수
count += even(N - 1) # 마지막 칸을 트로미노로 덮어 "odd" 상태로 만든 경우
count += odd(N - 1) # 기존 "odd" 상태에서 도미노를 추가한 경우
hasOddCashe[N] = True # 현재 상태를 계산했음을 표시
oddCashe[N] = count % MOD # 결과를 캐시에 저장하며 MOD 연산 적용
return oddCashe[N]
return even(n) % MOD # 최종적으로 "even(n)"의 값을 반환
참고 문제 풀이
https://youtu.be/fQd104LDSfU?si=bLbxck9-0-E3wU0q
https://youtu.be/o75alNvYqHU?si=hXTX84BB-Z69n-8E
============================================================================================
https://artofcom77.tistory.com/218
[DP-1D][Medium] 790. Domino and Tromino Tiling
https://leetcode.com/problems/domino-and-tromino-tiling LeetCode - The World's Leading Online Programming Learning Platform Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next i
artofcom77.tistory.com
직관적으로.. => RecursionError: maximum recursion depth exceeded in comparison
class Solution:
def numTilings(self, n: int) -> int:
def dfs(n):
if n==1:
return 1
elif n==2:
return 2
elif n==3:
return 5
mod = 10**9 + 7
sub_sum = 0
# 올바르게 nn에 대해 dfs 호출
for nn in range(n - 3, -1, -1):
sub_sum = (sub_sum + dfs(nn)) % mod
# 전체 결과 계산
prev = dfs(n-1)
prevprev = dfs(n-2)
res = (prev + prevprev + (sub_sum * 2) + 2) % mod
return res
return dfs(n)
메모리제이션 => RecursionError: maximum recursion depth exceeded in comparison
class Solution:
def numTilings(self, n: int) -> int:
memo = {}
def dfs(n):
if n==1:
return 1
elif n==2:
return 2
elif n==3:
return 5
if n in memo:
return memo[n]
mod = 10**9 + 7
sub_sum = 0
# 올바르게 nn에 대해 dfs 호출
for nn in range(n - 3, -1, -1):
sub_sum = (sub_sum + dfs(nn)) % mod
# 전체 결과 계산
prev = dfs(n-1)
prevprev = dfs(n-2)
res = (prev + prevprev + (sub_sum * 2) + 2) % mod
memo[n] = res
return memo[n]
return dfs(n)
DP로..
class Solution:
def numTilings(self, n: int) -> int:
if n==1:
return 1
elif n==2:
return 2
elif n==3:
return 5
dp = [0]*(n+1)
dp[1] = 1
dp[2] = 2
dp[3] = 5
mod = 10**9 + 7
sub_sum = 0
# 올바르게 nn에 대해 dfs 호출
# for nn in range(n - 3, -1, -1):
for nn in range(4, n+1):
sub_sum = (sub_sum + dp[nn]) % mod
# 전체 결과 계산
for i in range(4, n+1):
dp[i] = (dp[i - 1] + dp[i - 2] + 2 * sum(dp[:i - 2]) + 2) % mod
return dp[n]
추가 문제들
- 873. Length of Longest Fibonacci Subsequence
- 688. Knight Probability in Chessboard
- 42. Trapping Rain Water
'LeetCode > LeetCode75' 카테고리의 다른 글
[LeetCode 75] Medium - 901. Online Stock Span (4) | 2024.11.10 |
---|---|
[LeetCode 75] Medium - 739. Daily Temperatures (0) | 2024.11.10 |
[LeetCode 75] Medium - 435. Non-overlapping Intervals (0) | 2024.11.10 |
[LeetCode 75] Medium - 1268. Search Suggestions System (0) | 2024.11.09 |
[LeetCode 75] Medium - 1318. Minimum Flips to Make a OR b Equal to c (0) | 2024.11.09 |