정재익 2025. 7. 30. 20:39
package org.example;

import java.util.*;

//BFS 알고리즘 기본 개념
//**BFS(Breadth-First Search, 너비 우선 탐색)**는 그래프나 트리를 탐색하는 알고리즘입니다. 시작점에서 가까운 노드부터 차례대로 탐색하며, 마치 물결이 퍼져나가듯이 동심원 형태로 탐색합니다.
//시간복잡도: O(V + E) - V는 정점 수, E는 간선 수
//공간복잡도: O(V) - 큐와 방문 배열에 필요한 공간
//먼저 알아야 할 기본 개념:
//
//큐(Queue) 자료구조: FIFO(First In First Out) 원리
//그래프 표현 방법: 인접 리스트, 인접 행렬
//방문 체크의 중요성
//
//코딩테스트에서 BFS가 사용되는 문제 유형:
//
//최단 거리 문제: 미로 탈출, 최소 이동 횟수
//레벨별 탐색: 각 깊이별로 처리해야 하는 문제
//연결된 구역 찾기: 섬의 개수, 연결 요소
//상태 공간 탐색: 게임 상태 변화, 퍼즐 해결
/**
 * BFS(너비 우선 탐색) 교육용 예제
 *
 * 이 예제는 2D 격자에서 시작점(S)에서 목표점(G)까지의 최단 경로를 찾는 문제입니다.
 * 0: 이동 가능한 길, 1: 벽(이동 불가), S: 시작점, G: 목표점
 */
public class BFS {

    // 상하좌우 이동을 위한 방향 벡터
    // 이것은 BFS에서 매우 중요한 개념입니다!
    // dx[0], dy[0] = 상(-1, 0), dx[1], dy[1] = 하(1, 0)
    // dx[2], dy[2] = 좌(0, -1), dx[3], dy[3] = 우(0, 1)
    static int[] dx = {-1, 1, 0, 0};  // 행 이동 (상, 하, 좌, 우)
    static int[] dy = {0, 0, -1, 1};  // 열 이동 (상, 하, 좌, 우)

    // 격자의 크기
    static int rows = 5;
    static int cols = 5;

    // 예제 격자 (직접 입력된 데이터)
    // S = 시작점, G = 목표점, 0 = 길, 1 = 벽
    static char[][] grid = {
            {'S', '0', '1', '0', '0'},
            {'0', '0', '1', '0', '1'},
            {'0', '1', '0', '0', '0'},
            {'0', '0', '0', '1', 'G'},
            {'1', '0', '0', '0', '0'}
    };

    // 각 위치까지의 최단 거리를 저장하는 배열
    // -1로 초기화하여 아직 방문하지 않은 곳을 표시
    static int[][] distance;

    // BFS에서 사용할 위치 정보를 담는 클래스
    // 왜 클래스를 만들까? 큐에 (행, 열) 정보를 함께 저장하기 위해서입니다!
    static class Position {
        int x, y;  // x는 행(row), y는 열(column)

        Position(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public static void main(String[] args) {
        System.out.println("=== BFS 알고리즘 교육용 예제 ===\n");

        // 초기 격자 출력
        System.out.println("초기 격자 상태:");
        printGrid();

        // BFS 실행 버튼 역할 - 이 메소드만 호출하면 됩니다!
        int result = runBFS();

        // 결과 출력
        System.out.println("\n=== 결과 ===");
        if (result != -1) {
            System.out.println("최단 거리: " + result + "칸");
            System.out.println("\n각 위치까지의 거리 정보:");
            printDistance();
        } else {
            System.out.println("목표점에 도달할 수 없습니다!");
        }
    }

    /**
     * BFS 알고리즘의 핵심 구현부
     * 이 메소드가 BFS의 모든 것을 담고 있습니다!
     */
    public static int runBFS() {
        System.out.println("\n=== BFS 탐색 시작 ===");

        // 1단계: 초기화
        // distance 배열을 -1로 초기화 (아직 방문하지 않음을 의미)
        distance = new int[rows][cols];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                distance[i][j] = -1;  // -1은 아직 방문하지 않은 상태
            }
        }

        // 2단계: 시작점과 목표점 찾기
        int startX = -1, startY = -1;  // 시작점 좌표
        int goalX = -1, goalY = -1;    // 목표점 좌표

