Home > Algorithm > BOJ > [BOJ] 1260번 DFS와 BFS

[BOJ] 1260번 DFS와 BFS
C++ 백준 BOJ 알고리즘 Algorithm

🔗 문제

https://www.acmicpc.net/problem/1260


📌 문제 요약

그래프를 DFS로 탐색한 결과와 BFS로 탐색한 결과를 출력하는 프로그램을 작성하시오. 단, 방문할 수 있는 정점이 여러 개인 경우에는 정점 번호가 작은 것을 먼저 방문하고, 더 이상 방문할 수 있는 점이 없는 경우 종료한다. 정점 번호는 1번부터 N번까지이다.

입력: 첫째 줄에 정점의 개수 N(1 ≤ N ≤ 1,000), 간선의 개수 M(1 ≤ M ≤ 10,000), 탐색을 시작할 정점의 번호 V가 주어진다. 다음 M개의 줄에는 간선이 연결하는 두 정점의 번호가 주어진다. 어떤 두 정점 사이에 여러 개의 간선이 있을 수 있다. 입력으로 주어지는 간선은 양방향이다
출력: 첫째 줄에 DFS를 수행한 결과를, 그 다음 줄에는 BFS를 수행한 결과를 출력한다. V부터 방문된 점을 순서대로 출력하면 된다.


💡 접근 방법

문제 유형: 그래프 탐색 (DFS / BFS 기본기)

그래프 탐색의 교과서 같은 문제다. 알고리즘 자체보다 “정점 번호가 작은 것부터 방문”
이라는 조건을 어떻게 보장할 것인가가 실제 구현 포인트다.

여기서 자료구조 선택이 갈린다.

  • vector<vector<int>>로 인접 리스트를 만들면 → 간선을 다 넣은 뒤 각 리스트를 sort 해야 한다
  • set<int>으로 인접 관계를 담으면 → 넣는 순간 이미 오름차순이고 중복 간선(“어떤 두 정점 사이에 여러 개의 간선이 있을 수 있다”)도 알아서 걸러진다

문제가 중복 간선을 명시적으로 허용하고 있어서, 정렬과 중복 제거를 한 번에 해결하는
set 쪽을 골랐다. 그래서 set을 이용해 노드 구조체를 임시로 만들었고,
map을 이용해 그래프를 설계 했다.

예제 그래프

⚠️ 처음에 했던 실수에서 쓴 입력(4 4 1 / 1 2 / 1 3 / 2 3 / 3 4)을 그림으로 보면 이렇다.

  간선: 1-2, 1-3, 2-3, 3-4        시작 정점 V = 1

        (1)
       ╱   ╲           1은 2, 3과 이어져 있다
     (2)───(3)
              ╲          3은 4와 이어져 있다
              (4)

  DFS: 1 → 2 → 3 → 4      (더 깊이 갈 수 있으면 계속 내려간다)
       1의 이웃 {2,3} 중 작은 2로 내려감
       2의 이웃 {1,3} 중 미방문 3으로 내려감
       3의 이웃 {1,2,4} 중 미방문 4로 내려감

  BFS: 1 → 2 → 3 → 4      (거리가 가까운 것부터 훑는다)
       1에서 갈 수 있는 2,3을 먼저 큐에 넣고
       그다음 층인 4를 넣는다

이 케이스는 DFS와 BFS 결과가 우연히 같아서, 방문 처리 타이밍이 틀려도 출력이 맞아 보일 수 있다.
아래 실수 항목이 정확히 그 함정이었다.


⚠️ 처음에 했던 실수

4 4 1
1 2
1 3
2 3
3 4

bfs 조건 체크 타이밍을 잘못 두어 에러가 났던 테스트케이스이다.

원인은 BFS에서 방문 표시를 “큐에서 꺼낼 때” 했던 것이다.
BFS는 큐에 넣는 순간 방문 처리를 해야 한다. 꺼낼 때 처리하면
아직 큐에 들어있고 아직 꺼내지지 않은 정점을 “미방문”으로 보고 한 번 더 집어넣게 된다.

위 그래프에서 1을 꺼내 2와 3을 넣고, 2를 꺼내 이웃을 보면 3이 아직 “미방문”이라
3이 큐에 두 번 들어가서 1 2 3 3 4 같은 출력이 나온다.
그래서 아래 코드에선 target[index] = i 바로 앞에서 graph[i].setBFS()를 호출한다.


💻 코드

#include <iostream>
#include <map>
#include <set>
using namespace std;

int N, M, V;

struct Node {
public:
  void push(int n) { lines.emplace(n); }
  bool isContain(int n) { return lines.find(n) != lines.end(); }
  void setDFS() { isDFS = true; }
  bool getDFS() { return isDFS; }
  void setBFS() { isBFS = true; }
  bool getBFS() { return isBFS; }

private:
  set<int> lines;
  bool isDFS = false, isBFS = false;
};

