给它一个初始状态,给它一些规则,推它一把,它就生生不息了。

  1. 问题描述

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
    }
}