        // 격자를 전체 탐색하여 S와 G의 위치를 찾습니다
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == 'S') {
                    startX = i;
                    startY = j;
                } else if (grid[i][j] == 'G') {
                    goalX = i;
                    goalY = j;
                }
            }
        }

        System.out.println("시작점: (" + startX + ", " + startY + ")");
        System.out.println("목표점: (" + goalX + ", " + goalY + ")");

        // 3단계: BFS의 핵심! 큐 생성 및 시작점 추가
        // 큐를 사용하는 이유: FIFO 방식으로 가까운 거리부터 탐색하기 위해
        Queue<Position> queue = new LinkedList<>();

        // 시작점을 큐에 추가하고 거리를 0으로 설정
        queue.offer(new Position(startX, startY));
        distance[startX][startY] = 0;  // 시작점까지의 거리는 0

        System.out.println("\nBFS 탐색 과정:");
        int step = 0;  // 진행 단계를 표시하기 위한 변수

        // 4단계: BFS 메인 루프
        // 큐가 비어있지 않은 동안 계속 탐색합니다
        while (!queue.isEmpty()) {
            step++;
            System.out.println("\n--- 단계 " + step + " ---");

            // 큐에서 현재 위치를 꺼냅니다 (FIFO)
            // 이것이 BFS의 핵심! 먼저 들어간 것이 먼저 나옵니다
            Position current = queue.poll();
            int currentX = current.x;
            int currentY = current.y;

            System.out.println("현재 탐색 위치: (" + currentX + ", " + currentY +
                    ") - 거리: " + distance[currentX][currentY]);

            // 목표점에 도착했는지 확인
            if (currentX == goalX && currentY == goalY) {
                System.out.println("목표점 도달! 최단 거리: " + distance[currentX][currentY]);
                return distance[currentX][currentY];
            }

            // 5단계: 4방향 탐색 (상하좌우)
            // 현재 위치에서 상하좌우로 이동할 수 있는지 확인합니다
            for (int dir = 0; dir < 4; dir++) {
                // 새로운 위치 계산
                // 왜 dx, dy를 사용할까? 상하좌우 이동을 간단하게 표현하기 위해!
                int nextX = currentX + dx[dir];
                int nextY = currentY + dy[dir];

                System.out.println("  방향 " + getDirectionName(dir) + " 체크: (" +
                        nextX + ", " + nextY + ")");

                // 6단계: 유효성 검사 (이 부분이 매우 중요합니다!)
                // 다음 위치가 유효한지 확인하는 조건들:

                // 조건 1: 격자 범위 내에 있는가?
                if (nextX < 0 || nextX >= rows || nextY < 0 || nextY >= cols) {
                    System.out.println("    -> 격자 범위를 벗어남");
                    continue;  // 다음 방향으로 넘어감
                }

                // 조건 2: 벽이 아닌가?
                if (grid[nextX][nextY] == '1') {
                    System.out.println("    -> 벽이므로 이동 불가");
                    continue;
                }

                // 조건 3: 이미 방문했는가?
                // distance[nextX][nextY] != -1 이면 이미 방문한 곳
                // BFS에서는 처음 방문할 때가 항상 최단 거리이므로 재방문 불필요!
                if (distance[nextX][nextY] != -1) {
                    System.out.println("    -> 이미 방문한 위치");
                    continue;
                }

                // 7단계: 새로운 위치를 큐에 추가하고 거리 업데이트
                // 모든 조건을 통과했다면 이 위치는 탐색 가능합니다!
                distance[nextX][nextY] = distance[currentX][currentY] + 1;
                queue.offer(new Position(nextX, nextY));

                System.out.println("    -> 큐에 추가! 거리: " + distance[nextX][nextY]);
            }

            // 현재 큐 상태 출력 (학습 목적)
            System.out.println("  현재 큐 크기: " + queue.size());
        }

        // 큐가 비었는데도 목표점에 도달하지 못한 경우
        System.out.println("\n큐가 비었습니다. 목표점에 도달할 수 없습니다.");
        return -1;  // 도달 불가능
    }

    /**
     * 방향을 문자열로 변환하는 헬퍼 메소드
     * 0: 상, 1: 하, 2: 좌, 3: 우
     */
    private static String getDirectionName(int dir) {
        switch (dir) {
            case 0: return "상";
            case 1: return "하";
            case 2: return "좌";
            case 3: return "우";
            default: return "알 수 없음";
        }
    }

    /**
     * 현재 격자 상태를 출력하는 메소드
     */
    private static void printGrid() {
        System.out.println("격자 범례: S=시작점, G=목표점, 0=길, 1=벽");
        System.out.println();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                System.out.print("[" + grid[i][j] + "] ");
            }
            System.out.println();
        }
    }

    /**
     * 각 위치까지의 거리 정보를 출력하는 메소드
     * -1: 도달 불가능, 0 이상: 시작점으로부터의 거리
     */
    private static void printDistance() {
        System.out.println("거리 정보 (-1: 도달불가, 0 이상: 거리):");
        System.out.println();

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (distance[i][j] == -1) {
                    System.out.print("[-1] ");
                } else {
                    System.out.printf("[%2d] ", distance[i][j]);
                }
            }
            System.out.println();
        }
    }
}

/*
=== BFS 알고리즘의 핵심 원리 요약 ===

1. 큐(Queue) 사용: FIFO 방식으로 가까운 거리부터 탐색
   - 왜 큐를 사용할까? 동심원 형태로 퍼져나가는 탐색을 위해

2. 방문 체크: 같은 곳을 두 번 방문하지 않음
   - distance 배열이 방문 체크와 거리 저장을 동시에 담당

3. 4방향 탐색: dx, dy 배열을 사용한 상하좌우 이동
   - 이것은 2D 격자 문제에서 표준적인 기법

4. 최단 거리 보장: BFS의 특성상 처음 도달한 경로가 최단 경로
   - 이것이 DFS와 가장 큰 차이점!

=== 시간복잡도 분석 ===
- 각 칸을 최대 한 번씩만 방문: O(rows × cols)
- 각 칸에서 4방향 체크: O(4) = O(1)
- 전체 시간복잡도: O(rows × cols)

=== 공간복잡도 분석 ===
- 큐 최대 크기: O(rows × cols)
- distance 배열: O(rows × cols)
- 전체 공간복잡도: O(rows × cols)

=== 코딩테스트에서 BFS 문제 해결 팁 ===

1. 최단 거리/최소 횟수 키워드가 나오면 BFS 고려
2. 레벨별 처리가 필요하면 큐 크기를 이용한 레벨 구분
3. 시작점이 여러 개인 경우 모든 시작점을 큐에 먼저 추가
4. 상태를 저장해야 하는 경우 Position 클래스를 확장
5. 방문 체크는 큐에 추가할 때 즉시! (중복 방지)

이 예제를 완전히 이해했다면 미로 탈출, 토마토, 나이트의 이동 등
다양한 BFS 문제를 해결할 수 있습니다!
*/