본문 바로가기
Tech/Coding

달팽이2

by redcubes 2025. 8. 4.

문제 링크: https://www.acmicpc.net/problem/1952

1. 공식 도출 과정

1.1 기본 개념 정의

달팽이 경로에서 **레이어(layer)**란 표의 가장자리부터 안쪽으로 들어가는 동심원 형태의 고리를 의미합니다.

$k = \min(M, N) \text{ (총 레이어 수)}$

1.2 표를 이용한 레이어 시각화

5×4 표에서의 레이어 구조:

┌─────────────────────────────────────┐
│ Layer 1 (외곽)                      │
│ →  →  →  →  (4칸, 꺾임없음)         │
│ ↓              ↓                    │
│ ↓              ↓                    │
│ ↓              ↓                    │
│ ←  ←  ←  ←  ←  (5칸, 1회 꺾임)      │ 
│    ↗                                │
│ ↑  Layer 2     ↑ (3칸, 1회 꺾임)    │
│ └──────────────┘                    │
└─────────────────────────────────────┘

Layer 1: 외곽 (4→ + 3↓ + 4← + 2↑) = 13칸, 2회 꺾임
Layer 2: 내부 (2→ + 1↓ + 2← + 0↑) = 5칸, 1회 꺾임  
Layer 3: 중심 (2→) = 2칸, 0회 꺾임

레이어별 꺾임 패턴 분석:

  레이어 크기 완전한 사각형? 꺾임 횟수 설명
1 5×4 ✓ (완전) 2회 오른쪽 끝→아래, 아래 끝→왼쪽
2 3×4 ✓ (완전) 2회 오른쪽 끝→아래, 아래 끝→왼쪽

1.3 단계별 수식 도출

Step 1: 레이어 수 정의

M×N 표에서 가능한 레이어 수는 짧은 변의 절반으로 결정됩니다.

$k = \min(M, N)$

Step 2: 완전 레이어와 불완전 레이어 구분

레이어 $i$ (1-indexed)의 실제 크기:

  • 가로: $M - 2(i-1)$
  • 세로: $N - 2(i-1)$

레이어가 완전한 사각형을 이루는 조건: $M - 2(i-1) \geq 2 \text{ and } N - 2(i-1) \geq 2$

즉, $i \leq \frac{\min(M,N)}{2}$일 때 완전한 레이어입니다.

Step 3: 완전 레이어의 꺾임 횟수

완전한 레이어에서는 사각형을 한 바퀴 돌면서 정확히 2번 꺾입니다:

$\text{완전 레이어 수} = \left\lfloor \frac{\min(M,N)}{2} \right\rfloor$

$\text{완전 레이어들의 총 꺾임} = 2 \times \left\lfloor \frac{\min(M,N)}{2} \right\rfloor$

Step 4: 불완전 레이어(마지막 레이어) 처리

$\min(M,N)$이 홀수인 경우, 중앙에 불완전한 레이어가 남습니다.

불완전 레이어의 형태:

  • $M > N$이고 $N$이 홀수: 세로 한 줄 → 1회 꺾임
  • $M < N$이고 $M$이 홀수: 가로 한 줄 → 0회 꺾임
  • $M = N$이고 홀수: 중심점 하나 → 0회 꺾임

Step 5: 통합 공식 도출

케이스 분석을 통한 일반 공식:

$\text{총 꺾임 횟수} = \begin{cases} 2\left\lfloor \frac{\min(M,N)}{2} \right\rfloor & \text{if } \min(M,N) \text{이 짝수} \ 2\left\lfloor \frac{\min(M,N)}{2} \right\rfloor + \delta & \text{if } \min(M,N) \text{이 홀수} \end{cases}$

여기서 $\delta$는 불완전 레이어의 꺾임 횟수:

$\delta = \begin{cases} 1 & \text{if } M > N \text{ and } N \text{ is odd} \ 0 & \text{otherwise} \end{cases}$

Step 6: 공식 단순화

$\min(M,N) = k$라 하면:

