코딩테스트 하나를 봤다. 4시간이었고 두 문제였다. 하나는 트리로 잘(왜 이 말이 불안하지..) 풀었고, 하나는 음.... 이것저것 끄적거리기는 했지만 결과적으로 못 풀었다.
좌표평면의 사각형을 움직일 수 있을 때까지 움직이는 문제였다. 간단해보이면서 코드로 구현하려니 아주 죽을 맛이었다. 이런 문제 유형을 만나보니 여기서 사용할 수 있는 알고리즘을 배워놓으면 쓸만한 곳이 많겠다는 생각이 들었다.
그래도 예전에는 트리를 사용할 줄도 모르던 내가 트리로 하나 풀어냈다는 것은 만족스러웠다.
그러므로 오늘은 그래프를 한다. 트리는 많이 했으니까.
그래프는 예전에 말했듯 두 개의 요소로 이루어져 있다.
3 개가 있다.
구현체로는 AdjacencyList, AdjacencyMatrix 가 있다.
matrix[row][column]
에서 row 는 간선의 출발점, column 은 도착점이 된다. 반환 값은 가중치 혹은 정점의 데이터가 된다.참고로 구현체 각각의 시간복잡도는 다음과 같다.
Operations | Adjacency List | Adjacency Matrix |
---|---|---|
Storage Space (공간 복잡도/확장) | O(V+E) | O(V2) |
Add Vertex (정점 삽입) | O(1) | O(V2) |
Add Edge (간선 삽입) | O(1) | O(1) |
Finding Edges and Weight (간선 혹은 가중치를 검색) | O(V) | O(1) |
마지막으로 그래프의 API 는 다음과 같다.
간선의 수를 기준으로 정해볼 수 있다.
그래프를 구현할 때는 다음의 과정을 거친다.
struct Vertext<T> {
let index: Int
let data: T
}
// 필요 시
extension Vertex: Equatable where T: Equatable {}
extension Vertex: Hashable where T: Hashable {}
struct Edge<T> {
let source: Vertex<T>
let destination: Vertex<T>
let weight: Double?
}
extension Edge: Equatable where T: Equatable {}
protocol Graph {
associatedtype Element
// 정점 만들기
func createVertex(data: Element) -> Vertex<Element>
// 간선(단방향) 만들기
func addDirectedEdge(
from source: Vertex<Element>,
to destination: Vertex<Element>
weight: Double?)
// 간선(양방향) 만들기
func addUndirectedEdge(
between source: Vertex<Element>,
and destination: Vertex<Element>,
weight: Double?)
// 정점 사이에 정점 끼워넣기
func add(
_ edge: EdgeType,
from source: Vertex<Element>,
to destination: Vertex<Element>,
weight: Double?)
// 시작(정)점부터 탐색이 끝나기 까지의 간선들을 모두 반환
func edges(from source: Vertex<Element>) -> [Edge<Element>]
// 시작(정)점부터 끝(정)점까지의 가중치(탐색비용)을 반환
func weights(
from source: Vertex<Element>,
to destination: Vertex<Element>) -> Double?
}
extension Graph {
func addUndirectedEdge(
between source: Vertex<Element>,
and destination: Vertex<Element>,
weight: Double?) {
addDirectedEdge(from: source, to: destination, weight: weight)
addDirectedEdge(from: destination, to: source, weight: weight)
}
func add(
_ edge: EdgeType,
from source: Vertex<Element>,
to destination: Vertex<Element>,
weight: Double?) {
switch edge {
case .directed:
addDirectedEdge(from: source, to: destination, weight: weight)
case .undirected:
addUndirectedEdge(between: source, and: destination, weight: weight)
}
}
}
extension Graph where Element: Hashable {
class AdjacencyList<T: Hashable>: Graph {
private var adjacencies: [Vertex<T>: [Edge<T>]] = [:]
init() {}
func createVertex(data: T) -> Vertex<T> {
let vertex = Vertex(index: adjacencies.count, data: data)
adjacencies[vertex] = []
return vertex
}
func addDirectedEdge(
from source: Vertex<T>,
to destination: Vertex<T>,
weight: Double?) {
let edge = Edge(
source: source,
destination: destination,
weight: weight)
adjacencies[source]?.append(edge)
}
func edges(from source: Vertex<T>) -> [Edge<T>] {
adjacencies[source] ?? []
}
func weight(
from source: Vertex<T>,
to destination: Vertex<T>) -> Double? {
edges(from: source)
.first({ $0.destination == destination})?
.weight
}
}
class AdjacencyMatrix<T: Hashable>: Graph {
private var vertices: [Vertex<T>] = []
private var weights: [[Double?]] = []
init() {}
func createVertex(data: T) -> Vertex<T> {
let vertex = Vertex(index: vertices.count, data: data)
vertices.append(vertex)
for i in 0..<weights.count {
weights[i].append(nil) // Add Column
}
let row = [Double?](repeating: nil, count: vertices.count)
weights.append(row)
return vertex
}
func addDirectedEdge(
from source: Vertex<T>,
to destination: Vertex<T>,
weight: Double?) {
weights[source.index][destination.index] = weight
}
func edges(from source: Vertex<T>) -> [Edge<T>] {
var edges: [Edge<T>] = []
for column in 0..<weights.count {
guard let weight = weights[source.index][column] else {
continue
}
edges.append(
Edge(
source: source,
destination: vertices[column],
weight: weight)
)
}
return edges
}
func weight(
from source: Vertex<T>,
to destination: Vertex<T>) -> Double? {
weights[source.index][destination.index]
}
}
구현만 한번 쭉 훑어보았다. 하지만 이걸론 부족하다.
요즘 코딩테스트 마스터가 되기 위해 광야를 떠도는 느낌으로 공부를 하면서 느낀 것이 있다.
어떤 주제든 문제 2개는 풀어봐야 익힐 수 있다. 답을 봐도 괜찮고 안보고 풀었다면 칭찬 드림.
그러므로 지금 구현한 인접행렬이나 인접배열을 이용해서 풀 수 있는 문제를 찾아보기로 했다.
전체적으로 느껴진 점은 경로찾기
문제가 아주 적당할 것 같다. 백준에 아주 재미있는 문제를 봤는데, 알고리즘 수업
이라는 카테고리로 분류되어 있는 문제들인 것 같다. 의사코드를 제시해주고 풀이에 따른 성능 차이를 비교하라는 것이다.
문제 : https://www.acmicpc.net/problem/24418
인접행렬에서 Greedy 알고리즘을 통해 가장 큰 값을 반환하는 방법으로 재귀
, 동적 프로그래밍
이 있다는 점을 설명해주고 있다. 실제 시간복잡도를 비교하기 위해 다음의 의사코드를 Swift 로 구현해 보고 #코드1, #코드2 라고 명시된 부분이 실행되는 횟수를 반환하는 것이다.
# 행렬 경로 문제 재귀호출 의사 코드는 다음과 같다.
matrix_path(m[][], n) { # (1, 1)에서 (n, n)에 이르는 최고 점수를 구한다.
return matrix_path_rec(m, n, n);
}
matrix_path_rec(m[][], i, j) { # (1, 1)에서 (i, j)에 이르는 최고 점수를 구한다.
if (i == 0 or j == 0) then
return 0; # 코드1
else
return (mij + (max(matrix_path(m, i-1, j), matrix_path(m, i, j-1))));
}
# 행렬 경로 문제 동적 프로그래밍 의사 코드는 다음과 같다.
matrix_path(m[][], n) { # (1, 1)에서 (n, n)에 이르는 최고 점수를 구한다.
for i <- 0 to n
d[i, 0] <- 0;
for j <- 1 to n
d[0, j] <- 0;
for i <- 1 to n
for j <- 1 to n
d[i, j] <- mij + max(d[i - 1, j], d[i, j - 1]); # 코드2
return d[n, n];
}
읽는 데 좀 무리가 있었지만 여차저차 구현하기는 했다. 예를 들어 동적 프로그래밍 구현을 보면 인덱스를 0부터 세는 것은 좋은데, #코드2 를 실행하는 부분을 보면 IndexOutOfRange 가 발생한다. 길이가 5인 배열에 인덱스 5를 지정하면 말이다.
여차저차 구현은 했고 답은 틀렸다.... 왜 틀리지???
// 재귀를 이용한 탐색. 의사 코드 이용.
func traversalCountOfRecursionPaths(
from matrix: [[Int]],
row: Int,
column: Int,
result numberOfCount: inout Int) -> Int { // numberOfCount 는 실제 코드를 실행할 때마다 추가되는 inout 파라미터이다.
guard matrix.reduce(0, { $0 + $1.count }) >= 4 else {
return matrix.first?.first ?? 0
}
if row == 0 || column == 0 {
numberOfCount += 1
return 0
}
let result = matrix[row][column]
let maxResult = max(
traversalCountOfRecursionPaths(
from: matrix,
row: row-1,
column: column,
result: &numberOfCount),
traversalCountOfRecursionPaths(
from: matrix,
row: row,
column: column-1,
result: &numberOfCount)
)
return result + maxResult
}
// 동적 프로그래밍을 이용한 탐색. 의사 코드 이용.
func traversalCountOfDynamicPaths(
from matrix: [[Int]],
row: Int,
column: Int,
result numberOfCount: inout Int) -> Int {
guard matrix.reduce(0, { $0 + $1.count }) >= 4, row >= 2, column >= 2 else {
return matrix.first?.first ?? 0
}
var matrix = matrix
for i in 0..<n {
numberOfCount += 1
matrix[i][0] = 0
guard i != 0 else { continue }
numberOfCount += 1
matrix[0][i] = 0
}
for i in 1..<n {
for j in 1..<n {
numberOfCount += 1
matrix[i][j] = matrix[i][j] + max(matrix[i-1][j], matrix[i][j-1])
}
}
return matrix[n-1][n-1]
}
이리저리 돌려보고 굴려봤지만 답이 나오질 않는다.
우선 여기까지 정리하고 넘어가야 할 것 같다. 공부할 다른 것들이 많은데 여기서 시간을 계속 잡아먹을 수는 없다. 근데 궁금증이 없는 건 아니다.
의사코드 자체를 그냥 코드로 옮긴다는 아이디어 자체가 틀렸을 수도 있다. 머리 좀 식히고 다시 돌아와야겠다.
광야를 떠도는 코딩테스트 마스터(예정) 의 여정은 아직 끝나기엔 멀었다.