탐색 알고리즘 DFS/BFS

DFS(Depth First Search), BFS(Breadth Fisrt Search)는 모두 탐색 알고리즘으로,
그래프를 탐색하는 과정에서 깊이를 우선으로 하는 지 (DFS),
너비를 우선으로 하는 지 (BFS)로 각 알고리즘의 특징을 나눌 수 있다.

1. DFS(Depth First Search) - 깊이 우선 탐색

DFS는 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘이다. 다음 그래프를 보고 DFS를 이해하여 보겠다.


위 그래프에서 시작 노드를 A로 하였을 때의 DFS 탐색 순서를 구해보고자 한다.
이때 스택을 활용하여 DFS 탐색 순서를 확인해 볼 수 있다.

* 노드를 스택에 넣는 행위를 방문한 것으로 정의 *

1. 시작 노드 A를 탐색한다(A를 방문한 것으로 정의), A의 인접 노드 B, C, D를 순서대로 스택에 push한다. // 탐색: A , 스택: B, C, D
2. 스택에서 pop을 진행하고 pop된 D를 탐색한다, D의 인접 노드 중 방문한 적이 없는 H를 스택에 push한다. // 탐색: A->D , 스택: B, C, H
3. pop 진행 H 탐색, H의 인접 노드 중 방문한 적이 없는 F, I를 push한다. // 탐색: A->D->H , 스택: B, C, F, I
4. pop 진행 I 탐색, I의 인접 노드 중 방문한 적이 없는 G를 push한다. // 탐색: A->D->H->I , 스택: B, C, F, G
5. pop 진행 G 탐색, G의 인접 노드 중 방문한 적이 없는 E를 push한다. // 탐색: A->D->H->I->G , 스택: B, C, F, E
6. pop 진행 E 탐색, E의 인접 노드 중 방문한 적이 없는 노드가 없음으로 push하지 않고 끝낸다. // 탐색: A->D->H->I->G->E , 스택: B, C, F
7. pop 진행 F 탐색, F의 인접 노드 중 방문한 적이 없는 노드가 없음으로 push하지 않고 끝낸다. // 탐색: A->D->H->I->G->E->F , 스택: B, C
8. pop 진행 C 탐색, C의 인접 노드 중 방문한 적이 없는 노드가 없음으로 push하지 않고 끝낸다. // 탐색: A->D->H->I->G->E->F->C , 스택: B
9. pop 진행 B 탐색, B의 인접 노드 중 방문한 적이 없는 노드가 없음으로 push하지 않고 끝낸다. // 탐색: A->D->H->I->G->E->F->C->B , 스택:
10. 스택이 비었기에  탐색을 종료한다.
” A->D->H->I->G->E->F->C->B “ 순서로 연결된 모든 노드들을 DFS 알고리즘에 맞추어 탐색하였다.

DFS를 실행할 때, 스택에 담기는 인접 노드의 순서에 따라 DFS의 탐색 결과는 다양한 경우가 나올 수 있음을 인지한다.



C++ 객체를 사용해 인접행렬을 기반으로 한 그래프에서 (위 그림 그래프 사용) DFS 메소드를 구현하면 아래와 같다.
이때 아래 코드는 스택이 아닌 재귀호출을 이용하여 DFS를 구현하였다.

#define MAX_VTXS 9

class AdjMatGraph{ // 인접 행렬 그래프
protected:
    int size;
    char vertices[MAX_VTXS];
    int adj[MAX_VTXS][MAX_VTXS];
public:
    AdjMatGraph(){reset();}
    void reset(){
        size = 0;
        for (int i = 0; i < MAX_VTXS; i++){
            for (int j = 0; j < MAX_VTXS; j++){
                setEdge(i, j, 0);
            }
        }
    }
    void setEdge(int i, int j, int val){ adj[i][j] = val; }
    int getEdge(int i, int j){ return adj[i][j]; }
    char getVertex(int i){ return vertices[i]; }
    bool isEmpty(){ return size == 0; }
    bool isFull(){ return size >= MAX_VTXS; }
    void insertVertex(char name){
        if(!isFull()) vertices[size++] = name;
        else cout << "error, isFull" << endl;
    }
    void insertEdge(int u, int v){
        setEdge(u, v, 1);
        setEdge(v, u, 1);
    }
};

