Caza de huevos al estilo Collatz

11

¡Inspirado en The Great API Easter Egg Hunt!

Resumen

Su tarea es buscar un número entero predeterminado en el "espacio de Collatz" (que se explicará más adelante) utilizando el menor número de pasos posible.

Introducción

Este desafío se basa en la famosa conjetura de Collatz de la que esperamos que al menos todos aquí hayan oído hablar. Aquí hay un resumen tomado de Imprimir los números de Super Collatz .

La secuencia de Collatz (también llamada problema 3x + 1) es donde comienzas con cualquier número entero positivo, para este ejemplo usaremos 10 y le aplicaremos este conjunto de pasos:

if n is even:
    Divide it by 2
if n is odd:
    Multiply it by 3 and add 1
repeat until n = 1

La distancia de Collatz C(m,n)entre los dos números my n, para el propósito de este desafío, es la distancia entre dos números en el gráfico de Collatz (Créditos a @tsh para contarme sobre este concepto), que se define de la siguiente manera: (usando 21y 13como ejemplos ):

Escriba la secuencia de Collatz para m(en este caso 21):

21, 64, 32, 16, 8, 4, 2, 1

Escriba la secuencia de Collatz para n(en este caso 13):

13, 40, 20, 10, 5, 16, 8, 4, 2, 1

Ahora cuente cuántos números aparecen solo en una de las secuencias. Esto se define como la distancia de Collatz entre my n. En este caso 8, a saber,

21, 64, 32, 13, 40, 20, 10, 5

Entonces tenemos la distancia de Collatz entre 21y 13como C(21,13)=8.

C(m,n) tiene las siguientes propiedades agradables:

C(m,n)=C(n,m)
C(m,n)=0 iff. m=n

Esperemos que la definición de C(m,n)ahora sea clara. ¡Comencemos a buscar huevos en el espacio de Collatz!

Al comienzo del juego, un controlador decide la posición de un huevo de pascua, que se expresa mediante su coordenada unidimensional: un número entero en el intervalo [p,q](en otras palabras, un número entero entre py q, ambos extremos inclusive).

La posición del huevo permanece constante durante todo el juego. Denotaremos esta coordenada como r.

Ahora puede hacer una suposición inicial a 0 , y el controlador lo grabará. Esta es tu 0ª ronda. Si eres tan afortunado de haber acertado en el primer lugar (es decir, un 0 = r), el juego termina y tu puntaje es 0(Cuanto más bajo sea el puntaje, mejor). De lo contrario, ingresas a la primera ronda y haces una nueva aproximación a 1 , esto continúa hasta que lo has hecho bien, es decir, a n = r, y tu puntaje será n.

Para cada ronda posterior al 0, el controlador le ofrece una de las siguientes retroalimentaciones para que pueda adivinar mejor en función de la información proporcionada. Supongamos que se encuentra actualmente en la ntercera ronda y, por lo tanto, su suposición es un n

  • "¡Lo encontraste!" si a n = r, en cuyo caso el juego termina y usted anota n.
  • "Estás más cerca :)" si C (a n , r) <C (a n-1 , r)
  • "Estás dando vueltas alrededor del huevo" si C (a n , r) = C (a n-1 , r)
  • "Estás más lejos :(" si C (a n , r)> C (a n-1 , r)

Para guardar algunos bytes, llamaré a las respuestas como "Correcto", "Más cerca", "Mismo", "Más lejos", en el orden presentado anteriormente.

Aquí hay un ejemplo de juego con p=1,q=15.

  • a 0 = 10
  • a 1 = 11, respuesta: "Más cerca"
  • a 2 = 13, respuesta: "Más lejos"
  • a 3 = 4, respuesta: "Más lejos"
  • a 4 = 3, respuesta: "Más cerca"
  • a 5 = 5, respuesta: "Mismo"
  • a 6 = 7, respuesta: "Correcto"

Puntuación: 6.

Desafío

Diseña una estrategia determinista para jugar p=51, q=562con el mejor puntaje.

Las respuestas deben describir los algoritmos en detalle. Puede adjuntar cualquier código que ayude a dilucidar el algoritmo. Esto no es codegolf, por lo que se recomienda escribir código legible.

Las respuestas deben incluir el peor puntaje que puedan lograr para todos los casos posibles r, y el que tenga el puntaje más bajo gana. En el caso de empate, rganan los algoritmos que tienen un mejor puntaje promedio para todos los posibles s (que también deben incluirse en las respuestas). No hay más desempates y podemos tener múltiples ganadores al final.

Especificaciones

Recompensa (agregado después de que se publique la primera respuesta)

Personalmente, puedo ofrecer una recompensa por una respuesta en la que todas las suposiciones se hacen dentro del rango [51,562]sin dejar de tener un puntaje peor razonablemente bajo.

