그래프의 구현 방식에는 인접 행렬(adjacency matrix)과 인접 리스트(adjacency list)가 있다.
인접 행렬
구조
배열 구조 : 2차원 배열
배열의 인덱스 : 노드
배열의 값 : 노드의 가중치
행 인덱스 : 출발
열 인덱스 : 도착
장점
간선의 정보를 확인할 때의 시간 복잡도가 O(1) 이다.
단점
노드 수에 비해 간선 수가 매우 적을 경우 N x N 크기의 공간이 낭비된다.
노드들의 값의 차이가 매우 클 경우 해당 값으로 행렬의 크기를 잡아야 하므로 낭비된다.
인접 리스트
노드 : [정점(v), 가중치(w)]
장점
정점에 연결된 간선의 정보를 빠르게 알 수 있다.
공간 복잡도가 O(E[간선수] + V[정점수]) 이다.
단점
A에서 B로 연결된 간선을 리스트에 연결된 개수만큼 소요된다.
그래프 탐색
그래프 탐색에는 깊이 우선 탐색(depth-first search, DFS)과 너비 우선 탐색(breadth first search, BFS)이 있다.
깊이 우선 탐색
깊이 우선 탐색은 시작 노드부터 탐색을 시작하여 간선을 따라 최대 깊이 노드까지 이동하며 차례대로 방문한다.
일단 시작 노드를 정하고, 스택에 시작 노드를 push 한다. 스택에 있는 노드는 아직 방문하지 않았지만 방문할 예정인 노드이다. 시작 노드를 push 했으면 다음 과정을 반복한다.
스택이 비었는지 확인한다. 스택이 비었으면 탐색을 종료한다.
스택에서 노드를 pop 한다.
pop 한 노드의 방문 여부를 확인하고 방문 처리한다.
방문한 노드와 인접한 모든 노드를 확인하고, 방문하지 않았다면 노드를 스택에 push 한다.
이 과정을 코드로 구현할 때 다음 세 가지 사항을 고려해야 한다.
탐색할 노드가 없을 때까지 간선을 타고 내려갈 수 있어야 한다.
가장 최근에 방문한 노드를 알아야 한다.
이미 방문한 노드인지 확인할 수 있어야 한다.
깊이 우선 탐색의 핵심은 '가장 깊은 노드까지 방문한 후에 더 이상 방문할 노드가 없으면 최근 방문한 노드로 돌아온 다음, 해당 노드에서 방문할 노드가 있는지 확인한다' 이다.
깊이 우선 탐색 구현 코드
// 깊이 우선 탐색 (DFS) 구현 예제// 그래프를 인접 리스트로 표현하기 위한 클래스classGraph(privateval numVertices: Int) {// 각 정점에 연결된 인접 정점들의 리스트를 저장하는 배열privateval adjacencyList: Array<MutableList<Int>> =Array(numVertices) { mutableListOf() }// 간선 추가 함수funaddEdge(source: Int, destination: Int) { adjacencyList[source].add(destination)// 무방향 그래프의 경우 다음 줄의 주석을 해제하여 양방향 간선 추가// adjacencyList[destination].add(source) }// DFS 탐색 시작 함수fundfs(startVertex: Int) {val visited =BooleanArray(numVertices) // 방문한 정점을 표시하는 배열dfsRecursive(startVertex, visited) // 재귀적으로 DFS 수행 }// DFS를 재귀적으로 구현한 함수privatefundfsRecursive(vertex: Int, visited: BooleanArray) { visited[vertex] =true// 현재 정점을 방문으로 표시println("방문한 노드: $vertex") // 방문한 정점을 출력// 현재 정점에 인접한 모든 정점을 순회for (adjacentVertex in adjacencyList[vertex]) {if (!visited[adjacentVertex]) {dfsRecursive(adjacentVertex, visited) // 미방문 정점에 대해 재귀 호출 } } }}funmain() {val graph =Graph(6) // 정점의 개수가 6인 그래프 생성// 간선 추가 graph.addEdge(0, 1) graph.addEdge(0, 2) graph.addEdge(1, 3) graph.addEdge(1, 4) graph.addEdge(2, 5)// DFS 탐색 시작println("깊이 우선 탐색 결과:") graph.dfs(0) // 노드 0에서 DFS 수행}
너비 우선 탐색
너비 우선 탐색은 시작 노드와 거리가 가장 가까운 노드를 우선하여 방문하는 방식의 알고리즘이다.
일단 시작 노드를 정하고, 큐에 시작 노드를 add 한다. 시작 노드를 큐에 add 하면서 방문 처리한다. 큐에 있는 노드는 이미 방문 처리했고, 그 노드와 인접한 노드는 아직 탐색하지 않은 상태라고 생각하면 된다. 이후 다음 과정을 반복한다.
큐가 비었는지 확인한다. 큐가 비었다면 탐색을 종료한다.
큐에서 노드를 poll 한다.
poll 한 노드와 인접한 모든 노드를 확인하고, 방문하지 않은 노드를 큐에 add 하며 방문 처리한다.
이 과정을 코드로 구현할 때 다음 두 가지 사항을 고려해야 한다.
현재 방문한 노드와 직접 연결된 모든 노드를 방문할 수 있어야 한다.
이미 방문한 노드인지 확인할 수 있어야 한다.
너비 우선 탐색 구현 코드
// 너비 우선 탐색 (BFS) 구현 예제import java.util.LinkedListimport java.util.Queue// 그래프를 인접 리스트로 표현하기 위한 클래스classGraph(privateval numVertices: Int) {// 각 정점에 연결된 인접 정점들의 리스트를 저장하는 배열privateval adjacencyList: Array<MutableList<Int>> =Array(numVertices) { mutableListOf() }// 간선 추가 함수funaddEdge(source: Int, destination: Int) { adjacencyList[source].add(destination)// 무방향 그래프의 경우 다음 줄의 주석을 해제하여 양방향 간선 추가// adjacencyList[destination].add(source) }// BFS 탐색 시작 함수funbfs(startVertex: Int) {val visited =BooleanArray(numVertices) // 방문한 정점을 표시하는 배열val queue: Queue<Int> =LinkedList() // 탐색에 사용할 큐 visited[startVertex] =true// 시작 정점을 방문으로 표시 queue.add(startVertex) // 시작 정점을 큐에 추가while (queue.isNotEmpty()) {val vertex = queue.poll() // 큐에서 정점을 하나 꺼냄println("방문한 노드: $vertex") // 방문한 정점을 출력// 현재 정점에 인접한 모든 정점을 순회for (adjacentVertex in adjacencyList[vertex]) {if (!visited[adjacentVertex]) { visited[adjacentVertex] =true// 인접 정점을 방문으로 표시 queue.add(adjacentVertex) // 인접 정점을 큐에 추가 } } } }}funmain() {val graph =Graph(6) // 정점의 개수가 6인 그래프 생성// 간선 추가 graph.addEdge(0, 1) graph.addEdge(0, 2) graph.addEdge(1, 3) graph.addEdge(1, 4) graph.addEdge(2, 5)// BFS 탐색 시작println("너비 우선 탐색 결과:") graph.bfs(0) // 노드 0에서 BFS 수행}
깊이 우선 탐색과 너비 우선 탐색 비교
깊이 우선 탐색
더 이상 탐색할 수 없으면 백트래킹하여 최근 방문 노드부터 다시 탐색을 진행한다는 특성
모든 가능한 해를 찾는 백트래킹 알고리즘을 구현할 때 사용
그래프의 사이클을 감지해야 하는 경우 활용
탐색을 해야할 때, 최단 경로를 찾는 문제가 아니면 깊이 우선 탐색을 우선 고려
너비 우선 탐색
찾은 노드가 시작 노드로부터 최단 경로임을 보장한다.
답이 많은 경우 가장 가까운 답을 찾을 때 유용하다.
미로 찾기 문제에서 최단 경로를 찾거나, 네트워크 분석 문제를 풀 때 활용
그래프 최단 경로 구하기
최단 경로는 그래프의 종류에 따라 다르게 해석될 수 있다.
가중치가 없는 경우 : 간선 개수가 가장 적은 경로
가중치가 있는 경우 : 간선의 가중치의 총합이 최소
최단 경로를 구하는 대표적인 알고리즘인 다익스트라 알고리즘, 벨만-포드 알고리즘이 있다.
다익스트라 알고리즘
가중치가 있는 그래프의 최단 경로를 구하는 문제는 대부분 다익스트라 알고리즘을 사용한다.
다익스트라 알고리즘은 다음과 같은 과정으로 동작한다.
시작 노드를 설정하고 시작 노드로부터 특정 노드까지의 최소 비용을 저장할 공간과 직전 노드를 저장할 공간을 마련한다.
최소 비용을 저장할 공간은 모두 매우 큰 값(INF, 무한대)으로 초기화한다. 직전 노드를 저장할 공간도 INF로 초기화한다.
시작 노드의 최소 비용은 0, 직전 노드는 자신으로 한다.
해당 노드를 통해 방문할 수 있는 노드 중, 즉 아직 방문하지 않은 노드 중 현재까지 구한 최소 비용이 가장 적은 노드를 선택한다.
해당 노드를 거쳐서 각 노드까지 가는 최소 비용과 현재까지 구한 최소 비용을 비교하여 작은 값을 각 노드의 최소 비용으로 갱신한다.
이때 직전 노드도 같이 갱신한다.
노드 개수에서 1을 뺀 만큼 반복한다.
다익스트라 구현 코드
import java.util.PriorityQueue// 간선을 나타내는 데이터 클래스dataclassEdge(val to: Int, val weight: Int)// 우선순위 큐에서 사용할 노드를 나타내는 데이터 클래스dataclassNode(val index: Int, val distance: Int) : Comparable<Node> {// 거리(distance)를 기준으로 정렬하기 위해 Comparable 인터페이스 구현overridefuncompareTo(other: Node): Int=this.distance - other.distance}// 다익스트라 알고리즘 함수fundijkstra(graph: List<List<Edge>>, start: Int): IntArray {val n = graph.size// 시작 노드로부터 각 노드까지의 최단 거리를 저장하는 배열val distances =IntArray(n) { Int.MAX_VALUE } distances[start] =0// 시작 노드까지의 거리는 0으로 설정// 우선순위 큐를 사용하여 방문할 노드를 관리val priorityQueue =PriorityQueue<Node>() priorityQueue.add(Node(start, 0)) // 시작 노드를 큐에 추가while (priorityQueue.isNotEmpty()) {val currentNode = priorityQueue.poll() // 가장 작은 거리를 가진 노드를 꺼냄val currentIndex = currentNode.index// 이미 처리된 노드라면 스킵if (distances[currentIndex] < currentNode.distance) continue// 현재 노드와 인접한 모든 노드를 확인for (edge in graph[currentIndex]) {val nextIndex = edge.toval newDist = distances[currentIndex] + edge.weight // 새로운 거리 계산// 새로운 거리가 기존 거리보다 작으면 업데이트if (newDist < distances[nextIndex]) { distances[nextIndex] = newDist priorityQueue.add(Node(nextIndex, newDist)) // 다음 노드를 큐에 추가 } } }return distances // 시작 노드로부터 각 노드까지의 최단 거리 배열 반환}funmain() {// 그래프 초기화val n =5// 노드의 개수val graph =List(n) { mutableListOf<Edge>() } // 각 노드에 대한 인접 리스트 생성// 간선 추가 (예시) graph[0].add(Edge(1, 2)) // 노드 0에서 노드 1로 가중치 2인 간선 graph[0].add(Edge(2, 5)) // 노드 0에서 노드 2로 가중치 5인 간선 graph[1].add(Edge(2, 1)) // 노드 1에서 노드 2로 가중치 1인 간선 graph[1].add(Edge(3, 2)) // 노드 1에서 노드 3으로 가중치 2인 간선 graph[2].add(Edge(3, 3)) // 노드 2에서 노드 3으로 가중치 3인 간선 graph[2].add(Edge(4, 1)) // 노드 2에서 노드 4로 가중치 1인 간선 graph[3].add(Edge(4, 1)) // 노드 3에서 노드 4로 가중치 1인 간선val startNode =0// 시작 노드 설정val distances =dijkstra(graph, startNode) // 다익스트라 알고리즘 실행// 결과 출력for (i in distances.indices) {println("노드 $startNode 에서 노드 $i 까지의 최단 거리: ${distances[i]}") }}
벨만-포드 알고리즘
벨만-포드 알고리즘은 매 단계마다 모든 간선의 가중치를 다시 확인하여 최소 비용을 갱신하므로 음의 가중치를 가지는 그래프에서도 최단 경로를 구할 수 있다.
밸만-포드 알고리즘은 다음과 같이 동작한다.
시작 노드를 설정한 다음 시작 노드의 최소 비용은 0, 나머지 노드는 INF로 초기화한다. 이후 최소 비용을 갱신할 때 직전 노드도 갱신한다.
노드 개수 - 1 만큼 다음 연산을 반복한다.
시작 노드에서 갈 수 있는 각 노드에 대하여 전체 노드 각각을 거쳐갈 때 현재까지 구한 최소 비용보다 더 적은 최소 비용이 있는지 확인하여 갱신한다. 최소 비용을 갱신할 때, V의 직전 노드 값도 같이 갱신한다.
과정 (2)를 마지막으로 한 번 더 수행하여 갱신되는 최소 비용이 있는지 확인한다. 만약 있다면 음의 순환이 있음을 의미한다.
벨만-포드 구현 코드
// 간선을 나타내는 데이터 클래스dataclassEdge(val from: Int, val to: Int, val weight: Int)// 벨만-포드 알고리즘 함수funbellmanFord(edges: List<Edge>, vertexCount: Int, start: Int): Pair<IntArray, Boolean> {// 시작 노드로부터 각 노드까지의 최단 거리를 저장하는 배열val distances =IntArray(vertexCount) { Int.MAX_VALUE } distances[start] =0// 시작 노드까지의 거리는 0으로 설정// 최단 경로가 업데이트되었는지 여부를 나타내는 변수var updated: Boolean// 그래프에 있는 모든 간선에 대해 V-1번 반복for (i in0 until vertexCount -1) { updated =falsefor (edge in edges) {// 현재 간선을 통해 더 짧은 경로를 발견한 경우 업데이트if (distances[edge.from] != Int.MAX_VALUE && distances[edge.from] + edge.weight < distances[edge.to]) { distances[edge.to] = distances[edge.from] + edge.weight updated =true } }// 더 이상 업데이트가 발생하지 않으면 조기 종료if (!updated) break }// 음수 사이클 존재 여부 검사for (edge in edges) {if (distances[edge.from] != Int.MAX_VALUE && distances[edge.from] + edge.weight < distances[edge.to]) {// 음수 사이클이 존재하면 true 반환returnPair(distances, true) } }// 음수 사이클이 없으면 false 반환returnPair(distances, false)}funmain() {// 노드의 개수와 간선의 개수 설정val vertexCount =5val edgeList =mutableListOf<Edge>()// 간선 추가 (예시) edgeList.add(Edge(0, 1, 6)) // 노드 0에서 노드 1로 가중치 6인 간선 edgeList.add(Edge(0, 2, 7)) // 노드 0에서 노드 2로 가중치 7인 간선 edgeList.add(Edge(1, 2, 8)) // 노드 1에서 노드 2로 가중치 8인 간선 edgeList.add(Edge(1, 3, 5)) // 노드 1에서 노드 3으로 가중치 5인 간선 edgeList.add(Edge(1, 4, -4)) // 노드 1에서 노드 4로 가중치 -4인 간선 edgeList.add(Edge(2, 3, -3)) // 노드 2에서 노드 3으로 가중치 -3인 간선 edgeList.add(Edge(2, 4, 9)) // 노드 2에서 노드 4로 가중치 9인 간선 edgeList.add(Edge(3, 1, -2)) // 노드 3에서 노드 1로 가중치 -2인 간선 edgeList.add(Edge(4, 0, 2)) // 노드 4에서 노드 0으로 가중치 2인 간선 edgeList.add(Edge(4, 3, 7)) // 노드 4에서 노드 3으로 가중치 7인 간선val startNode =0// 시작 노드 설정// 벨만-포드 알고리즘 실행val (distances, hasNegativeCycle) =bellmanFord(edgeList, vertexCount, startNode)if (hasNegativeCycle) {println("음수 사이클이 존재합니다.") } else {// 결과 출력for (i in distances.indices) {val distanceStr =if (distances[i] == Int.MAX_VALUE) "도달 불가"else distances[i].toString()println("노드 $startNode 에서 노드 $i 까지의 최단 거리: $distanceStr") } }}
문제 1. 게임 맵 최단 거리
설명
ROR 게임은 두 팀으로 나누어서 진행하며 상대 팀 진영을 먼저 파괴하면 이기는 게임입니다. 그러므로 각 팀은 상대팀 진영에 최대한 빨리 도착해야 게임을 유리하게 이끌 수 있습니다. 지금부터 당신은 어느 한팀의 팀원이 되어 게임을 진행하려고 합니다. 다음은 5x5 크기의 맵에 당신의 캐릭터가 (1,1) 위치에 있고 상대 팀의 진영은 (5,5) 위치에 있는 예를 보여줍니다.
위 그림에서 검은색은 벽이고, 흰색은 길입니다. 캐릭터가 움직일 때는 동, 서, 남, 북 방향으로 한 칸씩 이동하며 맵을 벗어날 수 없습니다. 다음은 현재 상황에서 캐릭터가 상태 팀 진영으로 가는 2가지 방법을 보여줍니다. 그림에서 보듯 첫 번째 방법이 상대 팀 진영에 도착하는 방법입니다.
만약 이렇게 벽이 세워져 있다면 캐릭터는 상대 팀 진영에 도착할 수 없습니다.
게임 맵이 maps로 주어질 때 우리 팀 캐릭터가 상대 팀 진영에 도착하기 위해 지나가야 하는 길의 최소 개수를 반환하는 solution() 함수를 완성하세요. 만약 상대 팀 진영에 도착할 수 없다면 -1을 반환하세요.
funmain() {classNode(val row: Int, val col: Int)funsolution(maps: List<List<Int>>): Int {// 1. 이동할 수 있는 방향을 나타내는 리스트 생성val direction =listOf(0 to 1, 0 to -1, 1 to 0, -1 to 0)// 2. 맵의 크기를 저장val n = maps.sizeval m = maps[0].size// 3. 최단 거리를 저장할 배열 생성val distance =MutableList(n) { MutableList(m) { 0 } }// 4. bfs 탐색을 위한 큐 생성val queue =ArrayDeque<Node>()// 5. 시작 정점에 대해서 큐에 추가, 최단 거리 저장 queue.addLast(Node(0, 0)) distance[0][0] =1// 6. queue가 빌 때까지 반복while (queue.isNotEmpty()) {val now = queue.removeFirst()// 7. 현재 위치에서 이동할 수 있는 모든 방향for (i in0..3) {val nextRow = now.row + direction[i].firstval nextCol = now.col + direction[i].second// 8. 맵 밖으로 나가는 경우 예외 처리if (nextRow <0|| nextCol <0|| nextRow >= n || nextCol >= m) continue// 9. 벽으로 가는 경우 예외 처리if (maps[nextRow][nextCol] ==0) continue// 10. 이동한 위치가 처음 방문하는 경우, queue에 추가하고 거리 갱신if (distance[nextRow][nextCol] ==0) { queue.addLast(Node(nextRow, nextCol)) distance[nextRow][nextCol] = distance[now.row][now.col] +1 } distance.forEach { it.forEach { print("$it ") }; println() }println() } }returnif (distance[n -1][m -1] ==0) -1else distance[n -1][m -1] }val maps =listOf(listOf(1, 0, 1, 1, 1),listOf(1, 0, 1, 0, 1),listOf(1, 0, 1, 1, 1),listOf(1, 1, 1, 0, 1),listOf(0, 0, 0, 0, 1) )println(solution(maps))}
문제 2. 네트워크
설명
네트워크란 컴퓨터 상호 간에 정보를 교환하도록 연결된 어떤 형태를 의미합니다. 예를 들어 컴퓨터 A, B가 직접 연결되어 있고 컴퓨터 B, C가 직접 연결되어 있을 때 컴퓨터 A, C는 간접 연결되어 있어 정보를 교환할 수 있습니다. 그러면 컴퓨터 A, B, C는 모두 같은 네트워크 상에 있다고 할 수 있습니다. 컴퓨터 개수가 n, 연결 정보가 담긴 2차원 배열 computers가 주어질 때 네트워크 개수를 반환하는 solution() 함수를 작성하세요.
제약 조건
컴퓨터의 개수 n은 1 이상 200 이하인 자연수입니다.
각 컴퓨터는 0부터 n - 1인 정수로 표현합니다.
i번 컴퓨터와 j번 컴퓨터가 연결되어 있으면 computers[i][j]를 1로 표현합니다.
computers[i][i]는 항상 1입니다.
입출력의 예
n
computers
return
3
[[1, 1, 0], [1, 1, 0], [0, 0, 1]]
2
3
[[1, 1, 0], [1, 1, 1], [0, 1, 1]]
1
코드
funsolution(n: Int, computers: Array<IntArray>): Int {// 네트워크 수를 컴퓨터의 개수로 초기화var count = computers.size// computers 배열을 복제하여 그래프 생성val graph = computers.clone()// 컴퓨터 방문 여부를 저장할 배열 초기화val visited =Array(count) { false }// 자기 자신과의 연결을 제거 (대각선 요소를 0으로 설정)for (i in graph.indices) { graph[i][i] =0 }// 깊이 우선 탐색 함수 정의fundfs(i: Int, j: Int) {// i와 j 사이의 연결을 제거하여 중복 방문 방지 graph[i][j] =0 graph[j][i] =0// 컴퓨터 i를 방문으로 표시 visited[i] =true// 컴퓨터 j가 방문되지 않았다면if (!visited[j]) {// 컴퓨터 j를 방문으로 표시하고 네트워크 수 감소 visited[j] =true count-- }// 컴퓨터 j에 연결된 모든 컴퓨터를 탐색for (k in graph[j].indices) {// 연결이 없으면 다음으로 이동if (graph[j][k] ==0) continue// 연결이 있으면 재귀적으로 dfs 호출elsedfs(j, k) } }// 그래프를 순회하며 연결된 컴퓨터를 찾음for (i in graph.indices) {for (j in graph[i].indices) {// i와 j가 연결되어 있으면 dfs 호출if (graph[i][j] ==1) {dfs(i, j) } } }return count}