class SrchAMGraph : public AdjMatGraph{ // 인접 행렬 그래프 상속, visited 추가
private:
    bool visited[MAX_VTXS];
public:
    void resetVisited(){ for (int i = 0; i < size; i++){ visited[i] = false; } }
    bool isLinked(int u, int v){ return getEdge(u, v) != 0; }
    void DFS(int v){ // DFS 구현 부분
        visited[v] = true;
        cout << getVertex(v) << " ";
        for (int w = 0; w < size; w++){
            if (isLinked(v, w) && visited[w] == false){
                DFS(w); // 재귀 호출
            }
        }
    }
};
main
    SrchAMGraph g;

    // 노드 추가
    g.insertVertex('A'); // 0
    g.insertVertex('B'); // 1
    g.insertVertex('C'); // 2
    g.insertVertex('D'); // 3
    g.insertVertex('E'); // 4
    g.insertVertex('F'); // 5
    g.insertVertex('G'); // 6
    g.insertVertex('H'); // 7
    g.insertVertex('I'); // 8
    g.insertVertex('J'); // error, isFull

    // 간선 연결
    g.insertEdge(0, 1);
    g.insertEdge(0, 2);
    g.insertEdge(0, 3);
    g.insertEdge(1, 4);
    g.insertEdge(2, 5);
    g.insertEdge(3, 7);
    g.insertEdge(4, 5);
    g.insertEdge(4, 6);
    g.insertEdge(5, 7);
    g.insertEdge(6, 8);
    g.insertEdge(7, 8);

    g.resetVisited();
    g.DFS(0);
    cout << endl;
    
    return 0;
output
error, isFull
A B E F C H D I G
왜 탐색 순서가 위에서와 다르나
  • 앞서 말했 듯이 DFS의 탐색 순서는 스택에 노드를 넣는 순서에 따라 다양하게 나올 수 있다.
  • DFS의 절대 목표는 결국 탐색이기에 연결된 모든 노드를 순회하는 것이 제대로 구현되었는 지가 중요하다.
  • 코드로 구현한 DFS 탐색 순서도 깊이를 우선으로 탐색하기에 DFS 결과 중 하나가 제대로 출력되었다고 볼 수 있다.




1. BFS(Breadth First Search) - 너비 우선 탐색

BFS는 그래프에서 가까운 부분을 우선적으로 탐색하는 알고리즘이다. DFS에서와 같은 그래프로 BFS를 이해하여 보겠다.


시작 노드를 A로 하였을 때의 BFS 탐색 순서를 구해보고자 한다.
이때 큐를 활용하여 BFS 탐색 순서를 확인해 볼 수 있다.

* 노드를 큐에 넣는 행위를 방문한 것으로 정의 *

1. 시작 노드 A를 탐색한다(A를 방문한 것으로 정의), A의 인접 노드 B, C, D를 순서대로 큐에 enqueue한다. // 탐색: A , 큐: B, C, D
2. 큐에서 dequeue를 진행하고 dequeue된 B를 탐색한다, B의 인접 노드 중 방문한 적이 없는 E를 큐에 enqueue한다. // 탐색: A->B , 큐: C, D, E
3. dequeue 진행 C 탐색, C의 인접 노드 중 방문한 적이 없는 F를 enqueue한다. // 탐색: A->B->C , 큐: D, E, F
4. dequeue 진행 D 탐색, D의 인접 노드 중 방문한 적이 없는 H를 enqueue한다. // 탐색: A->B->C->D , 큐: E, F, H 
5. dequeue 진행 E 탐색, E의 인접 노드 중 방문한 적이 없는 G를 enqueue한다. // 탐색: A->B->C->D->E , 큐: F, H, G
6. dequeue 진행 F 탐색, F의 인접 노드 중 방문한 적이 없는 노드가 없음으로 enqueue하지 않고 끝낸다. // 탐색: A->B->C->D->E->F , 큐: H, G
7. dequeue 진행 H 탐색, H의 인접 노드 중 방문한 적이 없는 I를 enqueue한다. // 탐색: A->B->C->D->E->F->H , 큐: G, I
8. dequeue 진행 G 탐색, G의 인접 노드 중 방문한 적이 없는 노드가 없음으로 enqueue하지 않고 끝낸다. // 탐색: A->B->C->D->E->F->H->G , 큐: I
9. dequeue 진행 I 탐색, I의 인접 노드 중 방문한 적이 없는 노드가 없음으로 enqueue하지 않고 끝낸다. // 탐색: A->B->C->D->E->F->H->G->I , 큐: 
10. 큐가 비었기에  탐색을 종료한다.
” A->B->C->D->E->F->H->G->I “ 순서로 연결된 모든 노드들을 BFS 알고리즘에 맞추어 탐색하였다.

BFS 역시 DFS와 마찬가지로 큐에 담기는 인접 노드의 순서에 따라 다양한 탐색 결과가 나올 수 있다.



C++ 객체를 사용해 인접 리스트를 기반으로 한 그래프에서 (위 그림 그래프 사용) BFS 메소드를 구현하면 아래와 같다.

#define MAX_VTXS 9
#define PUSH 1
#define POP 2