$\left\lfloor \frac{k}{2} \right\rfloor = \begin{cases} \frac{k}{2} & \text{if } k \text{ is even} \ \frac{k-1}{2} & \text{if } k \text{ is odd} \end{cases}$

이를 이용하여 통합하면:

$T = 2\left\lfloor \frac{k}{2} \right\rfloor + \begin{cases} 0 & \text{if } k \text{ is even} \ 1 & \text{if } k \text{ is odd and } M > N \ 0 & \text{if } k \text{ is odd and } M \leq N \end{cases}$

Step 7: 최종 간소화

위 식을 다시 정리하면:

$T = k - 1 + \left\lfloor \frac{k}{2} \right\rfloor + \begin{cases} 1 & \text{if } k \text{ is odd and } M > N \ 0 & \text{otherwise} \end{cases}$

더 간단한 형태로:

$T = 2k - 1 - \mathbf{1}_{{M \leq N}}$

여기서 $\mathbf{1}_{{M \leq N}}$는 지시함수입니다.

증명:

  • $M > N$인 경우: $T = 2k - 1$
  • $M \leq N$인 경우: $T = 2k - 1 - 1 = 2k - 2$

1.4 공식 검증 표

M N k=min(M,N) M>N? 예상 공식 실제 꺾임
3 4 3 2×3-1-1=4 4 ✓
4 3 3 2×3-1-0=5 5 ✓
5 5 5 2×5-1-1=8 8 ✓
6 2 2 2×2-1-0=3 3 ✓
1 7 1 2×1-1-1=0 0 ✓

2. 알고리즘 구현 세부사항

2.1 시뮬레이션 접근법 상세 분석

def snail_simulation(M, N):
    # 1. 초기 설정
    matrix = [[0] * M for _ in range(N)]  # N×M 행렬 (행×열)
    directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]  # 동, 남, 서, 북
    x, y = 0, 0  # 현재 위치 (열, 행)
    direction_idx = 0  # 현재 방향 인덱스
    turns = 0
    
    # 2. 경로 추적 루프
    for step in range(M * N - 1):  # 마지막 칸 제외하고 이동
        # 2.1 현재 위치 방문 표시
        matrix[y][x] = 1
        
        # 2.2 다음 위치 계산
        dx, dy = directions[direction_idx]
        next_x, next_y = x + dx, y + dy
        
        # 2.3 꺾임 조건 검사
        need_turn = False
        
        # 경계 검사
        if not (0 <= next_x < M and 0 <= next_y < N):
            need_turn = True
        # 이미 방문한 칸 검사    
        elif matrix[next_y][next_x] == 1:
            need_turn = True
            
        # 2.4 꺾임 처리
        if need_turn:
            turns += 1
            direction_idx = (direction_idx + 1) % 4  # 시계방향 회전
            dx, dy = directions[direction_idx]
            next_x, next_y = x + dx, y + dy
            
        # 2.5 위치 업데이트
        x, y = next_x, next_y
    
    return turns

2.2 핵심 로직 설명

방향 벡터 설계:

  • (0, 1): 동쪽(오른쪽) - x 증가
  • (1, 0): 남쪽(아래) - y 증가
  • (0, -1): 서쪽(왼쪽) - x 감소
  • (-1, 0): 북쪽(위) - y 감소

꺾임 감지 조건:

  1. 경계 이탈: 다음 위치가 표 범위를 벗어남
  2. 중복 방문: 다음 위치가 이미 방문된 칸

시계방향 회전:

direction_idx = (direction_idx + 1) % 4

0→1→2→3→0 순환으로 동→남→서→북 방향 전환

3. 시간/공간 복잡도 분석

3.1 공식 기반 접근법

def snail_formula(M, N):
    return (min(M, N) << 1) - 1 - (M <= N)

시간 복잡도: O(1)

  • 단순 산술 연산만 수행
  • 입력 크기와 무관한 상수 시간

공간 복잡도: O(1)

  • 추가 메모리 사용 없음
  • 변수 몇 개만 사용

