Tiempo Scala 9146 (min 7, max 15, avg 9.15): 2000 segundos
Al igual que muchas entradas, empiezo obteniendo la longitud total, luego buscando los espacios, obteniendo un poco más de información, reduciendo a los candidatos restantes y luego adivinando frases.
Inspirado por el cómic original de xkcd, traté de aplicar mi comprensión rudimentaria de la teoría de la información. Hay un billón de frases posibles o poco menos de 40 bits de entropía. Establecí un objetivo de menos de 10 conjeturas por frase de prueba, lo que significa que debemos aprender en promedio casi 5 bits por consulta (ya que el último es inútil). Con cada suposición, obtenemos dos números y, en términos generales, cuanto mayor sea el rango potencial de esos números, más esperamos aprender.
Para simplificar la lógica, utilizo cada consulta como efectivamente dos preguntas separadas, por lo que cada cadena de adivinanzas tiene dos partes, un lado izquierdo interesado en la cantidad de posiciones correctas (clavijas negras en la mente maestra) y un lado derecho interesado en la cantidad de caracteres correctos ( clavijas totales). Aquí hay un juego típico:
Phrase: chasteness legume such
1: p0 ( 1/21) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -aaaaaaaaaaaabbbbbbbbbcccccccccdddddddddeeeeeeeeeeeeeeefffffffffgggggggggggghhhhhhhhhiiiiiiiiiiiiiiiiiijjjjjjkkkkkkkkkllllllllllllmmmmmmmmmnnnnnnnnnnnnoooooooooooopppppppppqqqrrrrrrrrrrrrssssssssssssssstttttttttuuuuuuuuuuuuvvvvvvwwwwwwxxxyyyyyyyyyzzzzzz
2: p1 ( 0/ 8) - - - --- - ---aaaaaaaaaaaadddddddddeeeeeeeeeeeeeeefffffffffjjjjjjkkkkkkkkkllllllllllllooooooooooooqqqwwwwwwxxxyyyyyyyyyzzzzzz
3: p1 ( 0/11) ----- ------ ---------bbbbbbbbbdddddddddeeeeeeeeeeeeeeefffffffffgggggggggggghhhhhhhhhiiiiiiiiiiiiiiiiiikkkkkkkkkllllllllllllppppppppptttttttttvvvvvv
4: p1 ( 2/14) ---------- ------ ----ccccccccceeeeeeeeeeeeeeehhhhhhhhhkkkkkkkkkllllllllllllmmmmmmmmmooooooooooooqqqrrrrrrrrrrrrsssssssssssssssvvvvvvwwwwwwzzzzzz
5: p3 ( 3/ 3) iaaiiaaiai iaaiia iaaiaaaaaaaaaaaabbbbbbbbbdddddddddiiiiiiiiiiiiiiiiiikkkkkkkkkllllllllllllqqquuuuuuuuuuuuvvvvvvyyyyyyyyy
6: p3 ( 3/11) aaaasassaa aaaasa aaaaaaaaaaaaaaaabbbbbbbbbcccccccccdddddddddfffffffffhhhhhhhhhppppppppprrrrrrrrrrrrssssssssssssssstttttttttuuuuuuuuuuuuwwwwwwxxxyyyyyyyyy
7: p4 ( 4/10) accretions shrive pews
8: p4 ( 4/ 6) barometric terror heir
9: p4 SUCCESS chasteness legume such
Adivinando espacios
Cada conjetura de espacio puede devolver como máximo 2 clavijas negras; Traté de construir conjeturas para devolver 0,1 y 2 clavijas con probabilidades 1 / 4,1 / 2 y 1/4 respectivamente. Creo que esto es lo mejor que puede hacer para obtener 1,5 bits de información. Me decidí por una cadena alterna para la primera aproximación seguida de las generadas aleatoriamente, aunque resulta que generalmente vale la pena comenzar a adivinar en el segundo o tercer intento, ya que conocemos las frecuencias de longitud de palabra.
El juego de caracteres de aprendizaje cuenta
Para las conjeturas del lado derecho, elijo conjuntos de caracteres aleatorios (siempre 2 de e / i / a / s) para que el número esperado devuelto sea la mitad de la longitud de la frase. Una variación más alta significa más información y, desde la página de Wikipedia en la distribución binomial , estoy estimando aproximadamente 3.5 bits por consulta (al menos durante los primeros antes de que la información se vuelva redundante). Una vez que se conoce el espacio, utilizo cadenas aleatorias de las letras más comunes en el lado izquierdo, elegidas para no entrar en conflicto con el lado derecho.
Unir a los candidatos restantes
Este juego es una compensación de eficiencia de consulta / velocidad de cómputo y la enumeración de los candidatos restantes puede llevar mucho tiempo sin información estructurada como caracteres específicos. Optimicé esta parte al recopilar principalmente información que no varía con el orden de las palabras, lo que me permite calcular previamente los recuentos de juego de caracteres para cada palabra individual y compararlos con los recuentos aprendidos de las consultas. Empaquete estos conteos en un entero largo, usando el comparador de igualdad de máquina y el sumador para probar todos mis recuentos de caracteres en paralelo. Esta fue una gran victoria. Puedo empacar hasta 9 recuentos en Long, pero descubrí que recopilar información adicional no valía la pena y me decidí por 6 a 7.
Una vez que se conocen los candidatos restantes, si el conjunto es razonablemente pequeño, elijo el que tenga el registro más bajo esperado de los candidatos restantes. Si el conjunto es lo suficientemente grande como para que esto lleve mucho tiempo, elijo de un pequeño conjunto de muestras.
Gracias a todos. Este fue un juego divertido y me atrajo a inscribirme en el sitio.
Actualización: código limpio para simplificar y facilitar la lectura, con pequeños ajustes en el algoritmo, lo que resulta en una puntuación mejorada.
Puntuación original: 9447 (mínimo 7, máximo 13, promedio 9,45) tiempo: 1876 segundos
El nuevo código es 278 líneas de Scala, debajo
object HorseBatteryStapleMastermind {
def main(args: Array[String]): Unit = run() print ()
val n = 1000 // # phrases to run
val verbose = true // whether to print each game
//tweakable parameters
val prob = 0.132 // probability threshold to guess spacing
val rngSeed = 11 // seed for random number generator
val minCounts = 6 // minimum char-set counts before guessing
val startTime = System.currentTimeMillis()
def time = System.currentTimeMillis() - startTime
val phraseList = io.Source.fromFile("pass.txt").getLines.toArray
val wordList = io.Source.fromFile("words.txt").getLines.toArray
case class Result(num: Int = 0, total: Int = 0, min: Int = Int.MaxValue, max: Int = 0) {
def update(count: Int) = Result(num + 1, total + count, Math.min(count, min), Math.max(count, max))
def resultString = f"#$num%4d Total: $total%5d Avg: ${total * 1.0 / num}%2.2f Range: ($min%2d-$max%2d)"
def timingString = f"Time: Total: ${time / 1000}%5ds Avg: ${time / (1000.0 * num)}%2.2fs"
def print() = println(s"$resultString\n$timingString")
}
def run(indices: Set[Int] = (0 until n).to[Set], prev: Result = Result()): Result = {
if (verbose && indices.size < n) prev.print()
val result = prev.update(Querent play Oracle(indices.head, phraseList(indices.head)))
if (indices.size == 1) result else run(indices.tail, result)
}
case class Oracle(idx: Int, phrase: String) {
def query(guess: String) = Grade.compute(guess, phrase)
}
object Querent {
def play(oracle: Oracle, n: Int = 0, notes: Notes = Notes0): Int = {
if (verbose && n == 0) println("=" * 100 + f"\nPhrase ${oracle.idx}%3d: ${oracle.phrase}")
val guess = notes.bestGuess
val grade = oracle.query(guess)
if (verbose) println(f"${n + 1}%2d: p${notes.phase} $grade $guess")
if (grade.success) n + 1 else play(oracle, n + 1, notes.update(guess, grade))
}
abstract class Notes(val phase: Int) {
def bestGuess: String
def update(guess: String, grade: Grade): Notes
}
case object Notes0 extends Notes(0) {
def bestGuess = GuessPack.firstGuess
def genSpaceCandidates(grade: Grade): List[Spacing] = (for {
wlen1 <- WordList.lengthRange
wlen2 <- WordList.lengthRange
spacing = Spacing(wlen1, wlen2, grade.total)
if spacing.freq > 0
if grade.black == spacing.black(bestGuess)
} yield spacing).sortBy(-_.freq).toList
def update(guess: String, grade: Grade) =
Notes1(grade.total, genSpaceCandidates(grade), Limiter(Counts.withMax(grade.total - 2), Nil), GuessPack.stream)
}
case class Notes1(phraseLength: Int, spacingCandidates: List[Spacing], limiter: Limiter, guesses: Stream[GuessPack]) extends Notes(1) {
def bestGuess = (chance match {
case x if x < prob => guesses.head.spacing.take(phraseLength)
case _ => spacingCandidates.head.mkString
}) + guesses.head.charSet
def totalFreq = spacingCandidates.foldLeft(0l)({ _ + _.freq })
def chance = spacingCandidates.head.freq * 1.0 / totalFreq
def update(guess: String, grade: Grade) = {
val newLim = limiter.update(guess, grade)
val newCands = spacingCandidates.filter(_.black(guess) == grade.black)
newCands match {
case best :: Nil if newLim.full => Notes3(newLim.allCandidates(best))
case best :: Nil => Notes2(best, newLim, guesses.tail)
case _ => Notes1(phraseLength, newCands, newLim, guesses.tail)
}
}
}
case class Notes2(spacing: Spacing, limiter: Limiter, guesses: Stream[GuessPack]) extends Notes(2) {
def bestGuess = tile(guesses.head.pattern) + guesses.head.charSet
def whiteSide(guess: String): String = guess.drop(spacing.phraseLength)
def blackSide(guess: String): String = guess.take(spacing.phraseLength)
def tile(guess: String) = spacing.lengths.map(guess.take).mkString(" ")
def untile(guess: String) = blackSide(guess).split(" ").maxBy(_.length) + "-"
def update(guess: String, grade: Grade) = {
val newLim = limiter.updateBoth(whiteSide(guess), untile(guess), grade)
if (newLim.full)
Notes3(newLim.allCandidates(spacing))
else
Notes2(spacing, newLim, guesses.tail)
}
}
case class Notes3(candidates: Array[String]) extends Notes(3) {
def bestGuess = sample.minBy(expLogNRC)
def update(guess: String, grade: Grade) =
Notes3(candidates.filter(phrase => grade == Grade.compute(guess, phrase)))
def numRemCands(phrase: String, guess: String): Int = {
val grade = Grade.compute(guess, phrase)
sample.count(phrase => grade == Grade.compute(guess, phrase))
}
val sample = if (candidates.size <= 32) candidates else candidates.sortBy(_.hashCode).take(32)
def expLogNRC(guess: String): Double = sample.map(phrase => Math.log(1.0 * numRemCands(phrase, guess))).sum
}
case class Spacing(wl1: Int, wl2: Int, phraseLength: Int) {
def wl3 = phraseLength - 2 - wl1 - wl2
def lengths = Array(wl1, wl2, wl3)
def pos = Array(wl1, wl1 + 1 + wl2)
def freq = lengths.map(WordList.freq).product
def black(guess: String) = pos.count(guess(_) == ' ')
def mkString = lengths.map("-" * _).mkString(" ")
}
case class Limiter(counts: Counts, guesses: List[String], extraGuesses: List[(String, Grade)] = Nil) {
def full = guesses.size >= minCounts
def update(guess: String, grade: Grade) =
if (guesses.size < Counts.Max)
Limiter(counts.update(grade.total - 2), guess :: guesses)
else
Limiter(counts, guesses, (guess, grade) :: extraGuesses)
def updateBoth(whiteSide: String, blackSide: String, grade: Grade) =
Limiter(counts.update(grade.total - 2).update(grade.black - 2), blackSide :: whiteSide :: guesses)
def isCandidate(phrase: String): Boolean = extraGuesses forall {
case (guess, grade) => grade == Grade.compute(guess, phrase)
}
def allCandidates(spacing: Spacing): Array[String] = {
val order = Array(0, 1, 2).sortBy(-spacing.lengths(_)) //longest word first
val unsort = Array.tabulate(3)(i => order.indexWhere(i == _))
val wordListI = WordList.byLength(spacing.lengths(order(0)))
val wordListJ = WordList.byLength(spacing.lengths(order(1)))
val wordListK = WordList.byLength(spacing.lengths(order(2)))
val gsr = guesses.reverse
val countsI = wordListI.map(Counts.compute(_, gsr).z)
val countsJ = wordListJ.map(Counts.compute(_, gsr).z)
val countsK = wordListK.map(Counts.compute(_, gsr).z)
val rangeI = 0 until wordListI.size
val rangeJ = 0 until wordListJ.size
val rangeK = 0 until wordListK.size
(for {
i <- rangeI.par
if Counts(countsI(i)) <= counts
j <- rangeJ
countsIJ = countsI(i) + countsJ(j)
if Counts(countsIJ) <= counts
k <- rangeK
if countsIJ + countsK(k) == counts.z
words = Array(wordListI(i), wordListJ(j), wordListK(k))
phrase = unsort.map(words).mkString(" ")
if isCandidate(phrase)
} yield phrase).seq.toArray
}
}
object Counts {
val Max = 9
val range = 0 until Max
def withMax(size: Int): Counts = Counts(range.foldLeft(size.toLong) { (z, i) => (z << 6) | size })
def compute(word: String, x: List[String]): Counts = x.foldLeft(Counts.withMax(word.length)) { (c: Counts, s: String) =>
c.update(if (s.last == '-') Grade.computeBlack(word, s) else Grade.computeTotal(word, s))
}
}
case class Counts(z: Long) extends AnyVal {
@inline def +(that: Counts): Counts = Counts(z + that.z)
@inline def apply(i: Int): Int = ((z >> (6 * i)) & 0x3f).toInt
@inline def size: Int = this(Counts.Max)
def <=(that: Counts): Boolean =
Counts.range.forall { i => (this(i) <= that(i)) && (this.size - this(i) <= that.size - that(i)) }
def update(c: Int): Counts = Counts((z << 6) | c)
override def toString = Counts.range.map(apply).map(x => f"$x%2d").mkString(f"Counts[$size%2d](", " ", ")")
}
case class GuessPack(spacing: String, charSet: String, pattern: String)
object GuessPack {
util.Random.setSeed(rngSeed)
val RBF: Any => Boolean = _ => util.Random.nextBoolean() //Random Boolean Function
def genCharsGuess(q: Char => Boolean): String =
(for (c <- 'a' to 'z' if q(c); j <- 1 to WordList.maxCount(c)) yield c).mkString
def charChooser(i: Int)(c: Char): Boolean = c match {
case 'e' => Array(true, true, true, false, false, false)(i % 6)
case 'i' => Array(false, true, false, true, false, true)(i % 6)
case 'a' => Array(true, false, false, true, true, false)(i % 6)
case 's' => Array(false, false, true, false, true, true)(i % 6)
case any => RBF(any)
}
def genSpaceGuess(q: Int => Boolean = RBF): String = genPatternGuess(" -", q)
def genPatternGuess(ab: String, q: Int => Boolean = RBF) =
(for (i <- 0 to 64) yield (if (q(i)) ab(0) else ab(1))).mkString
val firstGuess = genSpaceGuess(i => (i % 2) == 1) + genCharsGuess(_ => true)
val stream: Stream[GuessPack] = Stream.from(0).map { i =>
GuessPack(genSpaceGuess(), genCharsGuess(charChooser(i)), genPatternGuess("eias".filter(charChooser(i))))
}
}
}
object WordList {
val lengthRange = wordList.map(_.length).min until wordList.map(_.length).max
val byLength = Array.tabulate(lengthRange.end)(i => wordList.filter(_.length == i))
def freq(wordLength: Int): Long = if (lengthRange contains wordLength) byLength(wordLength).size else 0
val maxCount: Map[Char, Int] = ('a' to 'z').map(c => (c -> wordList.map(_.count(_ == c)).max * 3)).toMap
}
object Grade {
def apply(black: Int, white: Int): Grade = Grade(black | (white << 8))
val Success = Grade(-1)
def computeBlack(guess: String, phrase: String): Int = {
@inline def posRange: Range = 0 until Math.min(guess.length, phrase.length)
@inline def sameChar(p: Int): Boolean = (guess(p) == phrase(p)) && guess(p) != '-'
posRange count sameChar
}
def computeTotal(guess: String, phrase: String): Int = {
@inline def minCount(c: Char): Int = Math.min(phrase.count(_ == c), guess.count(_ == c))
minCount(' ') + ('a' to 'z').map(minCount).sum
}
def compute(guess: String, phrase: String): Grade = {
val black = computeBlack(guess, phrase)
if (black == guess.length && black == phrase.length)
Grade.Success
else
Grade(black, computeTotal(guess, phrase) - black)
}
}
case class Grade(z: Int) extends AnyVal {
def black: Int = z & 0xff
def white: Int = z >> 8
def total: Int = black + white
def success: Boolean = this == Grade.Success
override def toString = if (success) "SUCCESS" else f"($black%2d/$white%2d)"
}
}