타일링 문제는 DP(Dynamic Programming)의 대표적인 유형입니다. 하지만 이 문제는 타일링과 다르게 '특이한 모양(Unique Shapes)'이 발생한다는 점에서 난이도가 급상승합니다.
가장 먼저 타일의 넓이를 생각해야 합니다. 우리가 사용하는 타일의 넓이는 ()입니다.
벽의 전체 넓이는 입니다.
이 커질 때 경우의 수가 어떻게 늘어나는지 단계별로 분석해 봅시다.
인 경우 ()
을 확장할 때 일반적인 경우 ()
예외 케이스: 특이한 모양 ()
위의 논리를 종합하면, 을 구하기 위해서는 의 결과뿐만 아니라 까지의 모든 결과를 참조해야 합니다.
이 수식을 코드로 옮길 때, 로 초기화해두면(아무것도 안 놓는 경우 1가지), 수식을 깔끔하게 처리할 수 있습니다.
단순히 을 구할 때 앞뒤의 곱으로만 처리하려 했으나, 이는 중복 집계와 누락이 동시에 발생하는 잘못된 접근입니다. 타일링에서 '분할 정복'처럼 접근할 때는 겹치는 경계선을 매우 주의해야 합니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static int N;
static int[] dp;
public static void main(String[] args) throws IOException {
N = Integer.parseInt(br.readLine());
if (N % 2 == 1) {
System.out.println(0);
return;
}
dp = new int[N + 1];
dp[2] = 3;
// 논리적 오류: 단순히 양쪽 끝을 기준으로 곱하는 방식은
// 3xN 타일링의 '고유 패턴(Unique Shapes)'을 정확히 반영하지 못합니다.
for (int n = 4; n <= N; n += 2) {
int i = n - 2;
int j = 2;
while (i >= j) {
dp[n] += (dp[i] * dp[j]);
i -= 2;
j += 2;
}
dp[n] += 2;
}
System.out.println(dp[N]);
}
}
위에서 유도한 점화식을 그대로 구현한 코드입니다. 이중 반복문을 사용하여 이전의 모든 짝수 값을 참조합니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static int N;
static int[] dp;
public static void main(String[] args) throws IOException {
N = Integer.parseInt(br.readLine());
// 홀수인 경우 0 출력
if (N % 2 == 1) {
System.out.println(0);
return;
}
dp = new int[N + 1];
dp[0] = 1; // 특이 케이스 계산을 위한 초기값
dp[2] = 3;
for (int n = 4; n <= N; n += 2) {
// 1. N-2까지 채우고 2칸을 채우는 경우 (3가지)
dp[n] = dp[n - 2] * dp[2];
// 2. 특이 케이스 누적 (4칸 이상 떨어진 곳들의 경우의 수 * 2)
for (int i = 4; i <= n; i += 2) {
dp[n] += dp[n - i] * 2;
}
}
System.out.println(dp[N]);
}
}
내부 반복문이 계속해서 sum을 구하는 구조임을 파악하면, 이를 변수 하나로 관리하여 으로 최적화할 수 있습니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static int N;
static long[] dp; // N이 30이면 int 범위를 넘을 수 있으므로 long 권장 (문제 범위상 int도 가능은 함)
public static void main(String[] args) throws IOException {
N = Integer.parseInt(br.readLine());
if (N % 2 == 1) {
System.out.println(0);
return;
}
dp = new long[N + 1];
dp[0] = 1;
dp[2] = 3;
long sum = 0; // 이전까지의 특이 케이스 누적합 (dp[N-4]*2 + dp[N-6]*2 ...)
for (int n = 4; n <= N; n += 2) {
// 직전 단계의 특이 케이스 값을 sum에 추가
sum += dp[n - 4] * 2;
// 점화식: (바로 앞 단계 * 3) + (지금까지 누적된 특이 케이스들의 합)
dp[n] = (dp[n - 2] * 3) + sum;
}
System.out.println(dp[N]);
}
}
sum 변수를 활용해 반복되는 덧셈 연산(Memoization of Sum)을 수행했습니다. 이를 통해 내부 반복문을 제거했습니다.