map<int, Node> graph;

void dfs(int root) {
  cout << root;
  graph[root].setDFS();   // 들어오자마자 방문 처리 (재귀 무한루프 방지)

  // 1번부터 훑으므로 "번호가 작은 정점 먼저" 조건이 자연스럽게 지켜진다
  for (int i = 1; i <= N; ++i) {
    if (graph[root].isContain(i) == false)
      continue;           // root와 i 사이에 간선이 없음
    if (graph[i].getDFS())
      continue;           // 이미 방문한 정점

    cout << " ";
    dfs(i);               // 더 깊이 내려간다
  }
  return;
}

void bfs(int root) {
  // queue 대신 배열을 큐처럼 쓴다.
  // 정점 번호가 1부터라서 0을 "비어있음" 표시로 쓸 수 있다.
  int target[1001] = {0};
  target[0] = root;
  int cnt = 0;      // 큐에서 꺼낼 위치 (front)
  int index = 1;    // 큐에 넣을 위치 (rear)

  while (target[cnt] != 0) {
    cout << target[cnt];
    graph[target[cnt]].setBFS();

    for (int i = 1; i <= N; ++i) {
      if (graph[target[cnt]].isContain(i) == false)
        continue;   // 간선 없음
      if (graph[i].getBFS())
        continue;   // 이미 큐에 넣었거나 방문한 정점
      if (index >= N)
        break;      // 큐가 가득 참 = 모든 정점을 이미 담았다

      // ★ 핵심: 꺼낼 때가 아니라 '넣을 때' 방문 처리한다.
      //   여기서 표시하지 않으면 같은 정점이 큐에 중복으로 들어간다.
      graph[i].setBFS();
      target[index] = i;
      index++;
    }
    cnt++;

    // 다음에 꺼낼 자리가 비었으면 = 큐가 비었으면 종료.
    // 여기서 return 해야 마지막 정점 뒤에 공백이 붙지 않는다.
    if (target[cnt] == 0)
      return;
    cout << " ";
  }
  return;
}

int main() {
  cin >> N >> M >> V;

  // 간선이 하나도 없는 정점도 map에 있어야 조회가 안전하다
  for (int i = 1; i <= N; ++i) {
    graph.emplace(i, Node());
  }

  for (int i = 0; i < M; ++i) {
    int key, value;
    cin >> key >> value;
    // 양방향 간선이므로 양쪽에 모두 등록.
    // lines가 set이라 중복 간선은 자동으로 무시된다.
    graph[key].push(value);
    graph[value].push(key);
  }

  dfs(V);
  cout << '\n';
  bfs(V);
  return 0;
}

⏱️ 시간·공간 복잡도

N = 정점 수(≤ 1,000), M = 간선 수(≤ 10,000)

구간 복잡도 설명
그래프 구성 O(M log N) 간선마다 map 조회 O(log N) + set 삽입 O(log deg)
DFS O(N² log N) 정점마다 1..N을 전부 훑고(O(N)), 매번 map/set 조회 O(log N)
BFS O(N² log N) 같은 이유
전체 시간 O(N² log N) N = 1,000 기준 약 10⁷ 연산
공간 O(N + M) 인접 정보 map<int, set<int>> + BFS 배열 O(N) + DFS 재귀 스택 O(N)

“간선이 M개뿐인데 왜 N²이냐”가 이 구현의 약점이다.
for (int i = 1; i <= N; ++i)로 이웃이 아닌 정점까지 전부 확인하기 때문에,
간선이 적어도(희소 그래프) 정점 수의 제곱만큼 돈다.
N이 1,000이라 통과했지만 N이 10만이었으면 바로 시간 초과다.


🔀 다른 풀이와 비교

방식 그래프 구성 탐색 메모
map + set (이 풀이) O(M log N) O(N² log N) 정렬·중복 제거 공짜. 대신 이웃만 도는 게 아니라 1..N을 다 돈다
vector<vector<int>> + sort O(M + N·deg log deg) O(N + M) 정석. set을 순회하는 대신 이웃만 직접 돈다
인접 행렬 bool adj[1001][1001] O(M) O(N²) 구현이 제일 단순. 1,000×1,000 = 1MB라 이 문제에선 메모리도 여유

핵심 차이는 “이웃을 어떻게 순회하느냐” 하나다.
set을 쓰더라도 for (int i = 1; i <= N; ++i) 대신
for (int next : graph[root].lines)처럼 set을 직접 range-for로 돌면 곧바로 O(N + M)이 된다.
자료구조를 잘 골라놓고 순회 방식에서 손해를 본 케이스였다.

DFS를 재귀로 짠 것도 N ≤ 1,000이라 괜찮았지, 정점이 10만 개인 문제였다면
재귀 깊이 때문에 스택 오버플로가 나므로 명시적 스택으로 바꿔야 한다.