Caça aos ovos no estilo Collatz

11

Inspirado pela grande caça aos ovos de Páscoa da API!

Sumário

Sua tarefa é procurar um número inteiro predeterminado no "espaço Collatz" (a ser explicado posteriormente) usando o menor número possível de etapas.

Introdução

Esse desafio é baseado na famosa conjectura de Collatz, da qual esperamos que todos aqui pelo menos tenham ouvido falar. Aqui está um resumo de Imprimir os números Super Collatz .

A sequência Collatz (também chamada de problema 3x + 1) é onde você começa com um número inteiro positivo; neste exemplo, usaremos 10 e aplicaremos este conjunto de etapas:

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

A distância Collatz C(m,n)entre os dois números me n, para o objetivo deste desafio, é a distância entre dois números no gráfico Collatz (créditos para @tsh por me falar sobre esse conceito), que é definida da seguinte forma: (usando 21e 13como exemplos ):

Anote a sequência Collatz para m(neste caso 21):

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

Anote a sequência Collatz para n(neste caso 13):

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

Agora conte quantos números aparecem apenas em uma das seqüências. Isso é definido como a distância da Collatz entre me n. Nesse caso 8, ou seja,

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

Portanto, temos a distância de Collatz entre 21e 13como C(21,13)=8.

C(m,n) tem as seguintes boas propriedades:

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

Esperemos que a definição de C(m,n)agora seja clara. Vamos começar a caçar ovos no espaço Collatz!

No início do jogo, um controlador decide a posição de um ovo de páscoa, que é expresso por sua coordenada unidimensional: Um número inteiro no intervalo [p,q](em outras palavras, um número inteiro entre pe q, ambas as extremidades, inclusive).

A posição do ovo permanece constante durante todo o jogo. Denotaremos essa coordenada como r.

Agora você pode fazer um palpite inicial de 0 e ele será gravado pelo controlador. Esta é sua 0ª rodada. Se você tiver tanta sorte que acertou em primeiro lugar (ou seja, 0 = r), o jogo termina e sua pontuação é 0(quanto menor a pontuação, melhor). Caso contrário, você entra na 1ª rodada e faz um novo palpite a 1 , isso continua até você acertar, ou seja, n = r, e sua pontuação será n.

Para cada rodada após o dia 0, o controlador fornece um dos seguintes feedbacks para que você possa adivinhar melhor com base nas informações fornecidas. Vamos supor que você esteja atualmente na nronda e, portanto, seu palpite é n

  • "Você achou!" se n = r, nesse caso o jogo termina e você marca n.
  • "Você está mais perto :)" se C (a n , r) <C (a n-1 , r)
  • "Você está circulando em torno do ovo" se C (a n , r) = C (a n-1 , r)
  • "Você está mais longe :(" se C (a n , r)> C (a n-1 , r)

Para salvar alguns bytes, chamarei as respostas como "Direita", "Mais próxima", "Mesmo", "Mais longe", na ordem apresentada acima.

Aqui está um exemplo de jogo com p=1,q=15.

  • a 0 = 10
  • a 1 = 11, resposta: "Mais próximo"
  • a 2 = 13, resposta: "Mais longe"
  • a 3 = 4, resposta: "Mais longe"
  • a 4 = 3, resposta: "Mais próximo"
  • a 5 = 5, resposta: "Mesmo"
  • a 6 = 7, resposta: "Certo"

Pontuação: 6.

Desafio

Crie uma estratégia determinística para jogar p=51, q=562com a melhor pontuação.

As respostas devem descrever os algoritmos em detalhes. Você pode anexar qualquer código que ajude a elucidar o algoritmo. Como não é um codegolf, você deve escrever um código legível.

As respostas devem incluir a pior pontuação possível para todos os casos possíveis re a que tiver a pior pontuação mais baixa vence. No caso de empate, os algoritmos com melhor pontuação média para todos os rs possíveis (que também devem ser incluídos nas respostas) vencem. Não há mais desempates e podemos ter vários vencedores no final.

Especificações

  • Para reiterar, restá no intervalo [51,562].
  • Aplicam-se brechas padrão .

Recompensa (adicionada após a primeira resposta ser publicada)

Pessoalmente, posso oferecer uma recompensa a uma resposta em que todas as suposições são feitas dentro do intervalo [51,562]e ainda tendo uma pontuação mais baixa razoavelmente baixa.