3.2 시뮬레이션 접근법

def snail_simulation(M, N):
    matrix = [[0] * M for _ in range(N)]  # O(M×N) 공간
    # ... 시뮬레이션 로직

시간 복잡도: O(M×N)

  • 표의 모든 칸을 한 번씩 방문
  • 각 칸에서 상수 시간 연산

공간 복잡도: O(M×N)

  • M×N 크기의 2차원 배열 필요
  • 방문 여부 추적용

 

4. 시각적 예시

4.0 표를 이용한 레이어별 꺾임 분석

4×3 표의 레이어별 분해:

Layer 1 (외곽 레이어):
┌───→───→───→┐
│ 1   2   3  │4  ← 꺾임 1: 오른쪽 끝에서 아래로
│            ↓
│11  12      │5
│ ↑          ↓  
│10   9   8  │6  ← 꺾임 2: 아래쪽 끝에서 왼쪽으로
└───←───←───←┘7
    ↑           ← 꺾임 3: 왼쪽 끝에서 위로 (내부 진입)

Layer 2 (내부 레이어):  
    ┌─→─┐
    │11 │12 ← 꺾임 4: 내부에서 오른쪽 끝 도달, 종료
    └───┘

총 꺾임: 4회 (Layer 1: 2회 + Layer 2: 2회)

5×5 표의 완전한 레이어 구조:

Layer 1: 외곽 5×5 사각형
 1→ 2→ 3→ 4→ 5
16                6  ← 꺾임 1
15                7
14                8  
13←12←11←10← 9     ← 꺾임 2

Layer 2: 내부 3×3 사각형  
    17→18→19
    24    20  ← 꺾임 3
    23←22←21  ← 꺾임 4

Layer 3: 중심 1×1
        25    (꺾임 없음)

총 꺾임: 8회 (Layer 1: 2회 + Layer 2: 2회 + 나머지 4회)

레이어 크기 변화 패턴:

레이어 원본 크기 레이어 크기  타입 꺾임
1 5×5 5×5 완전 사각형 2회
2 5×5 3×3 완전 사각형 2회
3 5×5 1×1 0회

$\text{레이어 } i \text{의 크기: } (M-2(i-1)) \times (N-2(i-1))$

4.1 작은 크기 표 (3×4)

최종 경로 결과:
[ 1][10][ 9]
[ 2][11][ 8] 
[ 3][12][ 7]
[ 4][ 5][ 6]

꺾임 지점 분석:
- 4→5: 1회 꺾임 (오른쪽 끝에서 아래로)
- 6→7: 2회 꺾임 (아래쪽 끝에서 왼쪽으로)  
- 9→10: 3회 꺾임 (왼쪽 끝에서 위로, 다음 레이어 진입)
- 10→11: 4회 꺾임 (내부 레이어에서 오른쪽으로)

총 꺾임 횟수: 4회
공식 검증: M=3, N=4, min(M,N)=3
T = 2×3 - 1 - (3≤4 ? 1 : 0) = 6 - 1 - 1 = 4회 ✓

4.2 중간 크기 표 (5×5)

최종 경로 결과:
[ 1][16][15][14][13]
[ 2][17][24][23][12]
[ 3][18][25][22][11]  
[ 4][19][20][21][10]
[ 5][ 6][ 7][ 8][ 9]

꺾임 지점 분석:
- 5→6: 1회 꺾임 (오른쪽 끝에서 아래로)
- 9→10: 2회 꺾임 (아래쪽 끝에서 왼쪽으로)  
- 13→14: 3회 꺾임 (왼쪽 끝에서 위로)
- 16→17: 4회 꺾임 (위쪽 끝에서 내부 레이어 진입, 오른쪽으로)
- 19→20: 5회 꺾임 (내부 레이어 오른쪽 끝에서 아래로)
- 21→22: 6회 꺾임 (내부 레이어 아래쪽 끝에서 왼쪽으로)
- 23→24: 7회 꺾임 (내부 레이어 왼쪽 끝에서 위로)
- 24→25: 8회 꺾임 (최내부 중심으로 진입)

