| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 스프링부트
- 인프라
- 캡스톤디자인프로젝트
- Route53
- 체크인미팅
- 개발공부
- EC2
- 프로그래밍
- bastion host
- 전국대학생게임개발동아리연합회
- UNICON2023
- 게임개발동아리
- AWS
- 오블완
- 42서울
- 생활코딩
- Redis
- openAI API
- spring ai
- NAT gateway
- 티스토리챌린지
- 프롬프트엔지니어링
- 프리티어
- Spring boot
- CICD
- UNICON
- 라피신
- UNIDEV
- 백엔드개발자
- 도커
- Today
- Total
Hyun's Wonderwall
[Do it! 알고리즘 코딩테스트 - 자바 편] 09. 그래프 본문
에지 리스트와 인접 리스트가 헷갈림: https://u-it.tistory.com/491
09. 그래프
그래프: 노드와 에지로 구성된 집합
- 노드: 데이터를 표현하는 단위
- 에지: 노드를 연결
(트리도 그래프의 일종)
09-1. 그래프의 표현
그래프를 구현하는 방법은 3가지.
1. 에지 리스트(edge list): "에지를 중심"으로 그래프를 표현.
- 배열에 "출발 노드, 도착 노드"를 저장하여 에지를 표현
- 또는 "출발 노드, 도착 노드, 가중치"를 저장하여 가중치가 있는 에지를 표현.
에지 리스트로 가중치 없는 그래프 표현하기
- 가중치가 없는 그래프는 출발 노드와 도착 노드만 표현 -> 배열 열은 2개면 충분.
노드는 여러 자료형을 사용할 수 있는데 다음의 경우 노드는 Integer형.
방향 그래프: 1->2, 1->3, 2->4, 2->5, 3->4, 4->5
=> [[1, 2]], [1, 3], [2, 4], [2, 5], [4, 5]] = A[N][2]
이처럼 방향이 있는 그래프는 순서에 맞게 노드를 배열에 저장하는 방식으로 표현한다.
그리고 노드를 배열에 저장하여 에지를 표현하므로 에지 리스트라고 한다.
- 만약 방향이 없는 그래프라면 1<->2는 [1, 2], [2, 1]
에지 리스트로 가중치 있는 그래프 표현하기
가중치가 있는 그래프는 열을 3개로 늘려 3번째 열에 가중치를 저장하면 된다.
1->2로 향하는 가중치가 8인 에지는 이제 [1, 2, 8]로 표현된다.
[[1, 2, 8], [1, 3, 3], ...] = A[N][3]
에지 리스트는 구현하기 쉬운데 특정 노드와 관련되어있는 에지를 탐색하기는 지 않다.
에지 리스트는 벨만 포드나 크루스칼(MST) 알고리즘에 사용한다.
2. 인접 행렬(adjacency matrix)은 2차원 배열을 자료구조로 이용하여 그래프를 표현한다.
노드 중심으로 그래프를 표현한다.
인접 행렬로 가중치 없는 그래프 표현하기
1~5번 노드가 각각 간선 가짐. 출발노드: 행, 도착노드: 열
x->y가 있으면 5*5 행렬에서 arr[x][y]=1로 설정
인접 행렬로 가중치 있는 그래프 표현하기
x-num->y가 있으면 5*5 행렬에서 arr[x][y]=가중치값num 으로 설정
인접 행렬을 이용한 그래프 구현은, 쉽고 두 노드를 연결하는 에지의 여부와 가중치값을 배열에 직접 접근해 바로 확인할 수 있는 게 장점.
하지만 노드와 관련되어있는 에지를 탐색하려면 N번 접근해야 하므로, 노드 개수에 비해 에지가 적을 때는 공간 효율성이 떨어진다.
또한 노드 개수가 많은 경우 아예 2차원 배열 선언 자체를 할 수 없음.
=> 따라서 인접 행렬은 노드 개수에 따라 사용 여부를 적절하게 판단해야 함
예를 들어 노드가 3만 개를 넘으면 자바 힙 스페이스 에러가 발생한다.
3.인접 리스트: 리스트 배열.
인접 리스트는 ArrayList로 그래프를 표현한다. 노드 개수만큼 ArrayList를 선언. 자료형은 경우에 맞게 사용.
ArrayList<Integer>[5]
- 인접 리스트에는 N번 노드와 연결되어 있는 노드를 배열의 위치 N에 연결된 노드 개수만큼 배열을 연결하는 방식으로 표현한다.
- 예를 들어 노드 1과 연결된 2, 3 노드는 ArrayList[1]에 [2, 3]을 연결하는 방식으로 표현한다.
ArrayList<Integer>[N] = [1]->[2],[3] [2]->[4],[5] ... 이런식
- 여기서도 방향이 있는 그래프를 표현한것
인접 리스트로 가중치 있는 그래프 표현하기
- 가중치가 있는 경우 자료형을 클래스로 사용한다.
(도착 노드, 가중치)를 갖는 Node 클래스를 선언하여 ArrayList에 사용한다.
ArrayList<Node>[N] = [1]->[2,8],[3,3] [2]->[4,4][5,15] ...
ArrayList[1]에 [(2,8), (3,3)]
노드 1과 2가 가중치 8 에지로, 노드 1과 3이 가중치 3 에지로 연결되어있다는 것을 보여줌. 방향성도 고려되어있음
인접리스트 구현은 복잡한 편이지만, 노드와 연결되어있는 에지를 탐색하는 시간이 매우 뛰어남
노드 개수가 커도 공간 효율이 좋아 메모리 초과 에러 발생 X
이런 장점으로 실제 코딩 테스트에서는 인접 리스트를 이용한 그래프 구현을 선호한다.
[18352번: 특정 거리의 도시 찾기] / 실2
Java
import java.io.*;
import java.util.*;
public class Main {
static List<Integer>[] graph;
static int N, M, K, X;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
K = Integer.parseInt(st.nextToken());
X = Integer.parseInt(st.nextToken());
graph = new ArrayList[N+1];
for(int i=1; i<=N; i++){
graph[i] = new ArrayList<>();
}
for(int i=0; i<M; i++){
st = new StringTokenizer(br.readLine());
int A = Integer.parseInt(st.nextToken());
int B = Integer.parseInt(st.nextToken());
graph[A].add(B);
}
int[] dist = new int[N+1];
Arrays.fill(dist, -1); // 방문 안 한 상태 초기화
Queue<Integer> q = new ArrayDeque<>();
q.add(X);
dist[X] = 0;
while(!q.isEmpty()){
int cur = q.poll();
for(int next : graph[cur]){
if(dist[next] == -1){ // 첫 방문일 때만
dist[next] = dist[cur] + 1;
q.add(next);
}
}
}
boolean printed = false;
for(int i=1; i<=N; i++){
if(dist[i] == K){
System.out.println(i);
printed = true;
}
}
if(!printed) System.out.println(-1);
}
}
C++
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int dist[300001];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m, k, x;
cin >> n >> m >> k >> x;
vector<vector<int>> graph(n+1);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
graph[a].push_back(b);
}
// dist 초기화 (-1 = 미방문)
fill(dist, dist+n+1, -1);
queue<int> q;
q.push(x);
dist[x] = 0;
// BFS
while (!q.empty()) {
int curr = q.front();
q.pop();
for (int next : graph[curr]) {
if (dist[next] == -1) {
dist[next] = dist[curr] + 1;
q.push(next);
}
}
}
bool found = false; // 1개라도 찾았는지 확인용
// 오름차순 출력
for (int i = 1; i <= n; i++) {
if (dist[i] == k) {
cout << i << "\n";
found = true;
}
}
if (!found) cout << -1;
}
[1325번: 효율적인 해킹] / 실1
Java
import java.io.*;
import java.util.*;
public class Main {
static List<Integer>[] graph;
static int N, M;
static int[] count;
static int[] visited;
static int visitId = 1; // 현재 visited하는 번호
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
graph = new ArrayList[N+1];
count = new int[N+1];
visited = new int[N+1];
for (int i = 1; i <= N; i++) graph[i] = new ArrayList<>();
// 방향: b -> a
for (int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
graph[b].add(a);
}
int max = 0;
for (int i = 1; i <= N; i++) {
count[i] = bfs(i);
max = Math.max(max, count[i]);
}
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= N; i++) {
if (count[i] == max) {
sb.append(i).append(' ');
}
}
System.out.println(sb);
}
static int bfs(int start) {
visitId++;
ArrayDeque<Integer> q = new ArrayDeque<>();
q.add(start);
visited[start] = visitId;
int cnt = 1;
while (!q.isEmpty()) {
int cur = q.poll();
for (int nxt : graph[cur]) {
if (visited[nxt] != visitId) { // 이번에 아직 방문 안 했는지
visited[nxt] = visitId;
cnt++;
q.add(nxt);
}
}
}
return cnt;
}
}
C++
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n+1);
for(int i=0; i<m; i++){
int a, b;
cin >> a >> b;
graph[b].push_back(a); // b로인해 a로 가는걸 확인해야
}
//BFS
int maxCnt=0;
vector<int> comp;
for(int i=1; i<=n; i++){
queue<int> q;
bool visited[n+1]={};
int cnt=0;
q.push(i);
visited[i]=true;
while(!q.empty()){
int c=q.front();
q.pop();
for(int j : graph[c]){
if(!visited[j]){
cnt++;
visited[j]=true;
q.push(j);
}
}
}
if(maxCnt==cnt) {
comp.push_back(i);
} else if(maxCnt<cnt){
maxCnt=max(maxCnt,cnt);
comp.assign(1, i);
}
}
for(int c : comp)
cout<<c<<" ";
return 0;
}
[1707번: 이분그래프] / 골4
Java
import java.io.*;
import java.util.*;
public class Main {
static List<Integer>[] graph;
static int[] color;
static int V, E;
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
int K = Integer.parseInt(br.readLine());
while (K-- > 0) {
StringTokenizer st = new StringTokenizer(br.readLine());
V = Integer.parseInt(st.nextToken());
E = Integer.parseInt(st.nextToken());
graph = new ArrayList[V + 1];
for (int i = 1; i <= V; i++)
graph[i] = new ArrayList<>();
for (int i = 0; i < E; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
graph[a].add(b);
graph[b].add(a);
}
color = new int[V+1]; // 0: 미방문, 1/-1: 두 그룹
if (check()) sb.append("YES\n");
else sb.append("NO\n");
}
System.out.print(sb.toString());
}
static boolean check() {
for (int i = 1; i <= V; i++) {
if (color[i] == 0) {
if (!bfs(i)) {
return false;
}
}
}
return true;
}
static boolean bfs(int start) {
Queue<Integer> q = new ArrayDeque<>();
q.add(start);
color[start] = 1;
while (!q.isEmpty()) {
int cur = q.poll();
for (int next : graph[cur]) {
if (color[next] == 0) {
color[next] = -color[cur]; // 반대 색으로 칠하기
q.add(next);
} else {
if (color[next] == color[cur]) { // 이미 색이 있는데, 인접한 두 정점의 색이 같다면 이분 그래프 X
return false;
}
}
}
}
return true;
}
}
C++
오랜만에 풀어서 이분그래프 뜻을 까먹었었는데... 노드마다 두가지 색을 칠할 수 있는데, 연결된 것끼리는 다 색이 다르면 이분그래프.
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
bool check(vector<vector<int>> graph, int v){
int visited[v+1];
fill(visited, visited+v+1, 0);
for(int i=0; i<v; i++){
queue<int> q;
q.push(i);
if(visited[i]==0) visited[i]=1;
while(!q.empty()){
int c=q.front();
q.pop();
for(int j : graph[c]){
if(visited[j]==0) {
q.push(j);
visited[j]=3-visited[c];
}if(visited[j]==visited[c]) {
return false;
}
}
}
}
return true;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int tk, v, e;
cin>>tk;
while(tk-->0){
cin>>v>>e;
vector<vector<int>> graph(v+1);
for(int i=0; i<e; i++){
int a, b;
cin>>a>>b;
graph[a].push_back(b);
graph[b].push_back(a);
}
if(check(graph, v))
cout<<"YES\n";
else
cout<<"NO\n";
}
}
유니온파인드
[1717]
[1976]
[1043]
위상 정렬 (Topological Sort)
방향 그래프에서 노드들을 선후관계를 지키면서 일렬로 나열하는 것
[과목 수강] 수학 → 물리 → 양자역학, 수학 → 통계. "위상 정렬 결과: 수학 → 물리 → 통계 → 양자역학"
순서. 줄세우기. 하면 위상정렬.
진입차수(indegree): 어떤 노드로 들어오는 간선의 수. (진입차수 0 = 선행 조건 없음 = 지금 당장 처리 가능)
알고리즘 동작 (BFS 방식)
1. 모든 노드의 진입차수 계산 (a->b면 v[a].push_back(b))
2. 진입차수 == 0인 노드를 큐에 삽입
3. 큐에서 노드를 꺼내 출력
4. 해당 노드의 이웃 노드들의 진입차수 -1
5. 진입차수가 0이 된 노드를 큐에 삽입
6. 큐가 빌 때까지 반복
DFS로도 할 수 있는데 BFS가 더 좋음. 아래는 DFS...
DFS로 끝까지 내려간 뒤
스택에 쌓고 역순 출력
void dfs(int x){
visited[x] = true;
for(int nxt : graph[x]){
if(!visited[nxt]) dfs(nxt);
}
stk.push(x); // 끝난 뒤 push
}
→ 마지막에 스택 pop하면 위상정렬
dfs(1)
dfs(2)
dfs(4)
갈 곳 없음 → stack push(4)
stack push(2)
dfs(3)
4는 이미 visited
stack push(3)
stack push(1)
stack: [1, 3, 2, 4] (top이 1)
출력: 1 3 2 4
중요한 조건
- DAG (Directed Acyclic Graph) 에서만 가능
- 사이클이 있으면 위상 정렬 불가 (큐가 비었는데 출력한 노드 수 < 전체 노드 수 → 사이클 존재)
사이클 어떻게 감지?
BFS는 if (출력한 노드 수 != n) { cout << "사이클 존재"; } 면 사이클 있음. 사이클 유무만 판단 가능.
DFS는 vector<int> state(n + 1, 0); 로 미방문했었으면 state[nxt]=0, 방문 중이면 state[nxt]=1, 완료했으면 state[nxt]=2로 체크.
사이클 경로를 추적할 수 있다.
void printCycle(int start, int cur) {
vector<int> cycle;
cycle.push_back(cur);
while (cur != start) { // 시작점 만나면 종료
cur = parent[cur];
cycle.push_back(cur);
}
reverse(cycle.begin(), cycle.end()); // 가장 부모부터 출력위해
for (int node : cycle) cout << node << ' ';
}
void dfs(int cur) {
state[cur] = 1;
for (int nxt : graph[cur]) {
if (state[nxt] == 1) {
printCycle(nxt, cur); // nxt가 사이클 시작점
return;
}
if (state[nxt] == 0) {
parent[nxt] = cur;
dfs(nxt);
}
}
state[cur] = 2;
}
다익스트라
벨만포드
플로이드워셜
최소신장트리
'Study > PS' 카테고리의 다른 글
| [Do it! 알고리즘 코딩테스트 - 자바 편] 07. 그리디 (0) | 2025.11.06 |
|---|---|
| [Do it! 알고리즘 코딩테스트 - 자바 편] 06. 탐색 (DFS, BFS, 백트래킹, 이진탐색) (1) | 2025.10.30 |
| [백준 골드2] 1655번: 가운데를 말해요 (우선순위 큐) - Java (0) | 2025.10.15 |
| [Do it! 알고리즘 코딩테스트 - 자바 편] 05. 정렬 (0) | 2025.10.06 |
| [Do it! 알고리즘 코딩테스트 - 자바 편] 04. 자료구조 (0) | 2025.09.25 |