BOJ_1904 01타일
- TOC {:toc}
이 글은 백준 온라인 저지의 1904번 문제를 파이썬(Python)으로 풀이한 것을 모아놓은 글입니다.
일종의 연습 기록이며 제가 정답을 받은 코드와 참고할만한 다른 코드를 같이 기록합니다. 필요한 경우 코드에 대한 해설을 기록합니다만 코드는 통과했어도 해설은 틀릴 수 있기 때문에 가볍게 참고해주시길 부탁드립니다. 피드백은 편하신 방법으로 자유롭게 주시면 감사하겠습니다.
2021.05.06 (Python)
메모리 | 시간 | 코드 길이 |
---|---|---|
68256 KB | 416 ms | 113 B |
단계 | 시작 시각 | 끝난 시각 | 걸린 시간 |
---|---|---|---|
문제 이해 | 20:21:06 | 20:21:41 | |
풀이 생각 1 | 20:21:51 | 20:34:12 | |
풀이 생각 2 | 21:02:15 | 21:19:59 | |
코딩 | 21:39:02 | 21:41:50 | |
디버깅 | 21:45:05 | 21:49:51 |
T = [0, 1, 2]
N = int(input())
for i in range(3, N + 1):
T.append((F[i - 1] + F[i - 2]) % 15746)
print(T[N])
아이디어 & 풀이
N이 증가할 때 새로운 수는 이전의 수에 1
또는 00
을 붙여서 만든다.
- 이전에 만든 수(N - 1)에
1
을 추가하거나 - 두 수 전에 만든 수 (N - 2)에
00
을 추가하면 된다 (자릿수가 두 자리이므로).- N - 1에
11
을 추가하는 것은 위의 경우를 두 번 반복한 것과 같으므로 고려하지 않아도 된다.
- N - 1에
N | 1 | 2 | 3 | 4 | 5 | … |
---|---|---|---|---|---|---|
타일 | 1 |
00 , 11 |
001 , 111 , 100 |
0011 , 1001 , 1111 , 0000 , 1100 |
00111 , 10011 , 11111 , 00001 , 11001 , 00100 , 11100 , 10000 |
|
+ 1 |
1 + 1 |
00 + 1 , 11 + 1 |
001 + 1 , 100 + 1 , 111 + 1 |
0011 + 1 , 1001 + 1 , 1111 + 1 , 0000 + 1 , 1100 + 1 |
||
+ 00 |
1 + 00 |
00 + 00 , 11 +00 |
001 + 00 , 111 + 00 , 100 + 00 |
- 추가된 파일을 붙이는 위치는 일관되게만 유지하면 중복이나 빠짐없이 모든 경우의 수를 구할 수 있다.
결국 N
개의 타일로 만들 수 있는 이진수는 N - 1
개로 만들 수 있는 이진수의 개수와 N - 2
로 만들 수 있는 이진수의 개수 합과 같다. 이는 타일의 개수가 피보나치 수열을 이룬다는 것을 의미한다.
N
의 값이 매우 클 수 있기 때문에 주어진 조건대로 수열을 구성할 때15746
으로 나눈 나머지로 구성한다.- 나머지 연산은 덧셈에 대해 분배법칙이 성립하기 때문에 수열을 구성할 때 미리 계산해도 상관없다.
디버그
- 처음에 개수를 나열했을 때 중간에 계산을 잘못해 피보나치 수열이라는 것을 인지하지 못했다.
1
과00
의 개수에 따라 순열을 구성할 수 있는 개수를 계산해보려고 했는데 동적 계획법과 관련이 없고 N // 2개 항에 대해 팩토리얼 계산을 해야 하는 게 너무 오래 걸릴 것 같아서 찝찝하더니 바로 실행하자마자 시간 초과 났다.
피드백
- 전체 수열을 리스트에 구성하는 것 보다 두 개의 변수에 매번 새로 얻은 값을 덧씌우면서 계산하는 것이 더 빠르다.
- 피보나치 수열을
15746
으로 나눈 ‘나머지’로T
를 구성하기 때문에 특정 지점에서 수열이 반복될 수 있고, 그렇다면 반복이 시작되는 지점 직전까지만 수열을 구성하는 게 효율적이다.
2022.03.23 (JS)
메모리 | 시간 | 코드 길이 |
---|---|---|
17432 KB | 196 ms | 348 B |
단계 | 시작 시각 | 끝난 시각 | 걸린 시간 |
---|---|---|---|
문제 이해 | 17:26:11 | 17:27:53 | |
풀이 생각 | 17:27:54 | 17:29:06 | |
코딩 | 17:29:09 | 17:37:37 |
const fs = require("fs");
const input = fs.readFileSync('/dev/stdin').toString().trim();
const N = parseInt(input);
tile = new Array(N + 1).fill(0);
function main(N) {
tile[1] = 1;
tile[2] = 2;
for (let i = 3; i < N + 1; i++) {
tile[i] = (tile[i - 1] + tile[i - 2]) % 15746;
}
return tile[N];
}
console.log(main(N));
참고 답안 1
# 선형적인 방법
def fib(n):
a, b = 0, 1
for i in range(n):
a, b = b, (a + b) % 15746
return b
print(fib(int(input())))
참고 답안 2
# 동적 계획법 + 중복 방지
# 풀이 2-1
T = {0: 0, 1: 1, 2: 2}
mod = 3
while True:
T[i] = (F[i - 1] + F[i - 2]) % 15746
if T[i] == 1:
if T[i - 1] == 0:
break
mod += 1
N = int(input())
print(d[N % mod])
# 풀이 2-2
T = [1, 1, 2]
# 마지막 두 값이 [1, 1]이 아닐 때까지
while T[-2:] != [1, 1]:
# 이전 값과 이전 값의 전 값을 더한 것을 15746으로 나눈 나머지를
# T에 추가한다.
T.append((F[-2] + F[-1]) % 15746)
# mod는 T의 길이에 2를 뺀 값이다.
mod = len(T) - 2
print(T[int(input()) % mod])
아이디어 & 풀이
T
의 원소가 반복되지 않을 때까지만 수열을 구성한다.
- 피보나치 수열을
15746
으로 나눈 ‘나머지’로T
를 구성하기 때문에 특정 지점에서 수열이 반복될 수 있다. - 피보나치 수열은 이전 두 수를 더해 다음 값을 결정하고 수열의 첫 시작이
[1, 1]
이기 때문에, 현재까지 만든 수열의 마지막 두 값이[1, 1]
이면 수열이 반복된다.
현재 수열의 마지막 두 값이 [1, 1]
와 일치할 때까지만 T
를 구성하고 반복 직전까지의 원소의 개수 mod
를 구해 이용하면 반복을 방지할 수 있다.
예를 들어 수열이 다음과 같이 10번마다 반복된다고 하면
i |
0 | 1 | 2 | 3 | 4 | … | 9 | 10 | 11 | 12 | … |
---|---|---|---|---|---|---|---|---|---|---|---|
T[i] |
1 | 1 | 2 | 3 | 5 | … | 7 | 1 | 1 | 2 | … |
mod
는 반복 직전까지의 원소의 개수로10
이다.T
는 반복을 알아채는 11번 인덱스까지T
를 구성하기 때문에 이는len(T) - 2
의 값과 같다.
- 그러면 10 이상의 입력
N
에 대해서는F[N % mod]
를 출력하면 굳이N
번쨰 원소까지T
를 구성하지 않아도 해당 값을 출력할 수 있다.
참고 답안 3
# 피보나치 수의 성질을 이용한 방법
import sys
n = int(input())
# 타일을 모아놓는 딕셔너리
T = {0: 0, 1: 1, 2: 2}
def fibo(n):
# n이 T안에 존재하지 않으면
if not n in T:
# n을 2로 나눈 몫을 n2라고 할 때
n2 = n // 2
# n이 짝수이면
if n % 2 == 0:
T[n] = fibo(n2) * fibo(n2) + fibo(ibo - 1) * fibo(n2 - 1)
# n이 홀수이면
else:
T[n] = fibo(n2) * fibo(n2 + 1) + fibo(n2 - 1) * fibo(n2)
return T[n] % 15746
print(fibo(n) % 15746)
아이디어 & 풀이
피보나치 수열은 기본적인 점화식 외에도 다양한 성질을 갖는다.
- $F_{2n-1} = F_n^2 + F_{n-1}^2$
- $F_{2n} = (F_{n-1} + F_{n+1})F_n = (2F_{n_1} + F_n)F_n$
참고 답안 4
# 행렬 곱을 이용한 방법
# 풀이 4-1
N = int(input()) + 1
def multiply(A, B):
return list(
map(
# 15746으로 나눈 나머지를 나머지를 저장하도록
# 긱 값을 lambda를 이용해 매핑한다.
lambda x: x % 15746,
(
A[0] * B[0] + A[1] * B[2],
A[0] * B[1] + A[1] * B[3],
A[2] * B[0] + A[3] * B[2],
A[2] * B[1] + A[3] * B[3],
),
)
)
# 입력받은 행렬을 n 제곱하는 함수
# 재귀 용법을 이용한다.
def powwer(A, n):
# N이 1이면
if n == 1:
# A를 반환한다.
return A
# A를 n을 2로 나눈 몫만큼 제곱한 값을 X에 대입한다.
X = power(A, n // 2)
# X를 두 번 곱한 값을 ans에 대입한다.
ans = multiply(X, X)
# n이 홀수이면
if n % 2 == 1:
# ans에 A를 한 번 더 곱해준다.
ans = multiply(ans, A)
# ans를 반환한다.
return ans
# 연산 결과의 2행 1열 원소를 출력한다.
print(power((1, 1, 1, 0), n)[2])
# 풀이 4-2
# 입력받은 행렬 A, B를 곱한 값을 반환하는 함수
def multiply(A, B):
return (
(A[0] * B[0] + A[1] * B[2]) % 15746,
(A[0] * B[1] + A[1] * B[3]) % 15746,
(A[2] * B[0] + A[3] * B[2]) % 15746,
(A[2] * B[1] + A[3] * B[3]) % 15746,
)
# 피보나치 행렬을 n 제곱하는 함수
def power(n):
# 피보나치 행렬
A = (1, 1, 1, 0)
# 현재 행렬의 값을 담을 변수.
# 초깃값으로 단위행렬을 사용한다.
B = (1, 0, 0, 1)
while n > 0:
# n이 홀수이면
if n & 1:
# B에 A를 곱한다.
B = multiply(A, B)
# n을 2로 나눈 뒤
n >>= 1
# A를 제곱한다.
A = multiply(A, A)
# B의 1행 1열 원소를 반환한다.
# B의 첫 값을 단위행렬로 정했기 때문에 한 번 덜 곱하게 되므로
# N + 1번째 항인 1행 1열 원소를 반환한다.
return B[0]
print(f"{power(int(input()))}")
아이디어 & 풀이
풀이 4는 행렬 곱을 이용해 문제를 해결한다.
피보나치 수열은 $F_n = F_{n-1} + F_{n-2}$를 만족하기 때문에 다음과 같이 행렬로 나타낼 수 있다.
$$ \begin{bmatrix} F_{N + 1} \ F_N \end{bmatrix} = \begin{bmatrix} F_N + F_{N-1} \ F_N \end{bmatrix} = \begin{bmatrix} 1 & 1 \ 1 & 0 \end{bmatrix} \begin{bmatrix} F_N \ F_{N-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 \ 1 & 0 \end{bmatrix}^N \begin{bmatrix} F_1 \ F_0 \end{bmatrix} $$
행렬은 튜플을 이용해서 나타내고 2 by 2 행렬의 각 행과 열에 다음과 같이 리스트 인덱스를 부여한다.
1열 | 2열 | |
---|---|---|
1행 | 0 | 1 |
2행 | 2 | 3 |
- 예를 들어 $A = \begin{bmatrix} 2 & 3 \ 1 & 2 \ \end{bmatrix}$ 는
A = (2, 3, 1, 2)
로 나타낸다. - 피보나치 행렬 $F = \begin{bmatrix} 1 & 1 \ 1 & 0 \ \end{bmatrix}$은
F = (1, 1, 0, 1)
로 나타낸다.
참고
- 피보나치 수 by 위키백과
- 피보나치 수를 구하는 여러가지 방법 by baekjoon @ BAEKJOON ONLINE JUDGE
- 피보나치 수를 계산하는 21가지 방법 by Monotone Cake & Coffee
-
ps-python