총 꺾임: 8회
공식 검증: M=N=5, min(M,N)=5
T = 2×5 - 1 - (5≤5 ? 1 : 0) = 10 - 1 - 1 = 8회 ✓

4.4 케이스별 마지막 레이어 분석

케이스 1: M > N (세로가 더 긴 경우) - 6×3 표

실제 경로 결과:
[ 1][14][13][12][11][10]  
[ 2][15][16][17][18][ 9]
[ 3][ 4][ 5][ 6][ 7][ 8]

꺾임 분석:
- 3→4: 1회 꺾임 (1,3)에서 동쪽→남쪽
- 8→9: 2회 꺾임 (6,3)에서 남쪽→서쪽  
- 10→11: 3회 꺾임 (6,1)에서 서쪽→북쪽
- 14→15: 4회 꺾임 (2,1)에서 북쪽→동쪽 (내부 진입)
- 15→16: 5회 꺾임 (2,2)에서 동쪽→남쪽

총 꺾임: 5회
공식: k=3, M>N이므로 T = 2×3-1-0 = 5회 ✓

케이스 2: M < N (가로가 더 긴 경우) - 3×6 표

실제 경로 결과:
[ 1][14][13]
[ 2][15][12]
[ 3][16][11]
[ 4][17][10]
[ 5][18][ 9]
[ 6][ 7][ 8]

꺾임 분석:
- 6→7: 1회 꺾임 (1,6)에서 동쪽→남쪽
- 8→9: 2회 꺾임 (3,6)에서 남쪽→서쪽
- 13→14: 3회 꺾임 (3,1)에서 서쪽→북쪽  
- 14→15: 4회 꺾임 (2,1)에서 북쪽→동쪽 (내부 진입)

총 꺾임: 4회
공식: k=3, M<N이므로 T = 2×3-1-1 = 4회 ✓

케이스 3: M = N (정사각형) - 4×4 표

실제 경로 결과:
[ 1][12][11][10]
[ 2][13][16][ 9]
[ 3][14][15][ 8]
[ 4][ 5][ 6][ 7]

꺾임 분석:
- 4→5: 1회 꺾임 (1,4)에서 동쪽→남쪽
- 7→8: 2회 꺾임 (4,4)에서 남쪽→서쪽
- 10→11: 3회 꺾임 (4,1)에서 서쪽→북쪽
- 12→13: 4회 꺾임 (2,1)에서 북쪽→동쪽 (내부 진입)
- 14→15: 5회 꺾임 (2,3)에서 동쪽→남쪽
- 15→16: 6회 꺾임 (3,3)에서 남쪽→서쪽

총 꺾임: 6회
공식: k=4, M=N이므로 T = 2×4-1-1 = 6회 ✓

4.5 레이어별 꺾임 패턴 정리

일반적인 패턴:

  • 외곽 레이어: 항상 정확히 2번 꺾임 (오른쪽 끝→아래, 아래 끝→왼쪽)
  • 중간 레이어들: 각각 2번씩 꺾임
  • 마지막 레이어:
    • M > N: 추가 꺾임 발생
    • M ≤ N: 추가 꺾임 없음

수학적 검증: $T = 2 \times (\text{완전 레이어 수}) + (\text{마지막 레이어 보정})$ $= 2 \times \min(M,N) - 1 - \mathbf{1}_{{M \leq N}}$

5. 다른 해결 방법

5.1 재귀적 접근법

def snail_recursive(M, N, layer=0):
    """재귀적으로 각 레이어의 꺾임 횟수 계산"""
    # 기저 조건: 더 이상 레이어가 없음
    if layer * 2 >= min(M, N):
        return 0
    
    # 현재 레이어의 실제 크기
    current_M = M - 2 * layer
    current_N = N - 2 * layer
    
    # 마지막 레이어 판단
    if current_M == 1 or current_N == 1:
        # 한 줄 또는 한 열만 남음 - 꺾임 없음
        if current_M == 1 and current_N == 1:
            return 0  # 점 하나
        elif current_M == 1:
            return 0  # 가로 한 줄
        else:
            return 1  # 세로 한 줄 - 마지막에 한 번 꺾임
    
    # 일반 레이어 - 2번 꺾임 + 다음 레이어
    return 2 + snail_recursive(M, N, layer + 1)

