play scala 2 Water Pouring 问题的scala解法
给它一个初始状态,给它一些规则,推它一把,它就生生不息了。
问题描述
只有一只容量为4的杯子和一只容量为9的杯子,怎么才能取到正好6单位的水?
2. 分析
有三种移动水的方式:1. 往某个杯子装满水 2.倒光一个杯子的水 3. 将一个杯子的水倒入另一个杯子
如果有n个杯子,那么每次移动水都有 n + n + (n-1) * n 种选择。 (如果移动没有造成状态变化也计在内)
这里使用穷竭法解这个问题。我们用一个Stream存储所有可能的选择,然后遍历这些选择,只到遇到第一个解。
所有可能的选择必然是个无限集,Stream是表示无限集最合适的数据结构
3.1 最初的模型
package week7 class Pouring(capacity: Vector[Int]) { // 每个杯子的容量作为初始化参数 // 用 0,1,2...代表不同的杯子 type State = Vector[Int] // 类型别名 val initialStates = capacity map (_ => 0) // 每个杯子的初始状态都是0 trait Move // 定义trait Move,方便以后做模式匹配 case class Empty(glass: Int) extends Move case class Fill(glass: Int) extends Move case class Poure(from: Int, to: Int) extends Move class Path(history: List[Move], endState: State) // 一个Path代表一系列移动和移动的最终状态 }
3.2 move会导致状态变化,下面我们为trait Move添加一个抽象方法 change, 并在各子类中实现它
trait Move { def change(state: State): State } case class Empty(glass: Int) extends Move { def change(state: State) = state updated (glass, 0) } case class Fill(glass: Int) extends Move { def change(state: State) = state updated (glass, capacity(glass)) } case class Poure(from: Int, to: Int) extends Move { def change(state: State) = { val amount = state(from) min (capacity(to) - state(to)) state updated (from, state(from) - amount) updated (to, state(to) + amount) } }
3.3 表示出所有可能的移动
val moves = (for (g <- glasses) yield Empty(g)) ++ (for (g <- glasses) yield Fill(g)) ++ (for (from <- glasses; to <- glasses if from != to) yield Poure(from, to))
测试一下已有的代码
object p { val problem = new Pouring(Vector(0,1,2,3)) //> problem : week7.Pouring = [email protected] problem.moves //> res0: scala.collection.immutable.IndexedSeq[Product with Serializable with we //| ek7.p.problem.Move] = Vector(Empty(0), Empty(1), Empty(2), Empty(3), Fill(0), //| Fill(1), Fill(2), Fill(3), Poure(0,1), Poure(0,2), Poure(0,3), Poure(1,0), P //| oure(1,2), Poure(1,3), Poure(2,0), Poure(2,1), Poure(2,3), Poure(3,0), Poure( //| 3,1), Poure(3,2)) }
3.4 Path需要有哪些方法呢?我们需要看到这个Path所经过的Move,所以要重写toString方法。我们还要能扩展当前的Path,产生新的Path
class Path(history: List[Move], endState: State) { def extend(move: Move) = new Path(move::history, move change endState) override def toString = (history.reverse mkString " ") +" --> " + endState }
3.5 给它一个起点,让它生生不息的方法
val initialPath = new Path(Nil, initialStates) def from(paths: Set[Path]): Stream[Set[Path]] = if (paths.isEmpty) Stream.Empty else { val more = for { path <- paths move <- moves } yield (path extend move) paths #:: from(more) } val pathSets = from(Set(initialPath))
pathSets就表示所有可能的路径,我们来测试一下
package week7 object p { val problem = new Pouring(Vector(4, 9)) //> problem : week7.Pouring = [email protected] val pathSets = problem.pathSets //> pathSets : Stream[Set[week7.p.problem.Path]] = Stream(Set( --> Vector(0, 0) //| ), ?) pathSets.take(3).toList //> res0: List[Set[week7.p.problem.Path]] = List(Set( --> Vector(0, 0)), Set(Emp //| ty(1) --> Vector(0, 0), Poure(0,1) --> Vector(0, 0), Fill(0) --> Vector(4, 0 //| ), Fill(1) --> Vector(0, 9), Poure(1,0) --> Vector(0, 0), Empty(0) --> Vecto //| r(0, 0)), Set(Poure(1,0) Poure(1,0) --> Vector(0, 0), Fill(0) Empty(1) --> V //| ector(4, 0), Poure(1,0) Poure(0,1) --> Vector(0, 0), Fill(0) Poure(0,1) --> //| Vector(0, 4), Poure(0,1) Poure(0,1) --> Vector(0, 0), Empty(0) Fill(1) --> V //| ector(0, 9), Fill(1) Empty(1) --> Vector(0, 0), Fill(1) Fill(0) --> Vector(4 //| , 9), Poure(0,1) Fill(0) --> Vector(4, 0), Poure(1,0) Fill(1) --> Vector(0, //| 9), Empty(0) Empty(0) --> Vector(0, 0), Poure(0,1) Fill(1) --> Vector(0, 9), //| Empty(0) Fill(0) --> Vector(4, 0), Empty(1) Poure(1,0) --> Vector(0, 0), Em //| pty(1) Poure(0,1) --> Vector(0, 0), Empty(1) Empty(0) --> Vector(0, 0), Pour //| e(0,1) Empty(0) --> Vector(0, 0), Empty(0) Empty(1) --> Vector(0, 0), Empty( //| 1) Fill(0) --> Vector(4, //| Output exceeds cutoff limit. }
可以看出,里面包含很多重复的状态。下面我们增加一个限制,如果一个状态之前出现过,就不再添加产生此状态的路径
改进后的代码
val initialPath = new Path(Nil, initialState) def from(paths: Set[Path], explored: Set[State]): Stream[Set[Path]] = if (paths.isEmpty) Stream.Empty else { val more = for { path <- paths next <- moves map path.extend if ! explored.contains(next.endState) } yield next paths #:: from(more, explored ++ (more map (_.endState))) } val pathSets = from(Set(initialPath), Set())
测试
package week7 object p { val problem = new Pouring(Vector(4, 9)) //> problem : week7.Pouring = [email protected] val pathSets = problem.pathSets //> pathSets : Stream[Set[week7.p.problem.Path]] = Stream(Set(Vector(0, 0)), ?) //| pathSets.take(3).toList //> res0: List[Set[week7.p.problem.Path]] = List(Set(Vector(0, 0)), Set(Fill(1) //| --> Vector(0, 9), Poure(0,1) --> Vector(0, 0), Empty(1) --> Vector(0, 0), Em //| pty(0) --> Vector(0, 0), Fill(0) --> Vector(4, 0), Poure(1,0) --> Vector(0, //| 0)), Set(Fill(1) Fill(0) --> Vector(4, 9), Fill(1) Poure(1,0) --> Vector(4, //| 5), Fill(0) Fill(1) --> Vector(4, 9), Fill(0) Poure(0,1) --> Vector(0, 4))) //| }
可以看到,已经少了很多重复的状态,但是每个Set中还有重复的。这是因为我们用传入from方法的explored作为历史记录,然后“成批地”更新历史记录,而不是遇到一个新的状态就更新。那样做会造成大量的内存copy。
3.6 最后一步,求解问题。求解方法的返回值依然是个Stream,用户可以根据需要取1个或多个解。这里也算体现出了Scala的伸缩性
def solve(target: Int): Stream[Path] = for { pathSet <- pathSets path <- pathSet if path.endState contains target } yield path
测试
package week7 object p { val problem = new Pouring(Vector(4, 9)) //> problem : week7.Pouring = [email protected] problem.solve(6) //> res0: Stream[week7.p.problem.Path] = Stream(Fill(1) Poure(1,0) Empty(0) Poure //| (1,0) Empty(0) Poure(1,0) Fill(1) Poure(1,0) --> Vector(4, 6), ?) }
我们已经求得一个解,来验证一下
(0, 9) --> (4, 5) --> (0, 5) --> (4, 1) --> (0, 1) --> (1, 0) --> (1, 9) --> (4, 6)
3.7 完整代码
package week7 class Pouring(capacity: Vector[Int]) { // 每个杯子的容量作为初始化参数 // 用 0,1,2...代表不同的杯子 val glasses = 0 until capacity.length type State = Vector[Int] // 类型别名 val initialState = capacity map (_ => 0) // 每个杯子的初始状态都是0 trait Move { def change(state: State): State } case class Empty(glass: Int) extends Move { def change(state: State) = state updated (glass, 0) } case class Fill(glass: Int) extends Move { def change(state: State) = state updated (glass, capacity(glass)) } case class Poure(from: Int, to: Int) extends Move { def change(state: State) = { val amount = state(from) min (capacity(to) - state(to)) state updated (from, state(from) - amount) updated (to, state(to) + amount) } } val moves = (for (g <- glasses) yield Empty(g)) ++ (for (g <- glasses) yield Fill(g)) ++ (for (from <- glasses; to <- glasses if from != to) yield Poure(from, to)) class Path(history: List[Move], val endState: State) { def extend(move: Move) = new Path(move :: history, move change endState) override def toString = if (history.isEmpty) endState.toString else history.reverse.mkString(" ") + " --> " + endState } val initialPath = new Path(Nil, initialState) def from(paths: Set[Path], explored: Set[State]): Stream[Set[Path]] = if (paths.isEmpty) Stream.Empty else { val more = for { path <- paths next <- moves map path.extend if !explored.contains(next.endState) } yield next paths #:: from(more, explored ++ (more map (_.endState))) } val pathSets = from(Set(initialPath), Set(initialState)) def solve(target: Int): Stream[Path] = for { pathSet <- pathSets path <- pathSet if path.endState contains target } yield { path } }
转载于:https://blog.51cto.com/dingbo/1592771