물 붓기-절차 적 및 기능적 Kotlin

(Filippo Scognamiglio) ( 2019 년 9 월 20 일)

다른 것을 시도 해보자! 이 게시물에서는 Kotlin을 사용하여 유명한 “물 붓기”문제를 절차 적 방식과 기능적 방식으로 두 번 해결하려고합니다.

문제

표준”물 붓기 “퍼즐에는 알려진 용량이 다른 * n * 잔이 주어집니다. 하나 이상의 안경에서 원하는 수량으로 이어지는 작업 목록을 찾아야합니다. 다음 세 가지 작업 만 수행 할 수 있습니다.

  • 잔을 완전히 채우십시오
  • 잔을 완전히 비우십시오
  • 첫 번째는 비어 있거나 두 번째는 꽉 찼습니다

(나는 당신이 생각하는 것을 압니다… 당신은 잔을“반”으로 채워서 속일 수 없습니다… 나는 당신을 볼 수 있습니다).

도메인

도메인 특성화부터 시작하겠습니다. 다음 데이터 클래스를 사용하여 상태를 표시합니다.

data class Glass(val capacity: Int, val value: Int)
data class State(val glasses: List)

다음으로 현재 상태를 다음으로 변환하는 메서드가있는 추상 클래스를 사용하여 이동을 정의합니다. 수정 된 클래스 :

sealed class Move {
abstract fun update(state: State): State
}

봉인 된 클래스에는 세 가지 가능한 구현이 있습니다.

data class Fill(val index: Int): Move() {
override fun update(state: State): State =
State(state.glasses.mapAtIndex(index) {
it.copy(value = it.capacity)
})
}

data class Empty(val index: Int): Move() {
override fun update(state: State): State =
State(state.glasses.mapAtIndex(index) {
it.copy(value = 0)
})
}

data class Pour(val from: Int, val to: Int): Move() {
override fun update(state: State): State {
val fromGlass = state.glasses[from]
val toGlass = state.glasses[to] val maxFromQuantity = fromGlass.value
val maxToQuantity = toGlass.capacity - toGlass.value val quantity = min(maxFromQuantity, maxToQuantity)

return State(
state.glasses
.mapAtIndex(from) {
it.copy(value = it.value - quantity)
}
.mapAtIndex(to) {
it.copy(value = it.value + quantity)
}
)
}
}// Utility extension function
private fun List.mapAtIndex(index: Int, f: (T) -> T) =
this.mapIndexed { i, t -> if (i == index) f(t) else t }

또한 최종 상태로 이동하는 동작 목록을 나타내는 데이터 구조가 필요합니다.

data class Path(val finalState: State, val moves: List) {
fun extend(move: Move): Path {
val nextState = move.update(finalState)
val nextMoves = moves + move
return copy(finalState = nextState, moves = nextMoves)
}
}

이 모든 클래스는 설계 상 변경 불가능합니다. 다음 구현에서이를 활용합니다.

절차 적 구현

이 문제는 그래프로 표현할 수 있습니다. 여기서 각 상태는 노드이고 각 이동은 에지 연결입니다. 두 개의 이웃 국가. Breath First Search 알고리즘을 사용하여 탐색하여 해결책을 찾을 것입니다.

가능한 모든 동작을 생성하여 시작하겠습니다. 모든 잔을 채우고 모든 잔을 비우고 모든 잔을 다른 잔에 부을 수 있습니다.

private fun generateMoves(): List {
val result = mutableListOf()

val indices = initialState.glasses.indices

for (i in indices) {
result.add(Move.Fill(i))
result.add(Move.Empty(i))
}

for (i in indices) {
for (j in indices) {
if (i != j)
result.add(Move.Pour(i, j))
}
}

return result
}

이제 solve 메소드는 원하는 수량을 가져와 솔루션을 정의하는 최단 이동 목록을 반환합니다.

모든 미 탐색 상태를 MutableList . 처음에는 초기 상태 만 포함합니다.

