1. 문제 정보
- 문제 번호: 11724번
- 문제 이름: 연결 요소의 개수
- 문제 요약: 방향 없는 그래프가 주어졌을 때, 연결 요소(덩어리)의 개수를 구하라.
2. 결정적 문법: [] vs () 의 차이
① vector<int> graph[1001]; (배열의 각 칸이 벡터)
- 정체: vector<int> 타입의 객체 1001개를 담는 "배열"입니다.
- 용도: 그래프의 인접 리스트(Adjacency List)를 구현할 때 표준적으로 사용됩니다.
- graph[1]은 1번 정점과 연결된 정점들을 담는 벡터, graph[2]는 2번 정점과 연결된 정점들을 담는 벡터가 되는 식.
② vector<int> graph(1001); (크기가 1001인 하나의 벡터)
- 정체: 정수(int) 1001개를 담을 수 있는 "하나의 벡터"이다.
- 용도: 1차원 배열처럼 사용하고 싶을 때 쓴다. 만약 이걸로 인접 리스트를 만들고 싶다면 vector<vector<int>> graph(1001);처럼 선언해야 한다.
3. 알고리즘: 연결 요소 찾기 (DFS)
연결 요소는 쉽게 말해 "섬(Island)"과 같다.
- 전체 탐색: 1번 정점부터 N번 정점까지 차례대로 확인합니다.
- 새로운 섬 발견: 아직 방문하지 않은(!visited) 정점을 발견했다면, 새로운 연결 요소가 시작된 것이다. connected++를 해준다.
- 섬 전체 마킹: DFS(깊이 우선 탐색)를 시작하여 해당 정점과 연결된 모든 정점을 방문 처리(visited = true)합니다.
- 반복: 모든 정점을 확인할 때까지 반복합니다.
4. 코드 구현
#include <iostream>
#include <vector>
using namespace std;
// 1. 인접 리스트 선언 (배열의 각 칸이 벡터)
vector<int> graph[1001];
bool visited[1001] = {false};
void dfs(int start) {
visited[start] = true;
// 범위 기반 for문으로 연결된 정점 확인
for(int v : graph[start]) {
if(!visited[v]) {
dfs(v);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
// 2. 간선 정보 입력 (무방향 그래프이므로 양방향 추가)
for(int i = 0; i < M; i++) {
int v, p;
cin >> v >> p;
graph[v].push_back(p);
graph[p].push_back(v);
}
int connected = 0;
// 3. 모든 정점을 순회하며 DFS 실행
for(int i = 1; i <= N; i++) {
if(!visited[i]) {
dfs(i); // DFS가 한 번 끝날 때마다 섬 하나를 다 찾은 것
connected++; // 연결 요소 개수 증가
}
}
cout << connected << "\n";
return 0;
}
5. 배운 점 & 회고 (TIL) ✍️
1. C++ 문법의 재발견: [] vs ()
그래프를 구현할 때 사용하는 vector 선언 방식에 따라 메모리 구조가 완전히 달라짐을 이해해야 한다.
- vector<int> graph[1001]: 이는 vector<int> 타입의 객체 1001개를 담는 배열이다. 각 인덱스가 하나의 정점을 의미하며, 해당 정점에 연결된 이웃 노드들을 벡터 안에 저장하는 인접 리스트 구현에 최적화된 방식이다.
- vector<int> graph(1001): 이는 정수(int) 1001개를 저장할 수 있는 단 하나의 벡터를 생성하는 방식이다. 주로 1차원 배열이 필요할 때 사용한다.
- 정점의 개수가 고정적인 문제에서는 []를 이용한 배열 선언이 직관적이며 성능 면에서도 유리하다.
2. 그래프를 저장하는 두 가지 방법
그래프 자료구조를 메모리에 올리는 방법은 크게 두 가지로 나뉜다.
| 구분 | 인접 리스트 (Adjacency List) | 인접 행렬 (Adjacency Matrix) |
| 선언 방식 | vector<int> graph[N] | int graph[N][N] |
| 메모리 복잡도 | O(V+E) (실제 간선만큼 사용) | O(V^2) (무조건 정점의 제곱) |
| 탐색 시간 | O(degree(v)) (이웃만 확인) | O(V) (모든 칸 확인) |
| 주요 장점 | 메모리 효율적, 희소 그래프에 유리 | 두 노드의 연결 여부를 O(1)에 확인 |
왜 인접 리스트가 '국룰'인가?
본 문제에서 정점(N)이 1,000개일 때, 인접 행렬은 연결 여부와 상관없이 1,000,000개의 칸을 확인해야 한다. 반면 인접 리스트는 실제로 연결된 간선(M) 정보만 저장하고, DFS 탐색 시 실제로 존재하는 이웃만 쏙쏙 골라 방문한다. 따라서 데이터가 커질수록 시간과 공간 복잡도 면에서 압도적인 효율을 보여준다.
3. 핵심 로직: "방문 처리"와 "덩어리 찾기"
① 방문 처리(Visited)의 중요성
이미 방문한 정점을 다시 방문하지 않도록 막는 visited 배열은 DFS/BFS의 핵심이다. 이는 무한 루프를 방지할 뿐만 아니라, 하나의 시작점으로부터 갈 수 있는 모든 정점을 탐색하여 독립된 덩어리(연결 요소)를 정확히 구분해내는 역할을 한다.
② '유기농 배추' 문제와의 본질적 공통점
이전에 풀었던 1012번(유기농 배추) 문제와 본 문제의 논리는 동일하다.
- 유기농 배추: 2차원 격자(Grid) 내에서 상하좌우로 연결된 덩어리를 찾음이다.
- 연결 요소의 개수: 인접 리스트(Graph) 상에서 간선으로 연결된 덩어리를 찾음이다.
- 데이터의 표현 방식만 다를 뿐, "연결된 그룹의 개수를 세는 것"이 문제의 본질임을 이해하는 것이 중요하다.
4. 결론 및 회고
그래프 문제에서 자료구조의 선택이 알고리즘의 효율성을 결정짓는다는 것을 체감하였다. 특히 인접 리스트의 경제적인 메모리 사용과 빠른 탐색 속도는 대규모 그래프 문제를 해결하는 데 필수적인 요소이다. 또한, 격자형 탐색과 그래프 탐색이 결국 '연결 요소 찾기'라는 하나의 원리로 관통한다는 점을 배웠다.
'알고리즘 문제풀이 > C++' 카테고리의 다른 글
| [백준/C++] 18870번: 좌표 압축 - STL의 정교한 활용 (0) | 2026.03.25 |
|---|---|
| [C++ STL] map 컨테이너 완벽 정리: 구조부터 활용까지 (0) | 2026.03.25 |
| [백준/C++] 2805번: 나무 자르기 - 이분 탐색과 매개변수 탐색 (0) | 2026.03.19 |
| [백준/C++] 2630번: 색종이 만들기 - 분할 정복과 재귀의 정석 (0) | 2026.03.17 |
| [백준/C++] 1927번: 최소 힙 - priority_queue의 내부 구조 파헤치기 (0) | 2026.03.10 |