🔗 문제
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만 개인 문제였다면
재귀 깊이 때문에 스택 오버플로가 나므로 명시적 스택으로 바꿔야 한다.