给它一个初始状态,给它一些规则,推它一把,它就生生不息了。
问题描述
只有一只容量为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