バケツ問題

某プログラム勉強会で、お題が出たので解いてみる。


問題
水がいっぱい入ったバケツ一個と、空のバケツがいくつかあり、それぞれのバケツの容量は正確にわかっている。この状態から指定された容量を量りとる、最
小手数の手順を求めよ。
ただし
・バケツからバケツに水を移す操作を1手と数える。
・バケツからバケツに水を移す際は、注がれるバケツがいっぱいになるか、注ぐバケツが空になるまで行う。
・従って、水は一滴も漏れない。
例えば。バケツが4つ。10リットル,8リットル,3リットル。10リットルのバケツが満タンで、あとは空っぽ。ここから4リットル量りとりたい。
最小手数の手順は
 10リットルのバケツから、3リットルのバケツに注ぐ
 3リットルのバケツから、8リットルのバケツに注ぐ
 10リットルのバケツから、3リットルのバケツに注ぐ
の3手(要するに、10リットルから3リットルを2回減じている)となる。
表示を短くするために、バケツに順に 0, 1, 2 と番号を振ると、
0→2, 2→1, 0→2
と、表示することができる。

最初の状態から、他のバケツに注いだ状態、さらに他のバケツに注いだ状態と遷移させて状態を評価、解が出たら停止すればよい。
ただし、状態A→状態B→状態Aとなると永久に終了しないので、過去に評価した状態は記憶しておき同じ状態を繰り返さないようにする。
あと、状態A→状態B→状態Cと縦型探索をすると、状態A→状態Cという最適解を見逃してしまうので横型探索をする。

def solve(target, buckets)
  done = {}                                                      # 評価済みの状態保持用
  cur_fill = [["", [buckets.first] + [0] * (buckets.size - 1)]]  # 現在の状態
  while (!cur_fill.empty?)
    next_fill = []                                               # 生成された次の状態保存用
    cur_fill.each do |record, fill|
      buckets.size.times do |i|
        next if fill[i] == 0
        buckets.size.times do |j|
          next if i == j
          next if fill[j] == buckets[j]
          mv_vol = [fill[i], buckets[j] - fill[j]].min
          new_fill = fill.clone
          new_fill[i] -= mv_vol
          new_fill[j] += mv_vol
          next if done[new_fill]
          done[new_fill] = true
          new_record = record + " #{i}#{j}"
          if new_fill[i] == target or new_fill[j] == target
            puts "#{new_record.chars.count('')} 手 :#{new_record}"
            return
          end
          next_fill << [new_record, new_fill]
        end
      end
    end
    cur_fill = next_fill
  end
  puts "***解なし***"
end

val, *elem = ARGV.map(&:to_i)
solve(val, elem)

同様のアルゴリズムscala実装。
なぜかruby版より、かなり遅い。オブジェクトの生成回数が多いのがネックなのか。

object Bucket {
  import scala.collection.mutable.HashMap
  import scala.collection.mutable.Stack
  def main(args: Array[String]) {
    args.map(_.toInt).toList match {
      case x::xs => println(solve(x, xs))
      case _ => println("argument error")
    }
  }

  case class Bucket(id:Int, size:Int, fill:Int)
  case class State(parent:State, buckets:List[Bucket]) {
    def key:List[Int] = buckets.sort((a,b) => a.id < b.id).map(_.fill)
    def record:String = {
      if (parent == null) return ""
      var dst, src = 0
      var mv = parent.key.zip(key).zip(List.range(0,buckets.size))
      for (((a, b),i) <- mv if a != b) yield {
        if (a > b) src = i else dst = i
      }
      parent.record + " " + src.toString + "->" +dst.toString
    }
  }

  def solve(ans:Int, buckets:List[Int]):String = {
    val done = new HashMap[List[Int], Boolean]
    var curStates = new Stack[State]
    var rootState = Array.fromFunction(i => new Bucket(i, buckets(i), if (i==0) buckets(0) else 0))(buckets.size).toList
    curStates.push(new State(null,rootState))
    while (!curStates.isEmpty) {
      var nextStates = new Stack[State]
      for (cs <- curStates) yield {
        for (src <- cs.buckets if src.fill > 0; dst <- cs.buckets if dst.size > dst.fill && src != dst) yield {
          var mvVol = Math.min(src.fill, dst.size - dst.fill)
          var newSrc = new Bucket(src.id, src.size, src.fill - mvVol)
          var newDst = new Bucket(dst.id, dst.size, dst.fill + mvVol)
          var newState = new State(cs, newSrc::newDst::cs.buckets.filter((bc) => bc != src && bc != dst))
          var key = newState.key
          if (!done.contains(key)) {
            done.put(key, true)
            if (newSrc.fill == ans || newDst.fill == ans) return newState.record
            nextStates.push(newState)
          }
        }
      }
      curStates = nextStates
    }
    "no answer"
  }
}