atted
authorChristian Urban <christian.urban@kcl.ac.uk>
Thu, 04 Dec 2025 15:04:05 +0000
changeset 505 c06b45a52d50
parent 504 7653dd662db3
child 506 28f78d9081d7
atted
progs/countdown.scala
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/progs/countdown.scala	Thu Dec 04 15:04:05 2025 +0000
@@ -0,0 +1,337 @@
+// Countdown Game using the Powerset Function
+//============================================
+
+
+def powerset(xs: Set[Int]) : Set[Set[Int]] = {
+  if (xs == Set()) Set(Set())
+  else {
+    val ps = powerset(xs.tail)  
+    ps ++ ps.map(_ + xs.head)
+  }
+}  
+
+powerset(Set(1,2,3)).mkString("\n")
+
+// proper subsets
+def psubsets(xs: Set[Int]) = 
+  powerset(xs) -- Set(Set(), xs) 
+
+psubsets(Set(1,2,3)).mkString("\n")
+
+// pairs of subsets and their "complement"
+def splits(xs: Set[Int]) : Set[(Set[Int], Set[Int])] =
+  psubsets(xs).map(s => (s, xs -- s))
+
+splits(Set(1,2,3,4)).mkString("\n")
+
+
+// ususal trees representing infix notation for expressions
+enum Tree {
+  case Num(i: Int)
+  case Add(l: Tree, r: Tree)
+  case Mul(l: Tree, r: Tree)
+  case Sub(l: Tree, r: Tree)
+  case Div(l: Tree, r: Tree)
+}
+import Tree._
+
+//pretty printing for trees
+def pp(tr: Tree) : String = tr match {
+  case Num(n) => s"$n"
+  case Add(l, r) => s"(${pp(l)} + ${pp(r)})"
+  case Mul(l, r) => s"(${pp(l)} * ${pp(r)})"
+  case Sub(l, r) => s"(${pp(l)} - ${pp(r)})"
+  case Div(l, r) => s"(${pp(l)} / ${pp(r)})"
+}
+
+// evaluating a tree - might fail when dividing by 0 
+// (the for-notation makes it easy to deal with Options)
+def eval(tr: Tree) : Option[Int] = tr match {
+  case Num(n) => Some(n)
+  case Add(l, r) => 
+    for (ln <- eval(l); rn <- eval(r)) yield ln + rn
+  case Mul(l, r) => 
+    for (ln <- eval(l); rn <- eval(r)) yield ln * rn 
+  case Sub(l, r) => 
+    for (ln <- eval(l); rn <- eval(r); if rn <= ln) yield ln - rn
+  case Div(l, r) => 
+    for (ln <- eval(l); rn <- eval(r); if rn != 0) 
+      yield ln / rn 
+}
+
+
+// simple-minded generation of nearly all possible expressions
+// (the nums argument determines the set of numbers over which
+//  the expressions are generated)
+def gen1(nums: Set[Int]) : Set[Tree] = nums.size match {
+  case 0 => Set()
+  case 1 => Set(Num(nums.head))
+  case 2 => {
+    val ln = Num(nums.head)
+    val rn = Num(nums.tail.head)
+    Set(Add(ln, rn), Mul(ln, rn),
+        Sub(ln, rn), Sub(rn, ln),
+        Div(ln, rn), Div(rn, ln))
+  }
+  case n => {
+    val res = 
+      for ((ls, rs) <- splits(nums);
+            lt <- gen1(ls);
+            rt <- gen1(rs)) yield {
+            Set(Add(lt, rt), Mul(lt, rt),
+                Sub(lt, rt), Sub(rt, lt),
+                Div(lt, rt), Div(rt, lt))
+           }
+    res.flatten
+  }
+}
+
+
+// some testcases
+gen1(Set(1))
+gen1(Set(1, 2)).mkString("\n")
+gen1(Set(1, 2, 3)).map(pp).mkString("\n")
+gen1(Set(1, 2, 3)).map(tr => s"${pp(tr)} = ${eval(tr)}").mkString("\n")
+
+gen1(Set(1, 2, 3)).size             // => 168
+gen1(Set(1, 2, 3, 4, 5, 6)).size    // => 26951040
+
+/*
+    It is clear that gen1 generates too many expressions
+    to be fast overall.
+
+    An easy fix is to not generate improper Subs and 
+    Divs when they are at the leaves.
+*/
+
+
+def gen2(nums: Set[Int]) : Set[Tree] =  nums.size match {
+  case 0 => Set()
+  case 1 => Set(Num(nums.head))
+  case 2 => {
+    val l = nums.head
+    val r = nums.tail.head
+    Set(Add(Num(l), Num(r)), 
+        Mul(Num(l), Num(r)))
+        ++ Option.when(l <= r)(Sub(Num(r), Num(l)))
+        ++ Option.when(l > r)(Sub(Num(l), Num(r)))
+        ++ Option.when(r > 0 && l % r == 0)(Div(Num(l), Num(r)))
+        ++ Option.when(l > 0 && r % l == 0)(Div(Num(r), Num(l)))
+  }
+  case xs => {
+    val res = 
+      for ((lspls, rspls) <- splits(nums);
+           lt <- gen2(lspls); 
+           rt <- gen2(rspls)) yield {
+        Set(Add(lt, rt), Sub(lt, rt),
+            Mul(lt, rt), Div(lt, rt))
+    } 
+    res.flatten
+  }
+}
+
+gen2(Set(1, 2, 3)).size             // => 68
+gen2(Set(1, 2, 3, 4, 5, 6)).size    // => 6251936
+
+// OK, the numbers decreased in gen2 as it does 
+// not generate leaves like (2 - 3) and (4 / 3).
+// It might still generate such expressions "higher
+// up" though, for example (1 + 2) - (4 + 3)
+
+println(gen2(Set(1,2,3,4)).map(pp).mkString("\n"))
+
+// before taking any time measure, it might be good
+// to check that no "essential" expression has been
+// lost by the optimisation in gen2...some eyeballing
+
+gen1(Set(1,2,3,4)).map(eval) == gen2(Set(1,2,3,4)).map(eval)
+gen1(Set(1,2,3,4,5,6)).map(eval) == gen2(Set(1,2,3,4,5,6)).map(eval)
+
+
+// lets start some timing
+
+def time_needed[T](n: Int, code: => T) = {
+  val start = System.nanoTime()
+  for (i <- (0 to n)) code
+  val end = System.nanoTime()
+  (end - start) / 1.0e9
+}
+
+// check to reach a target
+def check(xs: Set[Int], target: Int) =
+  gen2(xs).find(eval(_) == Some(target))
+
+// example 1
+check(Set(50, 5, 4, 9, 10, 8), 560).foreach { sol =>
+  println(s"${pp(sol)} => ${eval(sol)}")
+}
+// => ((50 + ((4 / (10 - 9)) * 5)) * 8) => Some(560)
+
+time_needed(1, check(Set(50, 5, 4, 9, 10, 8), 560))
+// => ~14 secs
+
+
+// example 2
+check(Set(25, 5, 2, 10, 7, 1), 986).foreach { sol =>
+  println(s"${pp(sol)} => ${eval(sol)}")
+}
+
+time_needed(1, check(Set(25, 5, 2, 10, 7, 1), 986))
+// => ~15 secs
+
+// example 3 (unsolvable)
+check(Set(25, 5, 2, 10, 7, 1), -1)
+time_needed(1, check(Set(25, 5, 2, 10, 7, 1), -1))
+// => ~22 secs
+
+// example 4
+check(Set(100, 25, 75, 50, 7, 10), 360).foreach { sol =>
+  println(s"${pp(sol)} => ${eval(sol)}")
+}
+time_needed(1, check(Set(100, 25, 75, 50, 7, 10), 360))
+// => ~14 secs
+
+/*
+  Twenty-two seconds in the worst case...this does not yet 
+  look competitive enough.
+
+  Lets generate the expression together with the result 
+  and restrict the number of expressions in this way.
+*/
+
+
+def gen3(nums: Set[Int]) : Set[(Tree, Int)] =  nums.size match {
+  case 0 => Set()
+  case 1 => Set((Num(nums.head), nums.head))
+  case xs => {
+    val res =
+      for ((lspls, rspls) <- splits(nums);
+           (lt, ln) <- gen3(lspls); 
+           (rt, rn) <- gen3(rspls)) yield {
+        Set((Add(lt, rt), ln + rn),
+            (Mul(lt, rt), ln * rn))
+        ++ Option.when(ln <= rn)((Sub(rt, lt), rn - ln)) 
+        ++ Option.when(ln > rn)((Sub(lt, rt), ln - rn))
+        ++ Option.when(rn > 0 && ln % rn == 0)((Div(lt, rt), ln / rn))
+        ++ Option.when(ln > 0 && rn % ln == 0)((Div(rt, lt), rn / ln))
+    } 
+    res.flatten
+  }
+}
+
+// eyeballing that we did not lose any solutions...
+gen2(Set(1,2,3,4)).map(eval).flatten == gen3(Set(1,2,3,4)).map(_._2)
+gen2(Set(1,2,3,4,5,6)).map(eval).flatten == gen3(Set(1,2,3,4,5,6)).map(_._2)
+
+// the number of generated expressions
+gen3(Set(1, 2, 3)).size             // => 104
+gen3(Set(1, 2, 3))
+gen3(Set(1, 2, 3, 4, 5, 6)).size    // => 5092300
+
+// ...while this version does not "optimise" the leaves
+// as in gen2, the number of generated expressions grows
+// slower
+
+def check2(xs: Set[Int], target: Int) =
+  gen3(xs).find(_._2 == target)
+
+
+// example 1
+time_needed(1, check2(Set(50, 5, 4, 9, 10, 8), 560))
+// => ~14 secs
+
+// example 2
+time_needed(1, check2(Set(25, 5, 2, 10, 7, 1), 986))
+// => ~18 secs
+
+// example 3 (unsolvable)
+time_needed(1, check2(Set(25, 5, 2, 10, 7, 1), -1))
+// => ~19 secs
+
+// example 4
+time_needed(1, check2(Set(100, 25, 75, 50, 7, 10), 360))
+// => ~16 secs
+
+// ...we are getting better, but not yet competetive enough.
+// 
+// The problem is that splits generates for sets say {1,2,3,4}
+// the splits ({1,2}, {3,4}) and ({3,4}, {1,2}). This means that
+// we consider terms (1 + 2) * (3 + 4) and (3 + 4) * (1 + 2) as
+// separate candidates. We can avoid this duplication by returning
+// sets of sets of numbers, like 
+
+// pairs of subsets and their "complement"
+def splits2(xs: Set[Int]) : Set[Set[Set[Int]]] =
+  psubsets(xs).map(s => Set(s, xs -- s))
+
+splits(Set(1,2,3,4)).mkString("\n")
+splits2(Set(1,2,3,4)).mkString("\n")
+
+def gen4(nums: Set[Int]) : Set[(Tree, Int)] =  nums.size match {
+  case 0 => Set()
+  case 1 => Set((Num(nums.head), nums.head))
+  case xs => {
+    val res =
+      for (spls <- splits2(nums);
+           (lt, ln) <- gen4(spls.head); 
+           (rt, rn) <- gen4(spls.tail.head)) yield {
+        Set((Add(lt, rt), ln + rn),
+            (Mul(lt, rt), ln * rn))
+        ++ Option.when(ln <= rn)((Sub(rt, lt), rn - ln)) 
+        ++ Option.when(ln > rn)((Sub(lt, rt), ln - rn)) 
+        ++ Option.when(rn > 0 && ln % rn == 0)((Div(lt, rt), ln / rn))
+        ++ Option.when(ln > 0 && rn % ln == 0)((Div(rt, lt), rn / ln)) 
+    } 
+    res.flatten
+  }
+}
+
+// eyeballing that we did not lose any solutions...
+gen4(Set(1,2,3,4)).map(_._2) == gen3(Set(1,2,3,4)).map(_._2)
+gen4(Set(1,2,3,4,5,6)).map(_._2) == gen3(Set(1,2,3,4,5,6)).map(_._2)
+
+
+gen4(Set(1, 2, 3)).size             // => 43
+gen4(Set(1, 2, 3, 4))
+gen4(Set(1, 2, 3, 4, 5, 6)).size    // => 550218
+
+def check3(xs: Set[Int], target: Int) =
+  gen4(xs).find(_._2 == target)
+
+// example 1
+check3(Set(50, 5, 4, 9, 10, 8), 560).foreach { sol =>
+  println(s"${pp(sol._1)} => ${eval(sol._1)}")
+}
+// (((10 - 5) * (9 * 8)) + (4 * 50)) => Some(560)
+
+time_needed(1, check3(Set(50, 5, 4, 9, 10, 8), 560))
+// => ~1 sec
+
+
+// example 2
+check3(Set(25, 5, 2, 10, 7, 1), 986).foreach { sol =>
+  println(s"${pp(sol._1)} => ${eval(sol._1)}")
+}
+
+time_needed(1, check3(Set(25, 5, 2, 10, 7, 1), 986))
+// => ~1 sec
+
+// example 3 (unsolvable)
+check3(Set(25, 5, 2, 10, 7, 1), -1)
+time_needed(1, check3(Set(25, 5, 2, 10, 7, 1), -1))
+// => ~2 secs
+
+// example 4
+check3(Set(100, 25, 75, 50, 7, 10), 360).foreach { sol =>
+  println(s"${pp(sol._1)} => ${eval(sol._1)}")
+}
+time_needed(1, check3(Set(100, 25, 75, 50, 7, 10), 360))
+// => ~1 secs
+
+
+
+time_needed(1, check3(Set(50, 5, 4, 9, 10, 8), 560))
+time_needed(1, check3(Set(25, 5, 2, 10, 7, 1), 986))
+time_needed(1, check3(Set(25, 5, 2, 10, 7, 1), -1))
+time_needed(1, check3(Set(100, 25, 75, 50, 7, 10), 360))
+