범위가 조금 넓어서 이번주 스터디에선 그래프의 표현 , 유니온 파인드 , 위상 정렬에 대해서 스터디를 해봤습니다!
그래프는 전체가 하나로 이어져 있을 필요가 없다. 즉, 완전히 연결되지 않은 여러 덩어리로 나뉘어 있을 수 있다.
예를 들어,
친구 그룹 A(서로 다 알고 있음)
친구 그룹 B(서로 다 알고 있음)
A와 B는 서로 모름 → 연결 X
이런 식으로 연결된 덩어리가 여러 개 존재할 수 있는데, 이것을 고립된 부분 그래프라고 한다.
트리는 꼭 지켜야 할 규칙이 있지만, 그래프는 그런 제한이 없다.
트리는 반드시: 루트가 존재해야 하고, 부모–자식 관계가 있어야 하고, 사이클(순환)이 없어야 한다
반면 그래프는: 루트가 없어도 되고, 부모/자식 같은 위계가 없어도 되고, 사이클이 있든 없든 상관없다
즉, 트리는 규칙이 많은 특수한 그래프이고, 그래프는 훨씬 자유로운 형태의 연결 구조라고 이해하면 된다.
오일러 경로(Eulerian path)
- 오일러 경로(Eulerian path): 모든 간선을 정확히 한 번씩 지나는 경로(시작점으로 돌아올 필요 없음).
- 오일러 회로/투어(Eulerian circuit/tour): 모든 간선을 한 번씩 지나 시작 정점으로 돌아오는 경로.
- 존재 조건(무방향, 연결 가정)
- 오일러 회로: 모든 정점의 차수가 짝수.
- 오일러 경로: 차수가 홀수인 정점이 정확히 0개(=회로) 또는 2개. (질문 본문에는 “오일러 경로가 시작점으로 되돌아온다/모든 차수 짝수”라고 되어 있으나, 이는 오일러 회로의 조건입니다 — 위와 같이 정정합니다.)
(A, B)로 표기, (A, B) = (B, A)<A, B>로 표기, <A, B> ≠ <B, A>n(n−1)/2 [그래프1 - 방향이 없는 그래프]
[그래프2 - 방향이 있는 그래프]
정의 : 정점 수가 V일 때 V × V 크기의 행렬 M으로 간선을 표시.
M[i][j] ∈ {0,1}M[i][j] = 가중치(없으면 0 또는 ∞)M[i][j]만 채움M[i][j] = M[j][i] 대칭메모리: Θ(V^2)
주요 연산
O(1)u의 모든 이웃 탐색: O(V)O(1) (값 대입)장점
O(1)).단점
O(V)가 필요.언제 쓰나
친구 목록표라고 생각하면 된다.
즉, 모든 사람을 상대로 ‘친구 여부’를 0/1로 체크한 친구표가 바로 인접행렬.
예: 10,000개의 노드 → 10,000×10,000 = 1억 칸 필요
인접행렬은 그래프의 모든 노드 쌍을 표로 만들어, 연결 여부를 0/1로 저장하는 방식이다.
정의 : 각 정점 u에 대해, u와 연결된 (목적지, 가중치) 쌍의 리스트를 저장.
u → v는 u의 리스트에만 저장u ↔ v는 u와 v 양쪽 리스트에 저장메모리: Θ(V + E)
주요 연산
O(deg(u)) (u의 차수만큼 탐색)u의 모든 이웃 탐색: O(deg(u))O(1)(리스트 push)O(deg(u))(리스트에서 찾아 제거)장점
V+E만큼 저장).단점
O(1)) 확인할 수 없음(리스트 검색 필요).언제 쓰나
“친구 이름만 메모하는 방식”
예
A-B, B-C 연결이면:
A: B
B: A, C
C: B
장점: 공간 효율 높음
단점: A-B 연결됐나 확인하려면 리스트를 뒤져야 함 → O(연결된 친구 수)
정의 : 모든 간선을 배열/리스트 하나에 (u, v[, w]) 형태로 나열.
(u, v)와 (v, u) 중 하나만 저장하는 것이 일반적(표현 방식에 따라 다름)w 포함메모리: Θ(E) (+정점 메타데이터)
주요 연산
O(E)(전체를 훑어야 함)O(E)(필터 필요)O(1)(push)O(E)(검색 후 제거)장점
단점
O(E)).언제 쓰나
“연결된 쌍만 모아둔 명단”
예
A-B, B-C 연결이면:
(A, B)
(B, C)
가중치가 있다면:
(A, B, 3)
(B, C, 5)
장점: 가장 단순, 간선만 모아두면 됨
단점: 두 노드 연결 여부 확인이 느림 → 하나하나 검사해야 함
참고:
https://gmlwjd9405.github.io/2018/08/13/data-structure-graph.html
https://www.acmicpc.net/problem/2606
import java.io.*;
import java.util.*;
public class Main {
static int count, num, connections;
static boolean[] visited;
static List[] computers;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
num = Integer.parseInt(br.readLine());
connections = Integer.parseInt(br.readLine());
visited = new boolean[num+1];
computers = new List[num+1];
count = 0;
for(int i=1; i<num+1; i++) {
computers[i] = new ArrayList<Integer>();
}
StringTokenizer st;
for(int i=0; i<connections; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
computers[a].add(b);
computers[b].add(a);
}
bfs(1);
System.out.println(count-1); // 시작 지점 카운트 빼주기
br.close();
}
private static void bfs(int start) {
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(start);
while(!queue.isEmpty()) {
int now = queue.poll();
if(!visited[now]) {
count++;
visited[now] = true;
for(int i=0; i<computers[now].size(); i++) {
queue.add((int)computers[now].get(i));
}
}
}
}
}
import java.io.*;
import java.util.*;
public class Main {
static int count, num, connections;
static boolean[] visited;
static List[] computers;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
num = Integer.parseInt(br.readLine());
connections = Integer.parseInt(br.readLine());
visited = new boolean[num+1];
computers = new List[num+1];
count = 0;
for(int i=1; i<num+1; i++) {
computers[i] = new ArrayList<Integer>();
}
StringTokenizer st;
for(int i=0; i<connections; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
computers[a].add(b);
computers[b].add(a);
}
dfs(1);
System.out.println(count-1); // 시작 지점 카운트 빼주기
br.close();
}
private static void dfs(int start) {
if(!visited[start]) {
visited[start] = true;
count++;
for(int i=0; i<computers[start].size(); i++) {
dfs((int)computers[start].get(i));
}
}
}
}
}