class Queue {
private:
    int front;
    int rear;
    int lastOp;
    int data[MAX_VTXS];
public:
    Queue() {
        front = 0;
        rear = 0;
        lastOp = 0;
    }
    ~Queue() {}
    int isEmpty() {
        if (front == rear && lastOp != PUSH) {
            return 1;
        } else return 0;
    }
    bool isFull() {
        if (front == rear && lastOp == PUSH) return 1;
        else return 0;
    }
    void enqueue(int i) {
        if (!isFull()) {
            rear = (rear + 1) % MAX_VTXS;
            this->data[rear] = i;
            lastOp = PUSH;
        }
    }
    int dequeue() {
        if (!isEmpty()) {
            lastOp = POP;
            front = (front + 1) % MAX_VTXS;
            return data[front];
        }
        return 0;
    }
    void printQ() {
        int numberOfQ;
        numberOfQ = rear > front ? rear - front : (rear + MAX_VTXS) - front;
        int index = front + 1;
        for (int i = 0; i < numberOfQ; i++) {
            cout << data[(index + i) % MAX_VTXS] << " ";
        }
        cout << endl;
    }
};

class Node {
protected:
    int id;
    Node* link;
public:
    Node(int i, Node* l = nullptr) {
        id = i;
        link = l;
    }
    ~Node() { if (link != nullptr) delete link; }
    int getId() { return id; }
    Node* getLink() { return link; }
    void setLink(Node* l) { link = l; }
};

class AdjListGraph {
protected:
    int size;
    char vertices[MAX_VTXS];
    Node* adj[MAX_VTXS];
    bool visited[MAX_VTXS];
public:
    AdjListGraph() { size = 0; }
    ~AdjListGraph() { reset(); }
    void reset() {
        for (int i = 0; i < size; i++) {
            if (adj[i] != nullptr) delete adj[i];
        }
    }
    void insertVertex(char val) {
        if (!isFull()) {
            vertices[size] = val;
            adj[size++] = nullptr;
        } else cout << "error, isFull" << endl;
    }
    char getVertex(int v) { return vertices[v]; }
    void insertEdge(int u, int v) {
        adj[u] = new Node(v, adj[u]);
        adj[v] = new Node(u, adj[v]);
    }
    Node* adjacent(int v) { return adj[v]; }
    bool isFull() { return size >= MAX_VTXS; }
    bool isEmpty() { return size == 0; }
    bool isLinked(int u, int v) {
        Node* temp = adj[u];
        while (temp != nullptr) {
            if (temp->getId() == v) {
                return true;
            }
            temp = temp->getLink();
        }
        return false;
    }
    void BFS(int v) {
        resetVisited();
        visited[v] = true;
        cout << getVertex(v) << " ";
        Queue q;
        q.enqueue(v);
        while (!q.isEmpty()) {
            int u = q.dequeue();
            Node* temp = adj[u];
            while (temp != nullptr) {
                int w = temp->getId();
                if (!visited[w]) {
                    visited[w] = true;
                    cout << getVertex(w) << " ";
                    q.enqueue(w);
                }
                temp = temp->getLink();
            }
        }
    }
    void resetVisited() {
        for (int i = 0; i < size; i++) {
            visited[i] = false;
        }
    }
};
main
    AdjListGraph g;
    g.insertVertex('A');
    g.insertVertex('B');
    g.insertVertex('C');
    g.insertVertex('D');
    g.insertVertex('E');
    g.insertVertex('F');
    g.insertVertex('G');
    g.insertVertex('H');
    g.insertVertex('I');

    g.insertEdge(0, 1); // A-B
    g.insertEdge(0, 2); // A-C
    g.insertEdge(0, 3); // A-D
    g.insertEdge(1, 4); // B-E
    g.insertEdge(2, 5); // C-F
    g.insertEdge(3, 7); // D-H
    g.insertEdge(4, 5); // E-F 
    g.insertEdge(4, 6); // E-G
    g.insertEdge(5, 7); // F-H
    g.insertEdge(6, 8); // G-I
    g.insertEdge(7, 8); // H-I

    cout << "BFS: ";
    g.resetVisited();
    g.BFS(0);
    cout << endl;

    return 0;
output
BFS: A D C B H F E I G
DFS에서와 같은 이유로 다른 탐색 순서
  • 큐에 담기는 순서에 따라 다른 탐색 순서가 나온 것
  • A , DCB , HFE, IG 순서로 너비 우선 탐색이 제대로 적용되었다.




정리,
DFS는 깊이 우선 탐색으로 스택(Stack), 재귀호출을 이용해 구현할 수 있다.
BFS는 너비 우선 탐색으로 큐(Queue)를 이용하여 구현할 수 있다.
DFS와 BFS 알고리즘은 다양한 문제를 해결하는데 도움을 준다. (ex. 서로 연결되어 있는 묶음 찾기)