BFS: 두 판 사이의 차이

youngwiki
20번째 줄: 20번째 줄:
또한 BFS는 FIFO queue를 이용하므로, 먼저 발견된 노드를 먼저 처리해야 “너비 우선” 순서가 유지된다. 이때 새로운 정점이 발견되면 discovered[y] = true로 표시하고 큐에 추가한다. 즉, queue는 처리할 정점들이 저장되는 to-do list와 같은 존재이다.  
또한 BFS는 FIFO queue를 이용하므로, 먼저 발견된 노드를 먼저 처리해야 “너비 우선” 순서가 유지된다. 이때 새로운 정점이 발견되면 discovered[y] = true로 표시하고 큐에 추가한다. 즉, queue는 처리할 정점들이 저장되는 to-do list와 같은 존재이다.  


===Initializing BFS===
===BFS Code===
아래는 탐색을 시작하기 전에 배열을 초기화하는 함수이다.
아래는 탐색을 시작하기 전에 배열을 초기화하는 함수이다.
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
96번째 줄: 96번째 줄:


===Shortest Paths and BFS===
===Shortest Paths and BFS===
BFS는 간선 가중치가 모두 동일할 때, 시작 정점으로부터의 최단(간선 수 최소) 경로를 자동으로 보장한다. 이는 BFS가 레이어 순서대로 확장하기 때문이다. 거리 0(시작) → 거리 1 → 거리 2 → … 순서로 큐에서 꺼내 처리하기 때문에, 발견 트리에서 루트 내의 임의의 정점 x 로 내려가는 유일한 트리 경로는, 가능한 경로 중 간선 수가 최소이다. 이는 figure에서 알 수 있듯이 어떤 그래프가 주어졌을 때 parent[] 배열을 이용하여 발견 트리를 구성할 수 있으며, 이때 발견 트리의 어떤 정점에 대한 깊이는 루트에서의 해당 정점까지의 경로의 길이에 비례하기 때문이다.  
BFS는 간선 가중치가 모두 동일할 때, 시작 정점으로부터의 최단(간선 수 최소) 경로를 자동으로 보장한다. 이는 BFS가 레이어 순서대로 확장하기 때문이다. 거리 0(시작) → 거리 1 → 거리 2 → … 순서로 큐에서 꺼내 처리하기 때문에, 발견 트리에서 루트 내의 임의의 정점 x 로 내려가는 유일한 트리 경로는, 가능한 경로 중 간선 수가 최소이다. 이는 figure에서 알 수 있듯이 어떤 그래프가 주어졌을 때 parent[] 배열을 이용하여 발견 트리를 구성할 수 있으며, 이때 발견 트리의 어떤 정점에 대한 깊이는 루트에서의 해당 정점까지의 경로의 길이에 비례하기 때문이다.


==Applications of BFS==
==Applications of BFS==

2025년 10월 16일 (목) 03:27 판

상위 문서: 알고리즘 설계와 분석

개요

BFS(Breadth-First Traversal) 알고리즘의 핵심은 그래프를 체계적으로(순서 있게) 탐색하는 것이다. BFS는 모든 vertex와 edge를 한 번씩 방문한다. 이는 아래와 같은 규칙을 따른다.

  • “Breadth-First”는 탐색 시 가까운 노드(이웃)를 먼저 방문하고, 그 후에 더 멀리 있는 노드를 탐색하는 방식이다.
    • 예를 들어, 루트 노드로부터 거리 1인 정점들을 모두 방문한 뒤, 그 다음으로 거리가 2인 정점, 3인 정점...을 방문하는 방식이다.
  • BFS는 queue(큐)를 이용해 이 순서를 보장한다.

이에 따라 BFS는 특히 unweighted 그래프에서 최단 경로(shortest path)를 찾는 데 적합하다. 이는 DFS(깊이 우선 탐색)와 달리, BFS는 각 vertex까지의 최단 거리(간선 개수 기준)를 자연스럽게 구할 수 있기 때문이다.

Implementing BFS

Data Structures for BFS

BFS를 구현하기 위해 필요한 자료구조는 3가지이다:

bool processed[MAXV+1];   // 이미 완전히 처리된 정점 
bool discovered[MAXV+1];  // 방문은 했지만 아직 다 보지 않은 정점 
int parent[MAXV+1];       // 해당 정점을 방문하기 전의 부모 노드

