///
Search
▶️

그래프 알고리즘(Graph Algorithm)

1. 그래프 알고리즘(Graph Algorithm)

2. 최단 경로 알고리즘(Shortest Path Finding Algorithm)

Shortest Path Finding Algorithm
그래프를 이용한 최단경로 알고리즘은 간선의 가중치가 존재하는 그래프에서의 최단 경로를 찾는 알고리즘입니다.
간선의 가중치가 없는 그래프에서는 BFS 알고리즘으로 최단 경로를 찾을 수 있습니다.
반면 가중치가 존재하게 되면, A에서 B까지 간선의 가중치의 합이 최소가 되는 경로가 최단 경로가 됩니다.
위 문제를 해결하는 대표적인 3가지 그래프 알고리즘이 있습니다.
다익스트라 알고리즘(Dijkstra's Algorithm), 플로이드 알고리즘(Floyd Algorithm), 벨만포드 알고리즘(Bellman-Ford Algorithm)
최단 경로의 특징 : 정점 X 까지 최단거리로 가기 위해서는 그 직전 까지도 최단거리로 가야하며, 그 직전까지의 최단거리에서 직전부터 X까지의 최단 거리를 구함으로써 문제를 해결할 수 있습니다.

3. 최단 경로 알고리즘 - 다익스트라 알고리즘(Dijkstra's Algorithm)

다익스트라 알고리즘(Dijkstra's Algorithm)
가중치가 존재하는 그래프에서의 최단 경로를 구하는 알고리즘 중 가장 대표적인 알고리즘입니다.
가중치가 양수일때만 가능
한 정점으로부터 모든 정점까지의 최단 거리를 구할 수 있습니다.
최단 경로는 유일하기 때문에 사이클이 없는 그래프 즉, 트리를 만들 수 있습니다.(최단 경로 트리)

4. 다익스트라 알고리즘 구현

Tip
시작점은 가중치의 합이 0 즉, 최단 경로가 0이 됩니다.
시작점부터 현재 가중치의 합이 최소인 지점의 정점 번호를 찾고, 방문처리를 합니다.
그 후 뻗어 나갈 수 있는 인접한 정점에 대한 가중치와 시작점에서 인접한 정점까지의 가중치의 합 중 최솟값을 각 정점에 해당하는 가중치 배열에 저장합니다.
다시 현재 방문하지 않은 정점 중에서 최솟값을 찾고 방문처리를 합니다.
위 과정을 모든 정점에 대해 반복하면, 시작점으로부터 모든 정점의 최단 경로를 구할 수 있습니다.
func dijkstra(_ N: Int, _ start: Int, _ end: Int) -> Int { var distances: [Int] = Array(repeating: 987987987, count: N + 10) distances[start] = 0 for _ in 0..<N { var minCost, minIndex: Int minCost = 987987987 minIndex = -1 for j in 0..<N { if !visited[j] && minCost > distances[j] { minCost = distances[j] minIndex = j } } visited[minIndex] = true for j in 0..<graph[minIndex].edges.count { let y: Int = graph[minIndex].edges[j] let cost: Int = graph[minIndex].costs[j] if distances[y] > distances[minIndex] + cost { distances[y] = distances[minIndex] + cost } } } return distances[end] }
Swift
복사

5. 최소 신장 트리(Minimum Spanning Tree)

Minimum Spanning Tree
스패닝 트리 : 그래프의 모든 노드를 포함하는 트리(사이클이 없는 그래프)
스패닝 트리의 모든 가중치의 합을 스패닝 트리의 가중치라고 합니다.
여러 스패닝 트리의 가중치 중 그 값이 최소가 되는 스패닝 트리를 최소 신장(스패닝) 트리라고 합니다.
최소 신장 트리를 구하는 알고리즘으로 대표적인 크루스칼 알고리즘(Kruskal's Algorithm)이 있습니다.

6. 최소 신장 트리 - 크루스칼 알고리즘(Kruskal's Algorithm)

크루스칼 알고리즘(Kruskal's Algorithm)
Cut Property : 최소 신장 트리를 구하기 위해서 가장 먼저 알아야할 개념, 두 개의 Tree를 하나의 간선으로 연결할 때, 그 가중치가 가장 작은 간선으로 연결하는 것
크루스칼 알고리즘은 각 노드를 하나의 MST(Minimum Spanning Tree)로 보고 시작, 간선의 가중치가 작은 것부터 결정해 나가되, 사이클이 없어야 합니다.
사이클이 생기는 조건은 서로 다른 트리를 잇는 것은 문제되지 않으나, 하나의 트리 내에서 하나의 간선만 추가하더라도 사이클이 생기게 됩니다.
따라서 두 노드가 같은 트리 내에 있는지, 즉 사이클이 생기는지 여부를 판단하기 위해 Disjoint Set이라는 자료구조를 활용할 수 있습니다.
Disjoint Set은 두 정점 혹은 두 트리(그룹)을 하나의 트리(그룹)으로 묶는 작업과, 두 정점이 같은 트리(그룹)에 속하는지 여부를 알 수 있는 작업 등 두개의 작업을 담당하는 자료구조입니다.
Disjoint Set은 트리의 부모 자식 관계를 이용하여 구현할 수 있고, 두 트리(그룹)을 하나의 트리(그룹)로 묶을 때는 각 트리(그룹)의 대표 즉, 루트 노드를 이어 붙이게 됩니다.
그리고 두 정점이 같은 그룹인지 알고 싶을 때는 그 정점의 대표 즉, 루트 노드를 비교해봄으로써 알 수 있습니다.
따라서 크루스칼 알고리즘을 구현하기 위해서 간선의 가중치가 오름차순으로 정렬되어 있어야 하고, Disjoin Set 자료구조를 이용하여 사이클 여부를 판단해야 합니다.
한가지 Tip은 이제껏 그래프를 구현할 때 인접 행렬 혹은 인접 리스트로 구현해왔지만, 크루스칼 알고리즘의 경우는 간선에 관심이 있기 때문에 구조체로 시작점과 끝점, 그리고 가중치로 이루어진 구조체로 그래프를 구현하는 것이 구현에 있어 용이합니다.
//MARK: - Type struct Edge { //MARK: - Property var p, q, cost: Int //MARK: - Initializer init(_ p: Int, _ q: Int, _ cost: Int) { self.p = p self.q = q self.cost = cost } } //MARK: - Variable var parent: [Int] = [] //MARK: - Function func find(_ x: Int) -> Int { if x == parent[x] { return x } let y: Int = find(parent[x]) parent[x] = y return y } func unionFind(_ p: Int, _ q: Int) -> Bool { let rootP: Int = find(p) let rootQ: Int = find(q) if rootP == rootQ { return false } parent[rootP] = rootQ return true } func solution() -> Void { //MARK: - Input guard let input: [String] = readLine()?.components(separatedBy: " ") else { return } let N: Int = input.map { Int($0) }[0] ?? 0 let M: Int = input.map { Int($0) }[1] ?? 0 var result: Int = 0 var edges: [Edge] = [] parent = Array(repeating: 0, count: N + 10) for i in stride(from: 1, through: N, by: 1) { parent[i] = i } for _ in 0..<M { guard let inputData: [String] = readLine()?.components(separatedBy: " ") else { return } let a: Int = inputData.map { Int($0) }[0] ?? 0 let b: Int = inputData.map { Int($0) }[1] ?? 0 let c: Int = inputData.map { Int($0) }[2] ?? 0 edges.append(Edge(a, b, c)) } //MARK: - Process edges.sort { $0.cost < $1.cost } for edge in edges { result += unionFind(edge.p, edge.q) ? edge.cost : 0 } //MARK: - Output print(result) }
Swift
복사

7. 크루스칼 알고리즘과 다익스트라 알고리즘의 차이

Tip
다익스트라 알고리즘은 특정 노드에서 모든 노드로의 최단 거리를 위한 알고리즘이기 때문에, 결론적으로 그래프에서 최단 경로 트리를 구성하지만 시작점에 따라 그 경로가 달라질 수 있습니다. 따라서 무조건 최소 신장 트리를 만든다는 보장할 수 없습니다.
크루스칼 알고리즘은 그래프에서 모든 노드를 잇는 스패닝 트리 중 그 가중치의 합이 최소인 트리를 구하는 것이기 때문에, A에서 B까지의 최단 거리를 보장할 수는 없습니다. 따라서 두 알고리즘의 목적이 다르다고 할 수 있습니다.

8. SCC(Strongly Connected Component)

SCC
Connected Component : 무방향 그래프에서 연결되어 있는 그래프들의 그룹으로 나누는 것, DFS / BFS 즉, 그래프 순회로 Connected Component를 구할 수 있습니다.(서로 연결되어 있는 그래프의 노드들이 누구인지, 그 그룹의 수가 몇 개인지 구할 수 있음)
Strongly Connected Component, SCC : 방향 그래프에서는 연결되어 있다 하더라도, 돌아오지 못하는 경우가 있을 수 있으므로 같은 그룹의 노드라면 오고갈 수 있는 경로가 있는 그룹끼리 나누는 것을 SCC라고 합니다.
위 SCC를 구하는(오고갈 수 있는 경로가 항상 존재하는 서로 연결되어 있는 그래프의 노드들이 누구인지, 그 그룹이 몇 개인지 구하는) 대표적인 알고리즘이 코사라주 알고리즘입니다.

9. SCC - 코사라주 알고리즘(Kosaraju's Algorithm)

코사라주 알고리즘(Kosaraju's Algorithm)
코사라주 알고리즘을 구현하기 앞서 메타 그래프에 대해 알아야 하는데, 현재 SCC를 안다고 할 때 SCC끼리 하나의 노드로 보는 그래프를 말합니다. (Cycle이 없어야 합니다.)
따라서 메타 그래프에 대해 DFS 알고리즘을 2번 적용하게 되는데, 처음 DFS를 적용하면서 빠져나오는 노드의 번호를 차례로 저장하고,
위 과정을 거치고 난 후, 초기 그래프의 간선의 방향을 모두 바꾸게 되면 가장 나중에 빠져나온 노드 즉, 루트 노드가 속한 그룹이 메타 그래프의 단말 노드가 되는 것을 알 수 있습니다.
따라서 가장 나중에 빠져 나오는 노드 즉, 단말 노드이기 때문에 더이상 내려갈 곳이 없으므로 그 노드 부터 DFS를 한번 더 적용하기 위해서 간선의 방향이 반대인 그래프를 따로 저장할 필요가 있겠습니다.
간선의 방향이 반대인 그래프에 대해 가장 나중에 빠져 나온 단말 노드 부터 다시 한번 DFS를 적용하게 되면, SCC를 구할 수 있습니다.(서로 오고갈 수 있는 경로가 존재하는 노드들끼리 이루어진 그래프와, 그 개수)
따라서 코사라주 알고리즘은 DFS를 2번 적용한 알고리즘으로써 O(V + E) 시간복잡도를 가지게 됩니다.
//MARK: - Type struct Graph { //MARK: - Property var edges: [Int] //MARK: - Init init() { self.edges = [] } } //MARK: - Variable var graph: [Graph] = [], reverseGraph: [Graph] = [] var visited: [Bool] = [] var orders: [Int] = [], groups: [Int] = [] var orderCount: Int = 0, groupCount: Int = 0 //MARK: - Function func graphDFS(_ x: Int) -> Void { visited[x] = true for i in 0..<graph[x].edges.count { let y: Int = graph[x].edges[i] if !visited[y] { graphDFS(y) } } orders[orderCount] = x orderCount += 1 } func reverseGraphDFS(_ x: Int) -> Void { visited[x] = true groups[x] = groupCount for i in 0..<reverseGraph[x].edges.count { let y: Int = reverseGraph[x].edges[i] if !visited[y] { reverseGraphDFS(y) } } } func solution() -> Void { //MARK: - Input guard let input: [String] = readLine()?.components(separatedBy: " ") else { return } let N: Int = input.map { Int($0) }[0] ?? 0 let M: Int = input.map { Int($0) }[1] ?? 0 graph = Array(repeating: Graph(), count: N + 10) reverseGraph = Array(repeating: Graph(), count: N + 10) visited = Array(repeating: false, count: N + 10) orders = Array(repeating: 0, count: N + 10) groups = Array(repeating: 0, count: N + 10) for _ in 0..<M { guard let inputData: [String] = readLine()?.components(separatedBy: " ") else { return } let a: Int = inputData.map { Int($0) }[0] ?? 0 let b: Int = inputData.map { Int($0) }[1] ?? 0 graph[a].edges.append(b) reverseGraph[b].edges.append(a) } //MARK: - Process for i in stride(from: 1, through: N, by: 1) { if !visited[i] { graphDFS(i) } } visited = Array(repeating: false, count: N + 10) for i in stride(from: orderCount - 1, through: 0, by: -1) { if !visited[orders[i]] { reverseGraphDFS(orders[i]) groupCount += 1 } } //MARK: - Output print(groupCount) }
Swift
복사