diff --git a/nmcb/src/main/scala/examples/FriedmanNumber.scala b/nmcb/src/main/scala/examples/FriedmanNumber.scala new file mode 100644 index 00000000..a76a1c36 --- /dev/null +++ b/nmcb/src/main/scala/examples/FriedmanNumber.scala @@ -0,0 +1,136 @@ +package examples + +import nmcb.predef.* + +import java.lang.System.* +import scala.Option.when +import scala.collection.* +import scala.util.* + + +object FriedmanNumber: + + trait Operation derives CanEqual: + def run(l: Int, r: Int): Option[Int] + + case object Addition extends Operation: + def run(l: Int, r: Int): Option[Int] = + Some(l + r) + + case object Subtraction extends Operation: + def run(l: Int, r: Int): Option[Int] = + Some(l - r) + + case object Multiplication extends Operation: + def run(l: Int, r: Int): Option[Int] = + Some(l * r) + + case object Division extends Operation: + def run(l: Int, r: Int): Option[Int] = + when(r != 0 && l % r == 0)(l / r) + + case object Exponentiation extends Operation: + def run(l: Int, r: Int): Option[Int] = + Some(math.pow(l, r).toInt) + + case object Concatenation extends Operation: + def run(l: Int, r: Int): Option[Int] = + sys.error(s"unimplemented, special handling in computation") + + object Operation: + val all: Vector[Operation] = + Vector( + Addition, + Subtraction, + Multiplication, + Division, + Exponentiation, + Concatenation + ) + + + sealed trait ComputationOrder derives CanEqual: + + def isConcat: Boolean = + this match + case BinaryOperation(l, r, Some(Concatenation)) => l.isConcat && r.isConcat + case Value(v) => true + case _ => false + + def compute: Option[Int] = + this.runtimeChecked match + case Value(Some(v)) => Some(v) + case BinaryOperation(l, r, Some(Concatenation)) => when(l.isConcat && r.isConcat)(s"${l.compute.get}${r.compute.get}".toInt) + case BinaryOperation(l, r, Some(o)) => l.compute.flatMap(lr => r.compute.flatMap(rr => o.run(lr, rr))) + + def bind(operations: Vector[Operation], arguments: Vector[Int]): ComputationOrder = + val (result, _, _) = bindRecursively(operations, arguments) + result + + private def bindRecursively(operations: Vector[Operation], arguments: Vector[Int]): (ComputationOrder, Vector[Operation], Vector[Int]) = + this match + case Value(v) => + (Value(Some(arguments.head)), operations, arguments.tail) + case BinaryOperation(l, r, v) => + val (fl, ol, al) = l.bindRecursively(operations.tail, arguments) + val (fr, or, ar) = r.bindRecursively(ol, al) + (BinaryOperation(fl, fr, Some(operations.head)), or, ar) + + case class Value(v: Option[Int] = None) extends ComputationOrder + case class BinaryOperation(l: ComputationOrder, r: ComputationOrder, o: Option[Operation] = None) extends ComputationOrder + + + private val possibleOperationsMemo = Memo.empty[Int, Vector[Vector[Operation]]] + def possibleOperations(size: Int): Vector[Vector[Operation]] = + possibleOperationsMemo.memoize(size): + Vector + .fill(size)(Operation.all) + .flatten + .combinations(size).flatMap(_.permutations) + .filter(_.count(_ == Concatenation) != size) + .distinct + .toVector + + private val possibleDigitSequencesMemo = Memo.empty[Vector[Int], Vector[Vector[Int]]] + def possibleDigitSequences(digits: Vector[Int]): Vector[Vector[Int]] = + possibleDigitSequencesMemo.memoize(digits): + digits + .combinations(digits.length) + .flatMap(_.permutations) + .distinct + .toVector + + private val possibleComputationOrdersMemo = Memo.empty[Int, Vector[ComputationOrder]] + def possibleComputationOrders(size: Int): Vector[ComputationOrder] = + possibleComputationOrdersMemo.memoize(size): + size match + case 0 => Vector.empty + case 1 => Vector(Value()) + case n => + (0 until size).toVector.flatMap: leftSize => + for + t1 <- possibleComputationOrders(leftSize) + t2 <- possibleComputationOrders(size - leftSize - 1) + yield + BinaryOperation(t1, t2) + + + def isFriedmanNumber(number: Int): Boolean = + val digits: Vector[Int] = number.toString.toVector.map(_.asDigit) + possibleOperations(digits.length - 1).exists: operations => + possibleDigitSequences(digits).exists: sequence => + possibleComputationOrders(operations.size + sequence.size).exists: order => + order.bind(operations, sequence).compute.contains(number) + + def sieve(max: Int): Seq[Int] = + (0 to max).filter(isFriedmanNumber) + + + def main(args: Array[String]): Unit = + val max: Int = Try(args(0).toInt).getOrElse(10000) + val start: Long = currentTimeMillis + val result: Seq[Int] = sieve(max) + + println(s"friedman numbers up to $max:") + println(result.mkString(", ")) + println(s"took ${currentTimeMillis - start} ms") \ No newline at end of file diff --git a/nmcb/src/test/scala/examples/TestFriedmanNumber.scala b/nmcb/src/test/scala/examples/TestFriedmanNumber.scala new file mode 100644 index 00000000..63df8210 --- /dev/null +++ b/nmcb/src/test/scala/examples/TestFriedmanNumber.scala @@ -0,0 +1,14 @@ +package examples + +import org.scalatest.funsuite.AnyFunSuite + +class TestFriedmanNumber extends AnyFunSuite: + + /** [OEIS-A036057](https://oeis.org/A036057) */ + test("FriedmanNumber") { + assertResult( + Vector( + 25, 121, 125, 126, 127, 128, 153, 216, 289, 343, 347, 625, 688, 736, 1022, 1024, 1206, 1255, 1260, 1285, 1296, 1395, 1435, 1503, 1530, 1792, 1827, 2048, 2187, 2349, 2500, 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2592, 2737, 2916, 3125, 3159, 3281, 3375, 3378, 3685, 3784, 3864, 3972, 4088, 4096, 4106, 4167, 4536, 4624, 4628, 5120, 5776, 5832, 6144, 6145, 6455, 6880, 7928, 8092, 8192, 9025, 9216, 9261 + ) + )(FriedmanNumber.sieve(10000)) + } diff --git a/nmcb/src/test/scala/examples/TestVampireNumber.scala b/nmcb/src/test/scala/examples/TestVampireNumber.scala index b199c753..f9cd9fd0 100644 --- a/nmcb/src/test/scala/examples/TestVampireNumber.scala +++ b/nmcb/src/test/scala/examples/TestVampireNumber.scala @@ -6,7 +6,6 @@ class TestVampireNumber extends AnyFunSuite: /** [OEIS-A014575](https://oeis.org/A014575) */ test("VampireNumber") { - println(VampireNumber.sieve(135837)) assertResult( Vector( 1260, 1395, 1435, 1530, 1827, 2187, 6880, 102510, 104260, 105210, 105264, 105750, 108135,