또한 BFS는 FIFO queue를 이용하므로, 먼저 발견된 노드를 먼저 처리해야 “너비 우선” 순서가 유지된다. 이때 새로운 정점이 발견되면 discovered[y] = true로 표시하고 큐에 추가한다. 즉, queue는 처리할 정점들이 저장되는 to-do list와 같은 존재이다.

BFS Code

아래는 탐색을 시작하기 전에 배열을 초기화하는 함수이다.

void bfs(graph *g, int start) {
    queue q;           /* 방문할 정점들을 담는 큐 */
    int v;             /* 현재 방문 중인 정점 */
    int y;             /* 인접(후속) 정점 */
    edgenode *p;       /* 인접 리스트를 순회하기 위한 포인터 */

    /* 초기화 단계 */
    init_queue(&q);            // 큐 초기화
    enqueue(&q, start);        // 시작 정점을 큐에 넣기
    discovered[start] = true;  // 시작 정점을 "발견됨"으로 표시

    /* BFS 메인 루프 */
    while (!empty_queue(&q)) {
        v = dequeue(&q);           // 큐에서 정점을 하나 꺼냄
        process_vertex_early(v);   // 방문 직후 처리 (예: 출력)
        processed[v] = true;       // 완전히 처리된 정점으로 표시

        /* 인접 정점 순회 */
        p = g->edges[v];           // v의 인접 리스트의 첫 노드
        while (p != NULL) {
            y = p->y;              // 인접 정점 번호

            /* 간선 (v, y) 처리 */
            if ((!processed[y]) || g->directed) {
                process_edge(v, y); // 간선 처리 (예: 출력, 트리 생성 등)
            }

            /* 새로운 정점 발견 시 */
            if (!discovered[y]) {
                enqueue(&q, y);          // 큐에 추가
                discovered[y] = true;    // 발견됨 표시
                parent[y] = v;           // BFS 트리 상의 부모 설정
            }

            p = p->next;                 // 다음 인접 정점으로 이동
        }

        process_vertex_late(v);          // 정점 처리 완료 후 작업
    }
}

Exploiting Traversal

BFS 알고리즘은 그래프의 각 정점과 간선을 정확히 한번씩 방문하며, 이때 훅(hook) 함수들을 끼워 넣으면, 원하는 일을 자동으로 수행할 수 있다. 위에서 정의된 BFS() 함수에서의:

  • process_vertex_early(int v): 정점 v를 꺼냈을 때 (BFS면 큐에서 dequeue 직후) 가장 먼저 호출
    • 정점 방문 순서 출력, 레벨(거리) 기록, 통계 수집, 색칠 등의 작업을 실행
  • process_edge(int x, int y): 정점 x의 인접리스트를 훑는 각 간선 (x, y)를 만날 때마다 호출
    • 간선 방문 순서 출력, 선 개수 세기, 사이클 탐지 등을 함

이때 방문 순서를 출력할 경우 각 정점, 간선들은 정확히 한번 씩 출력되므로, 해당 함수들을 단순히 printf로 채우면 방문된 모든 정점과 간선이 중복 없이 출력된다. 하지만 undirected 그래프에서는 간선 (u, v)가 (v, u)와 같이 해석될 수 있으므로 “간선을 한 번만” 처리하려면 보통

if (!processed[y] || g->directed) process_edge(x, y);

와 같은 제약을 사용한다. 이는 위의 BFS() 함수 구현에서도 구현되어 있다.

Finding Paths

BFS는 정점을 처음 발견했을 때 “누가 이 정점을 발견했는지”를 parent[] 배열에 기록한다:

  • parent[i] = 정점 i를 처음 큐에 넣게 만든 “직전의” 정점(발견자).
    • 시작 정점 s는 보통 parent[s] = -1로 표시함.
  • 모든 parent 화살표를 모으면, 시작 정점을 루트 노드로 하는 “발견 트리(discovery tree)”가 생김
    • 이 트리는 탐색 순서를 보존함. 즉, 어떤 정점이 어떤 정점을 통해 처음 도달되었는지를 담고 있어, 경로 복원 등에 사용됨.

아래는 parent[]를 이용해 끝점에서 시작점으로 거꾸로 따라가면 경로를 복원하는 함수이다:

void find_path(int start, int end, int parents[]) {
    if ((start == end) || (end == -1)) { //루트 노드에 도달
          printf("\n%d", start);
    } else {
        find_path(start, parents[end], parents); //end의 부모 쪽으로 한 칸 올라가서 해당 경로 출력
        printf(" %d", end);
    }
}