그런 다음 반복을 시작합니다. , 각 단계에서 우리는 다음을 수행합니다.

  • 개척지에서 머리를 터뜨립니다. 이 상태가 해결책이라면 우리는 완료된 것입니다. 상태가 이미 탐색 된 경우 건너 뜁니다.
  • 탐색 된 상태 집합에이 상태를 추가합니다 (모든 종류의 순환을 방지 함).
  • 적용하여 가능한 모든 인접 상태를 계산합니다. 가능한 모든 이동을 수행하고이를 최전선 끝에 추가합니다.

해결책을 찾거나 전체 검색 공간을 탐색하면 완료됩니다.

fun solve(amount: Int): Path? {
val explored = mutableSetOf()
val frontier = mutableListOf(Path(initialState, listOf()))

while (frontier.isNotEmpty()) {
val currentPath = frontier.removeAt(0)

if (isSolution(amount, currentPath.finalState)) {
return currentPath
}

if (explored.contains(currentPath.finalState)) {
continue
}

explored.add(currentPath.finalState)

for (move in moves) {
val nextPath = currentPath.extend(move)
frontier.add(nextPath)
}
}

return null
}

기능 구현

함수 구현으로 이동하겠습니다. 전체 구조는 유사하며 먼저 generateMoves 메소드를 변경합니다.

private fun generateMoves(): List {
val indices = initialState.glasses.indices

val fillMoves = indices.map { Move.Fill(it) }
val emptyMoves = indices.map { Move.Empty(it) }
val pourMoves = indices
.flatMap { i -> indices.map { j -> i to j } }
.filter { (i, j) -> i != j }
.map { (i, j) -> Move.Pour(i, j) }

return fillMoves + emptyMoves + pourMoves
}

기능적 해결 의 경우 Martin Odersky .

재귀 함수를 정의합니다. nextPaths n 인 경로 목록이 제공된 div>는 길이가 n + 1 인 경로를 포함하는 목록 시퀀스를 반환합니다.

이 작업은 가능한 모든 이동으로 각 입력 경로에서 extend 메소드를 호출합니다 (탐색 된 상태를 필터링 함).

이 방법으로 우리는 최단 경로가 먼저 처리되고 숨 우선 검색의 또 다른 구현으로 이어집니다.

또한 Kotlin 시퀀스는 지연 적으로 평가 되므로 더 긴 경로는 최단 경로가 문제에 대한 해결책이 아닙니다.

fun solve(amount: Int): Path? {
val firstPath = Path(initialState, listOf()) return nextPaths(listOf(firstPath), setOf())
.flatten()
.filter { isSolution(amount, it.finalState) }
.firstOrNull()
}

fun nextPaths(paths: List, explored: Set): Sequence> {
return if (paths.isEmpty()) emptySequence()
else {
sequence {
val nextPaths = paths.flatMap {
extendWithMoves(it, explored)
}
val nextExplored = explored + paths.map {
it.finalState
}

yield(nextPaths)
yieldAll(nextPaths(nextPaths, nextExplored))
}
}
}

fun extendWithMoves(path: Path, explored: Set): List {
return moves
.map { path.extend(it) }
.filter { !explored.contains(it.finalState) }
}

결론

이 짧은 게시물의 요점을 강조해 보겠습니다.

  • 두 구현 모두 주어진 테스트 케이스에서 작동했습니다.
  • 절차 적 구현은 동일한 입력에서 2 ~ 3 배 더 빨랐습니다.
  • 기능 구현은 더 간결하고 분명했습니다.
  • 다중 코어를 활용하도록 기능적 구현을 ​​쉽게 최적화 할 수 있습니다.

어떤 것이 가장 좋은 접근 방식인지 찾기가 쉽지 않으며 함수형 프로그래밍이 은색이 아닙니다. 코딩의 총알.

좋은 소식은 은색 총알이 필요 없다는 것입니다 (고대적이고 변신하고 광대처럼 보이는 생물을 물리 칠 필요가없는 한). 다중 패러다임 프로그래밍을 사용하려면 사치 스러워야합니다. 작업에 적합한 도구를 쉽게 선택할 수 있습니다.

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다