Weijun Zhou
fuente
¿Tienes un controlador?
user202729
No uno que sea como el de la pregunta original.
Weijun Zhou
1
C (m, n) es la distancia de m, n en el gráfico de Collatz .
tsh
Se me ocurrió el concepto yo mismo y no conocía el gráfico de Collatz. Gracias por decirme eso. Incluiré la información en la pregunta.
Weijun Zhou

Respuestas:

5

Rubí, 196

Esto fue mucho más difícil de lo que inicialmente pensé. Tuve que manejar muchos casos oscuros y terminé con mucho código feo. ¡Pero fue muy divertido! :)

Estrategia

Cada secuencia de Collatz termina con una secuencia de poderes de 2 (ej .: [16, 8, 4, 2, 1]). Tan pronto como se encuentre una potencia de 2, dividimos por 2 hasta llegar a 1. Llamemos a la primera potencia de 2 en una secuencia pow2 más cercana (ya que esta también es la potencia más cercana de 2 a nuestro número usando Collatz Distance). Para el rango dado (51-562), todos los números de pow2 más cercanos posibles son: [16, 64, 128, 256, 512, 1024]

Version corta

El algoritmo realiza:

  • Una búsqueda binaria para determinar la potencia más cercana de 2 al número actual
  • una búsqueda lineal para descubrir cada elemento anterior en la secuencia hasta que se descubra el número objetivo.

Versión detallada

Dado un juego con el número objetivo r, la estrategia es la siguiente:

  1. Utilice la búsqueda binaria para determinar la potencia de 2 más cercana ren la menor cantidad de pasos posible.
  2. Si la potencia más cercana de 2 que se encontró es la solución, deténgase. De lo contrario, continúe con 3.
  3. Dado que la potencia de 2 que se encontró es la primera potencia de 2 que ocurre en la secuencia, si se sigue, ese valor se alcanzó al hacer una operación (* 3 + 1). (Si llegó después de una operación de 2, entonces el número anterior también habría sido una potencia de 2). Calcule el número anterior en la secuencia haciendo la operación inversa (-1 y luego / 3)
  4. Si ese número es el objetivo, deténgase. De lo contrario, continúe con 5.
  5. Dado el número actual conocido de la secuencia, es necesario regresar y descubrir el número anterior en la secuencia. No se sabe si se llegó al número actual mediante una operación (/ 2) o (* 3 +1), por lo que el algoritmo los prueba a ambos y ve cuál arroja un número que está más cerca (como Collatz Distance) del objetivo .
  6. Si el número recién descubierto es el correcto, deténgase.
  7. Usando el número recién descubierto, regrese al paso 5.

Los resultados

Ejecutar el algoritmo para todos los números en el rango 51-562 toma alrededor de un segundo en una PC normal y el puntaje total es 38665.

El código

Pruébalo en línea!

require 'set'

# Utility methods
def collatz(n)
  [].tap do |results|
    crt = n
    while true
      results << crt
      break if crt == 1
      crt = crt.even? ? crt / 2 : crt * 3 + 1
    end
  end
end

def collatz_dist(m, n)
  cm = collatz(m).reverse
  cn = collatz(n).reverse
  common_length = cm.zip(cn).count{ |x, y| x == y }
  cm.size + cn.size - common_length * 2
end



GuessResult = Struct.new :response, :score
# Class that can "play" a game, responding
# :right, :closer, :farther or :same when
# presented with a guess
class Game

  def initialize(target_value)
    @r = target_value
    @score = -1
    @dist = nil
    @won = false
  end
  attr_reader :score

  def guess(n)
    # just a logging decorator over the real method
    result = internal_guess(n)
    p [n, result] if LOGGING
    result
  end

  private

  def internal_guess(n)
    raise 'Game already won!' if @won
    @score += 1
    dist = collatz_dist(n, @r)
    if n == @r
      @won = true
      return GuessResult.new(:right, @score)
    end
    response = nil
    if @dist
      response = [:same, :closer, :farther][@dist <=> dist]
    end
    @dist = dist
    GuessResult.new(response)
  end

end

# Main solver code

def solve(game)
  pow2, won = find_closest_power_of_2(game)
  puts "Closest pow2: #{pow2}" if LOGGING

  return pow2 if won
  # Since this is the first power of 2 in the series, it follows that
  # this element must have been arrived at by doing *3+1...
  prev = (pow2 - 1) / 3
  guess = game.guess(prev)
  return prev if guess.response == :right

  solve_backward(game, prev, 300)
end