Shortest Paths and BFS

BFS는 간선 가중치가 모두 동일할 때, 시작 정점으로부터의 최단(간선 수 최소) 경로를 자동으로 보장한다. 이는 BFS가 레이어 순서대로 확장하기 때문이다. 거리 0(시작) → 거리 1 → 거리 2 → … 순서로 큐에서 꺼내 처리하기 때문에, 발견 트리에서 루트 내의 임의의 정점 x 로 내려가는 유일한 트리 경로는, 가능한 경로 중 간선 수가 최소이다. 이는 figure에서 알 수 있듯이 어떤 그래프가 주어졌을 때 parent[] 배열을 이용하여 발견 트리를 구성할 수 있으며, 이때 발견 트리의 어떤 정점에 대한 깊이는 루트에서의 해당 정점까지의 경로의 길이에 비례하기 때문이다.

Applications of BFS

BFS 알고리즘은 그 특성 덕분에 단순한 탐색을 넘어 더 다양한 분야에 사용된다.

Connected components

Connected components는 undirected 그래프를 여러 조각으로 나눈 것으로, 같은 조각 안의 임의의 두 정점 사이에는 경로가 있고, 서로 다른 조각 사이에는 경로가 없다. 이때 어떤 그래프가 주어졌을 때, 해당 그래프 내의 모든 조각들을 찾아내는 문제에 대해 BFS는 좋은 해결책이 된다. 이는 임의의 시작점에서 BFS를 한 번 돌리면 그 시작점과 같은 연결 요소에 있는 모든 정점이 발견되기 때문이다. 또한 아직 안 발견된 정점이 남아 있으면, 거기서 다시 BFS를 시작해 다음 연결 요소를 찾아서 문제를 해결할 수 있다. 이는 이를 구현하는 코드이다:

void connected_components(graph *g) {
    int c = 0;   // 조각의 숫자를 계산하는 카운터
    int i;

    initialize_search(g);   // discovered[], processed[], parent[] 등 초기화

    /* 그래프의 모든 정점을 순회 */
    for (i = 1; i <= g->nvertices; i++) {
        if (!discovered[i]) {           // 아직 방문되지 않은 정점이라면
            c++;                  // 새로운 연결 요소 발견 → 카운터 증가
            printf("Component %d:", c); // 몇 번째 조각인지 출력
            bfs(g, i);                  // 해당 정점부터 BFS 탐색 수행
            printf("\n");               // 한 컴포넌트 출력 후 줄바꿈
        }
    }
}
void process_vertex_early(int v) {
    printf(" %d", v);
}
void process_edge(int x, int y) {}

Two-Coloring Graphs

Two-Coloring Graphs 문제는 인접한 두 정점이 같은 색이 되지 않도록 색을 배정하는 문제이다. 또한 단 두 색으로도 충돌 없이 색칠 가능한 그래프를 이분 그래프(bipartite)라고 한다. 이는 아래와 같은 코드를 통해 해결할 수 있다:

void twocolor(graph *g) {
    int i;
    /* 모든 정점을 미색칠 상태로 초기화 */
    for (i = 1; i <= g->nvertices; i++) {
        color[i] = UNCOLORED;
    }

    bipartite = true;          // 이분 그래프라고 가정하고 시작
    initialize_search(g);      // BFS용 초기화 (discovered, processed 등)

    /* 모든 정점에 대해 BFS 수행 (연결되지 않은 그래프 대비) */
    for (i = 1; i <= g->nvertices; i++) {
        if (!discovered[i]) {  // 아직 방문하지 않은 정점이라면
            color[i] = WHITE;  // 시작 색은 WHITE로 설정
            bfs(g, i);         // BFS 탐색 시작
        }
    }
}
void process_edge(int x, int y) {
    if (color[x] == color[y]) {   // 같은 색이면 이분 그래프 아님
        bipartite = false;
        printf("Warning: not bipartite, due to (%d,%d)\n", x, y);
    }
    color[y] = complement(color[x]);  // y를 x의 반대 색으로 칠하기
}
int complement(int color) { //현재 색의 반대색 반환 함수
    if (color == WHITE) {
        return BLACK;
    } else if (color == BLACK) {
        return WHITE;
    } else return UNCOLORED;
}

각주