def snail_turns_recursive(M, N):
    return snail_recursive(M, N)

5.2 수학적 패턴 분석 접근법

def snail_pattern_analysis(M, N):
    """패턴 분석을 통한 직접 계산"""
    
    if M == 1 and N == 1:
        return 0
    
    if M == 1:  # 가로 한 줄
        return 0
    
    if N == 1:  # 세로 한 줄  
        return 0
    
    # 완전한 레이어 수
    complete_layers = min(M, N) // 2
    
    # 중앙 남은 부분
    remaining_M = M - 2 * complete_layers
    remaining_N = N - 2 * complete_layers
    
    # 완전한 레이어들의 꺾임: 각 레이어당 2번
    turns = complete_layers * 2
    
    # 중앙 남은 부분 처리
    if remaining_M > 0 and remaining_N > 0:
        if remaining_M == 1 and remaining_N == 1:
            # 중앙 점 하나 - 추가 꺾임 없음
            turns -= 1  # 마지막 레이어 진입 시 꺾임 제거
        elif remaining_M == 1:
            # 가로 한 줄 남음 - 꺾임 없음
            turns -= 1
        elif remaining_N == 1:
            # 세로 한 줄 남음 - 1번 꺾임
            turns -= 1  # 마지막 진입 꺾임만 남김
        else:
            # 2×2 이상 남음 - 재귀적으로 처리
            pass
    
    return max(0, turns)

5.3 동적 프로그래밍 접근법

def snail_dp(M, N):
    """메모이제이션을 사용한 동적 프로그래밍"""
    memo = {}
    
    def dp(m, n, visited_layers=0):
        if (m, n, visited_layers) in memo:
            return memo[(m, n, visited_layers)]
        
        # 기저 조건
        if m <= 0 or n <= 0:
            result = 0
        elif m == 1 and n == 1:
            result = 0
        elif m == 1:  # 가로 한 줄
            result = 0
        elif n == 1:  # 세로 한 줄
            result = 1 if visited_layers > 0 else 0
        else:
            # 외곽 레이어 처리 후 내부로 이동
            inner_m = m - 2
            inner_n = n - 2
            if inner_m > 0 and inner_n > 0:
                result = 2 + dp(inner_m, inner_n, visited_layers + 1)
            else:
                result = 1  # 마지막 꺾임
        
        memo[(m, n, visited_layers)] = result
        return result
    
    return dp(M, N)

5.4 기하학적 접근법

def snail_geometric(M, N):
    """기하학적 관점에서의 분석"""
    
    # 달팽이 경로를 동심원으로 해석
    # 각 원(레이어)은 직사각형 형태
    
    total_turns = 0
    current_M, current_N = M, N
    
    while current_M > 0 and current_N > 0:
        if current_M == 1 and current_N == 1:
            # 중심점 도달 - 더 이상 꺾임 없음
            break
        elif current_M == 1:
            # 가로 한 줄 - 꺾임 없이 직진
            break
        elif current_N == 1:
            # 세로 한 줄 - 마지막에 한 번만 꺾임
            total_turns += 1
            break
        else:
            # 완전한 사각형 레이어 - 2번 꺾임
            total_turns += 2
            current_M -= 2
            current_N -= 2
    
    return total_turns

6. 방법별 특징 비교

방법 시간복잡도 공간복잡도 가독성 적용범위
수학 공식 O(1) O(1) 낮음 모든 경우
시뮬레이션 O(MN) O(MN) 높음 모든 경우
재귀 O(min(M,N)) O(min(M,N)) 중간 모든 경우
DP O(min(M,N)) O(min(M,N)) 중간 복잡한 변형
기하학적 O(min(M,N)) O(1) 높음 기본 문제