--- /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))
+