我第一次看到以程序解决的浇水问题是Martin Odersky在Coursera上进行的关于功能程序设计的精彩演讲 。 该解决方案展示了
使用Scala在Streams中进行惰性评估 。
使用Kotlin解决倒水问题
我想探索如何使用Kotlin重写Martin Odersky所描述的解决方案,并且我意识到了两件事–一是Kotlin提供的不可变数据结构只是Java Collections库的包装并且不是真正的不可变,其次是使用Streams的解决方案。 Java中的功能将很困难。 但是, Vavr提供了一个很好的替代方案–一流的Immutable集合库和Streams库,我在用Kotlin和Vavr复制解决方案方面大胆尝试。
杯子看起来像这样,用Kotlin数据类表示 :
import io.vavr.collection.List data class Cup(val level: Int, val capacity: Int) {
override fun toString(): String {
return "Cup($level/$capacity)"
} }
由于倒水问题代表了一组杯子的“状态”,因此可以通过以下方式简单地表示为“类型别名”:
typealias State = List<Cup>
杯子中的水可以执行3种不同类型的移动–将其倒空,填充或从一个杯子倒入另一个杯子,再次以Kotlin数据类表示:
interface Move {
fun change(state: State): State } data class Empty(val glass: Int) : Move {
override fun change(state: State): State {
val cup = state[glass]
return state.update(glass, cup.copy(level = 0 ))
}
override fun toString(): String {
return "Empty($glass)"
} } data class Fill(val glass: Int) : Move {
override fun change(state: State): State {
val cup = state[glass]
return state.update(glass, cup.copy(level = cup.capacity))
}
override fun toString(): String {
return "Fill($glass)"
} } data class Pour(val from: Int, val to: Int) : Move {
override fun change(state: State): State {
val cupFrom = state[from]
val cupTo = state[to]
val amount = min(cupFrom.level, cupTo.capacity - cupTo.level)
return state
.update(from, cupFrom.copy(cupFrom.level - amount))
.update(to, cupTo.copy(level = cupTo.level + amount))
}
override fun toString(): String {
return "Pour($from,$to)"
} }
该实现利用Vavr的List数据结构的“更新”方法来创建一个仅更新相关元素的新列表。
“路径”表示导致当前状态的移动历史:
data class Path(val initialState: pour.State, val endState: State, val history: List<Move>) {
fun extend(move: Move) = Path(initialState, move.change(endState), history.prepend(move))
override fun toString(): String {
return history.reverse().mkString( " " ) + " ---> " + endState
} }
我正在使用列表的“ prepend”方法将元素添加到历史记录的开头。 列表的前面是O(1)操作,而列表的后面是O(n),因此是选择。
给定一个“状态”,以下是更改“状态”的一组可能动作–
1.清空眼镜–
( 0 until count).map { Empty(it) }
2.装满眼镜–
( 0 until count).map { Fill(it) }
3.从一杯倒入另一杯–
( 0 until count).flatMap { from ->
( 0 until initialState.length()).filter { to -> from != to }.map { to ->
Pour(from, to)
} }
现在,所有这些举动都用于从一种状态前进到另一种状态。 考虑说2杯容量为4升和9升的杯子,最初装有0升水,表示为“ List(杯子(0/4),Cup(0/9))”,所有可能的移动都使下一组状态杯子如下:
![倒水问题](https://i-blog.csdnimg.cn/blog_migrate/197a899a21154320b4d2b0d83f32b226.png)
类似地,将这些状态中的每个状态推进到一组新状态将是这样的(以某种简化的形式):
![倒水问题](https://i-blog.csdnimg.cn/blog_migrate/59bb9cbca1e0e7eb648e6faad2f1191f.png)
当每个州基于所有可能的移动前进到下一组状态时,可以看到可能路径的爆炸式增长,这就是Vavr的Stream数据结构提供的惰性出现的地方。流中的值仅根据要求计算。
给定一组路径,使用Stream通过以下方式创建新路径:
fun from(paths: Set<Path>, explored: Set<State>): Stream<Set<Path>> {
if (paths.isEmpty) {
return Stream.empty()
} else {
val more = paths.flatMap { path ->
moves.map { move ->
val next: Path = path.extend(move)
next
}.filter { !explored.contains(it.endState) }
}
return Stream.cons(paths) { from(more, explored.addAll(more.map { it.endState })) }
} }
因此,现在有了从初始状态到新状态的潜在路径流,对“目标”状态的解决方案变为:
val pathSets = from(hashSet(initialPath), hashSet()) fun solution(target: State): Stream<Path> {
return pathSets.flatMap { it }.filter { path -> path.endState == target } }
涵盖了解决方案,使用此代码进行的测试如下所示:有两个容量分别为4升和9升的杯子,最初装有0升水。 最终的目标状态是使第二个杯子装满6升水:
val initialState = list(Cup( 0 , 4 ), Cup( 0 , 9 )) val pouring = Pouring(initialState) pouring.solution(list(Cup( 0 , 4 ), Cup( 6 , 9 )))
.take( 1 ).forEach { path ->
println(path)
}
运行时,这会吐出以下解决方案:
Fill( 1 ) Pour( 1 , 0 ) Empty( 0 ) Pour( 1 , 0 ) Empty( 0 ) Pour( 1 , 0 ) Fill( 1 ) Pour( 1 , 0 ) Empty( 0 ) ---> List(Cup( 0 / 4 ), Cup( 6 / 9 ))
以图形表示的解决方案如下所示:
![倒水问题](https://i-blog.csdnimg.cn/blog_migrate/8ad0ac46c4364eb66f795e597dc657a2.png)
简单地遵循我的github回购中提供的示例的工作版本可能会更容易https://github.com/bijukunjummen/algos/blob/master/src/test/kotlin/pour/Pouring.kt
结论
尽管Kotlin缺乏对本机不可变数据结构的一流支持,但我感到Vavr与Kotlin的结合使该解决方案像Scala一样优雅。
翻译自: https://www.javacodegeeks.com/2019/03/water-pouring-problem-kotlin-vavr.html