def solve_backward(game, n, left)
  return 0 if left == 0
  puts "***      Arrived at  ** #{n} **" if LOGGING
  # try to see whether this point was reached by dividing by 2
  double = n * 2
  guess = game.guess(double)
  return double if guess.response == :right

  if guess.response == :farther && (n - 1) % 3 == 0
    # try to see whether this point was reached by *3+1
    third = (n-1) / 3
    guess = game.guess(third)
    return third if guess.response == :right
    if guess.response == :closer
      return solve_backward(game, third, left-1)
    else
      game.guess(n) # reset to n...
    end
  end
  return solve_backward(game, double, left-1)
end


# Every Collatz Sequence ends with a sequence of powers of 2.
# Let's call the first occurring power of 2 in such a sequence
# POW2
#
# Let's iterate through the whole range and find the POW2_CANDIDATES
#
RANGE = [*51..562]
POWERS = Set.new([*0..15].map{ |n| 2 ** n })

POW2_CANDIDATES =
  RANGE.map{ |n| collatz(n).find{ |x| POWERS.include? x} }.uniq.sort
# Turns out that the candidates are [16, 64, 128, 256, 512, 1024]

def find_closest_power_of_2(game)
  min = old_guess = 0
  max = new_guess = POW2_CANDIDATES.size - 1
  guess = game.guess(POW2_CANDIDATES[old_guess])
  return POW2_CANDIDATES[old_guess], true if guess.response == :right
  guess = game.guess(POW2_CANDIDATES[new_guess])
  return POW2_CANDIDATES[new_guess], true if guess.response == :right
  pow2 = nil

  while pow2.nil?

    avg = (old_guess + new_guess) / 2.0

    case guess.response
    when :same
      # at equal distance from the two ends
      pow2 = POW2_CANDIDATES[avg.floor]
      # still need to test if this is correct
      guess = game.guess(pow2)
      return pow2, guess.response == :right
    when :closer
      if old_guess < new_guess
        min = avg.ceil
      else
        max = avg.floor
      end
    when :farther
      if old_guess < new_guess
        max = avg.floor
      else
        min = avg.ceil
      end
    end

    old_guess = new_guess
    new_guess = (min + max) / 2
    new_guess = new_guess + 1 if new_guess == old_guess
    # so we get next result relative to the closer one
    # game.guess(POW2_CANDIDATES[old_guess]) if guess.response == :farther
    guess = game.guess(POW2_CANDIDATES[new_guess])

    if guess.response == :right
      pow2 = POW2_CANDIDATES[new_guess]
      break
    end

    if min == max
      pow2 = POW2_CANDIDATES[min]
      break
    end

  end

  [pow2, guess.response == :right]

end



LOGGING = false

total_score = 0
51.upto(562) do |n|
  game = Game.new(n)
  result = solve(game)
  raise "Incorrect result for #{n} !!!" unless result == n
  total_score += game.score
end
puts "Total score: #{total_score}"
Cristian Lupascu
fuente
Impresionante. Hay un punto menor: creo que uno de los comentarios no debería decir "cuadrado perfecto".
Weijun Zhou
1
@WeijunZhou Tienes razón. ¡Fijo!
Cristian Lupascu
Probablemente deberías incluir la peor puntuación para todos los casos, que es 196.
Weijun Zhou
3

peor puntaje: 11, puntaje total: 3986

Todas las conjeturas están dentro del alcance [51,562].

Mi algoritmo:

  1. Adivine por primera vez 512 y mantenga un conjunto de resultados posibles vals, inicialmente el conjunto contiene todos los números dentro del rango [51,562].
  2. En cada paso, haga lo siguiente:

    1. Encontrar el valor de siguiente suposición guessen el rango [51,562]de tal manera que, cuando los valores de vals(excluyendo guesssí mismo) se divide en 3 series correspondientes a los posibles resultados Closer, Samey Farther, el tamaño máximo de los 3 conjuntos es mínimo.
      Si hay varios valores posibles para guesssatisfacer lo anterior, elija el más pequeño.
    2. Adivina el valor guess.
    3. Si la respuesta es "Correcta", listo (salga del programa).
    4. Elimine todos los valores del conjunto de valsmodo que no puedan dar ese resultado.

Mi implementación de referencia escrita en C ++ y Bash se ejecuta en aproximadamente 7,6 segundos en mi máquina y proporciona la peor puntuación / puntaje de suma como se describe en el título.

Probar todos los valores de primera aproximación posibles llevará aproximadamente 1,5 horas en mi máquina. Puedo considerar hacer eso.

usuario202729
fuente
(P / S: se permiten envíos sin código. Si no confía en mi puntaje, simplemente
impleméntelo
Pero si realmente quiere verlo funcionar sin volver a implementarlo por alguna razón, ¡ Pruébelo en línea !
user202729
Espere un minuto, ¿por qué no puedo dejar que mi programa genere un árbol de decisión y puntúe: | sería mucho más rápido ...
user202729