Weijun Zhou
fonte
Você tem um controlador?
usar o seguinte comando
Não é igual ao da pergunta original.
Weijun Zhou
1
C (m, n) é a distância de m, n no gráfico de Collatz .
TSH
Eu mesmo criei o conceito e não conhecia o gráfico da Collatz. Obrigado por me dizer isso. Vou incluir as informações na pergunta.
Weijun Zhou

Respostas:

5

Ruby, 196

Isso foi muito mais difícil do que eu pensava inicialmente. Eu tive que lidar com muitos casos obscuros e acabei com um monte de código feio. Mas foi muito divertido! :)

Estratégia

Toda sequência de Collatz termina com uma sequência de potências de 2 (ex: [16, 8, 4, 2, 1]). Assim que uma potência de 2 é encontrada, dividimos por 2 até chegarmos a 1. Vamos chamar a primeira potência de 2 em uma sequência mais próxima de pow2 (pois essa também é a potência mais próxima de 2 do nosso número usando a distância de Collatz). Para o intervalo especificado (51-562), todos os números pow2 mais próximos possíveis são: [16, 64, 128, 256, 512, 1024]

Versão curta

O algoritmo executa:

  • uma pesquisa binária para descobrir a potência mais próxima de 2 do número atual
  • uma pesquisa linear para descobrir todos os elementos anteriores na sequência até que o número de destino seja descoberto.

Versão detalhada

Dado um jogo com o número alvo r, a estratégia é a seguinte:

  1. Use a pesquisa binária para descobrir a potência de 2 mais próxima rno menor número de etapas possível.
  2. Se a potência mais próxima de 2 encontrada for a solução, pare. Caso contrário, continue com 3.
  3. Como a potência 2 encontrada foi a primeira potência 2 ocorrendo na sequência, se segue que esse valor foi atingido através da operação (* 3 + 1). (Se surgisse após uma operação / 2, o número anterior também seria uma potência de 2). Calcule o número anterior na sequência executando a operação reversa (-1 e depois / 3)
  4. Se esse número for o alvo, pare. Caso contrário, continue com 5.
  5. Dado o número atual conhecido da sequência, é necessário voltar e descobrir o número anterior na sequência. Não se sabe se o número atual foi alcançado por uma operação (/ 2) ou (* 3 +1); portanto, o algoritmo tenta os dois e vê qual deles gera um número mais próximo (como distância de Collatz) do destino .
  6. Se o número recém-descoberto for o correto, pare.
  7. Usando o número recém-descoberto, volte para a etapa 5.

Os resultados

A execução do algoritmo para todos os números no intervalo 51-562 leva cerca de um segundo em um PC normal e a pontuação total é 38665.

O código

Experimente online!

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
fonte
Impressionante. Há uma questão menor: acredito que um dos comentários não deve dizer "quadrado perfeito".
Weijun Zhou
1
@WeijunZhou Você está certo. Fixo!
Cristian Lupascu 30/0318
Você provavelmente deve incluir o pior resultado para todos os casos, que é 196.
Weijun Zhou
3

pior pontuação: 11, soma resultado: 3986

Todas as suposições estão ao alcance [51,562].

Meu algoritmo:

  1. Adivinhe pela primeira vez 512 e mantenha um conjunto de resultados possíveis vals, inicialmente o conjunto contém todos os números no intervalo [51,562].
  2. Em cada etapa, faça o seguinte:

    1. Encontrar o valor do próximo suposição guessno intervalo [51,562]de tal modo que, quando os valores em vals(excluindo o guesspróprio) é dividida em 3 conjuntos correspondentes aos resultados possíveis Closer, Samee Farther, o tamanho máximo dos conjuntos 3 é mínima.
      Se houver vários valores possíveis deguess satisfazer o acima, escolha o menor.
    2. Adivinha o valor guess.
    3. Se a resposta for "Correta", finalizada (saia do programa).
    4. Remova todos os valores do conjunto para valsque eles não possam dar esse resultado.

Minha implementação de referência escrita em C ++ e Bash é executada em cerca de 7,6 segundos na minha máquina e fornece a pior pontuação de pontuação / soma, conforme descrito no título.

A tentativa de todos os valores possíveis de primeira estimativa leva cerca de 1,5 horas na minha máquina. Eu posso considerar fazer isso.

user202729
fonte
(P / S: apresentações não-código são permitidos Se você não confia em minha pontuação, apenas implementá-lo e ver.)
user202729
Mas se você realmente deseja vê-lo funcionando sem reimplementá-lo por alguns motivos, Experimente on-line !
precisa saber é o seguinte
Espere um minuto, por que não posso simplesmente deixar meu programa gerar uma árvore de decisão e pontuá-la: | seria muito mais rápido ...
user202729