Статьи

Решение проблемы «ведра с водой» с помощью Scala

Недавно я натолкнулся на загадку, которая в этой книге называлась проблемой «ведра с водой», и это меня совершенно озадачило.

У вас есть 12-галлонное ведро, 8-галлонное ведро и 5-галлонное ведро. 12-литровое ведро заполнено водой, а два других пусты. Не используя дополнительную воду, как вы можете разделить двенадцать галлонов воды поровну, чтобы в двух из трех ведер было ровно 6 галлонов воды?

Я и мой племянник потратили немало времени, пытаясь ее решить, и в конечном итоге сдались.

Тогда я вспомнил, что видел программное решение для аналогичной головоломки, разрабатываемой в курсе «Принципы функционального программирования в Scala» Мартина Одерского.

Это суть решения, полностью скопированного с курса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package bucket
 
case class Pouring(capacity: Vector[Int], initialState: Vector[Int]){
  type State = Vector[Int]
           
  trait Move {
    def change(state: State): State
  }
   
  case class Pour(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)
    }
  }
   
  class Path(history: List[Move], val endState: State) {
    def extend(move: Move) = new Path(move :: history, move change endState)
    override def toString = (history.reverse mkString " ") + "-->" + endState
  }
   
  val glasses = 0 until capacity.length
   
  val moves = for {
    from <- glasses
    to <- glasses
    if (from != to)
  } yield Pour(from, to)
   
  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 solution(target: State): Stream[Path] = {
    for {
      pathSet <- pathSets
      path <- pathSet
      if (path.endState == target)
    } yield path
  }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
package bucket
 
import org.scalatest.FunSuite
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
 
@RunWith(classOf[JUnitRunner])
class PouringTest extends FunSuite {
  test("Solution to the 12-gallon, 8-gallon, 5-gallon problem") {
    val p = Pouring(Vector(12, 8, 5), Vector(12, 0, 0))
    println(p.solution(Vector(6, 6, 0)))
  }
}

и запуск этой программы выплевывает следующее 7-шаговое решение! (индекс 0 — ведро на 12 галлонов, 1 — ведро на 8 галлонов и 2 — ведро на 5 галлонов)

1
2
3
4
5
6
7
Pour(0,1)
Pour(1,2)
Pour(2,0)
Pour(1,2)
Pour(0,1)
Pour(1,2)
Pour(2,0)

Если вы заинтересованы в том, чтобы узнать больше о коде этого решения, лучше всего следовать 7-й неделе курса Coursera, который я связал выше, Мартин Одерски проделывает фантастическую работу, казалось бы, находя решение на лету! ,