LoginSignup
3
1

More than 3 years have passed since last update.

Google OR-Toolsで「もっと金送れ」

Posted at

古典的な覆面算「もっと金送れ(SEND MORE MONEY)」を解きたいです。

 SEND
+MORE
-----
MONEY

これは、同じアルファベットには同じ数字を、異なるアルファベットには異なる数字を当てはめて、この筆算を成立させましょうというパズルです。たとえば「S=1 E=2 N=3 D=4 M=5 O=6 R=7 Y=8」とすると、

 1234
+5672
-----
56328

となって、これは成立しません。お金は送ってもらえませんでした。

解かせ方

さて、Scala から Google OR-Tools を呼びだして、このパズルを解かせてみましょう。まずアルファベットに対応する変数を用意します。

val dict = "SENDMOREMONEY".toCharArray.toSet[Char]
             .map(c => (c, solver.makeIntVar(0, 9, c.toString))).toMap

アルファベットは全て異なる数字であるという制約をかけます。

solver.addConstraint(solver.makeAllDifferent(dict.values.toArray))

そして「SEND + MORE = MONEY」の制約をかけます。

  val send = dict('S') * 1000 + dict('E') * 100 + dict('N') * 10 + dict('D')
  val more = dict('M') * 1000 + dict('O') * 100 + dict('R') * 10 + dict('E')
  val money = dict('M') * 10000 + dict('O') * 1000 + dict('N') * 100 + dict('E') * 10 + dict('Y')
  solver.addConstraint(send + more === money)

さてさて Google OR-Tools が出す答は???

6851 + 0738 = 07589
6853 + 0728 = 07581
5849 + 0638 = 06487
3821 + 0468 = 04289
3829 + 0458 = 04287
2819 + 0368 = 03187
2817 + 0368 = 03185
5732 + 0647 = 06379
5731 + 0647 = 06378
3712 + 0467 = 04179
3719 + 0457 = 04176
7643 + 0826 = 08469
7649 + 0816 = 08465
9567 + 1085 = 10652
8542 + 0915 = 09457
7534 + 0825 = 08359
7531 + 0825 = 08356
7539 + 0815 = 08354
6524 + 0735 = 07259
8432 + 0914 = 09346
7429 + 0814 = 08243
6415 + 0734 = 07149
6419 + 0724 = 07143
8324 + 0913 = 09237
7316 + 0823 = 08139

おやおや、いっぱい答が出ててしまいました。いっぱいお金も送ってもらえますね!

これは一番上の桁には 0 を入れてはならないというルールを忘れていたからでした。気を取りなおして、

solver.addConstraint(dict('S') !== 0)
solver.addConstraint(dict('M') !== 0)

さてさて?

9567 + 1085 = 10652

はい、上手に解けました。

コード一式

Google OR-Tools の環境構築手順は『巨大お絵かきロジックを最適化ソルバーで瞬殺(ネタバレあり)』を参照してください。

src/main/scala/SendMoreMoney.scala
object SendMoreMoney extends App {
  System.loadLibrary("jniortools")

  implicit val solver: Solver = new Solver("")

  val dict = "SENDMOREMONEY".toCharArray.toSet[Char]
               .map(c => (c, solver.makeIntVar(0, 9, c.toString))).toMap

  solver.addConstraint(solver.makeAllDifferent(dict.values.toArray))
  solver.addConstraint(dict('S') !== 0)
  solver.addConstraint(dict('M') !== 0)
  val send = dict('S') * 1000 + dict('E') * 100 + dict('N') * 10 + dict('D')
  val more = dict('M') * 1000 + dict('O') * 100 + dict('R') * 10 + dict('E')
  val money = dict('M') * 10000 + dict('O') * 1000 + dict('N') * 100 + dict('E') * 10 + dict('Y')
  solver.addConstraint(send + more === money)
  solver.newSearch(solver.makePhase(dict.values.toArray,
    Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MAX_VALUE))

  while (solver.nextSolution()) {
    println(
      "SEND".toCharArray.map(dict).map(_.value()).mkString("") + " + " +
        "MORE".toCharArray.map(dict).map(_.value()).mkString("") + " = " +
        "MONEY".toCharArray.map(dict).map(_.value()).mkString(""))
  }

  implicit class IntExprHelper(i: IntExpr)(implicit solver: Solver) {
    def +(j: IntExpr): IntExpr = solver.makeSum(i, j)
    def *(j: Long): IntExpr = solver.makeProd(i, j)
    def ===(j: IntExpr): Constraint = solver.makeEquality(i, j)
    def !==(j: Long): Constraint = solver.makeNonEquality(i, j)
  }
}
build.sbt
name := "send-more-money"
version := "0.1.0"
javacOptions ++= Seq("-source", "11", "-target", "11")
scalaVersion := "2.13.0"

fork := true
javaOptions += "-Djava.library.path=lib"

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1