Caccia alle uova in stile Collatz


11

Ispirato da The Great API Easter Egg Hunt!

Sommario

Il tuo compito è quello di cercare un numero intero predeterminato nello "spazio Collatz" (da spiegare in seguito) usando il minor numero possibile di passaggi.

introduzione

Questa sfida si basa sulla famosa congettura di Collatz di cui almeno tutti qui hanno sentito parlare. Ecco un riepilogo tratto da Stampa i numeri di Super Collatz .

La sequenza Collatz (chiamata anche problema 3x + 1) è dove inizi con qualsiasi numero intero positivo, per questo esempio useremo 10 e applicheremo questo set di passaggi ad esso:

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

La distanza Collatz C(m,n)tra i due numeri me n, ai fini di questa sfida, è la distanza tra due numeri nel grafico Collatz (crediti a @tsh per avermi parlato di questo concetto), che è definito come segue: (usando 21e 13come esempi ):

Annota la sequenza Collatz per m(in questo caso, 21):

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

Annota la sequenza Collatz per n(in questo caso, 13):

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

Ora conta quanti numeri compaiono solo in una delle sequenze. Questo è definito come la distanza Collatz tra me n. In questo caso 8, vale a dire

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

Quindi abbiamo la distanza Collatz tra 21e 13come C(21,13)=8.

C(m,n) hanno le seguenti belle proprietà:

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

Spero che la definizione di C(m,n)sia ora chiara. Cominciamo a cacciare le uova nello spazio Collatz!

All'inizio del gioco, un controller decide la posizione di un uovo di Pasqua, che è espresso dalla sua coordinata unidimensionale: un numero intero nell'intervallo [p,q](in altre parole, un numero intero tra pe q, entrambe le estremità incluse).

La posizione dell'uovo rimane costante durante il gioco. Indicheremo questa coordinata come r.

Ora puoi fare un'ipotesi iniziale a 0 e verrà registrata dal controller. Questo è il tuo 0 ° round. Se sei così fortunato da aver capito bene al primo posto (ovvero uno 0 = r), il gioco termina e il tuo punteggio è 0(Più basso è il punteggio, meglio è). Altrimenti, entri nel 1 ° round e fai una nuova ipotesi a 1 , questo continua fino a quando non hai capito bene, cioè a n = r, e il tuo punteggio sarà n.

Per ogni round successivo allo 0, il controller ti fornisce uno dei seguenti feedback in modo da poter fare un'ipotesi migliore sulla base delle informazioni fornite. Supponiamo che tu sia attualmente al nth round e quindi la tua ipotesi è un n

  • "L'hai trovato!" se a n = r, nel qual caso il gioco termina e si segna n.
  • "Sei più vicino :)" se C (a n , r) <C (a n-1 , r)
  • "Stai girando intorno all'uovo" se C (a n , r) = C (a n-1 , r)
  • "Sei più lontano :(" se C (a n , r)> C (a n-1 , r)

Per salvare alcuni byte, chiamerò le risposte come "Giusto", "Più vicino", "Stesso", "Più lontano", nell'ordine presentato sopra.

Ecco un esempio di gioco con p=1,q=15.

  • a 0 = 10
  • a 1 = 11, risposta: "Più vicino"
  • a 2 = 13, risposta: "Più lontano"
  • a 3 = 4, risposta: "Più lontano"
  • a 4 = 3, risposta: "Più vicino"
  • a 5 = 5, risposta: "Stesso"
  • a 6 = 7, risposta: "Giusto"

Punteggio: 6.

Sfida

Progetta una strategia deterministica per giocare p=51, q=562con il punteggio migliore.

Le risposte dovrebbero descrivere gli algoritmi in dettaglio. È possibile allegare qualsiasi codice che aiuti a chiarire l'algoritmo. Questo non è codegolf, quindi sei incoraggiato a scrivere un codice leggibile.

Le risposte dovrebbero includere il punteggio peggiore che possono raggiungere per tutti i casi possibili re quello con il punteggio peggiore più basso vince. In caso di pareggio, rvincono gli algoritmi che hanno un punteggio medio migliore per tutti i possibili s (che dovrebbero essere inclusi anche nelle risposte). Non ci sono ulteriori spareggi e alla fine potremmo avere più vincitori.

Specifiche

Bounty (aggiunto dopo la pubblicazione della prima risposta)

Personalmente posso offrire una generosità a una risposta in cui tutte le ipotesi sono fatte all'interno del range [51,562]pur avendo un punteggio peggiore ragionevolmente basso.


Hai un controller?
user202729

Non uno che è come quello nella domanda originale.
Weijun Zhou

1
C (m, n) è la distanza di m, n sul grafico Collatz .
TSH

Mi sono inventato il concetto da solo e non conoscevo il grafico di Collatz. Grazie per avermelo detto. Includerò le informazioni nella domanda.
Weijun Zhou

Risposte:


5

Ruby, 196

Questo era molto più difficile che inizialmente pensavo. Ho dovuto gestire molti casi oscuri e ho finito con un sacco di brutti codici. Ma è stato molto divertente! :)

Strategia

Ogni sequenza di Collatz finisce con una sequenza di poteri di 2 (es: [16, 8, 4, 2, 1]). Non appena si incontra una potenza di 2, ci dividiamo per 2 fino a raggiungere 1. Chiamiamo la prima potenza di 2 in una sequenza pow2 più vicina (poiché questa è anche la potenza più vicina di 2 al nostro numero usando la distanza Collatz). Per l'intervallo dato (51-562), tutti i possibili numeri pow2 più vicini sono: [16, 64, 128, 256, 512, 1024]

Versione breve

L'algoritmo esegue:

  • una ricerca binaria per capire la potenza più vicina di 2 al numero corrente
  • una ricerca lineare per capire ogni elemento precedente nella sequenza fino a quando non viene scoperto il numero di destinazione.

Versione dettagliata

Dato un gioco con il numero di destinazione r, la strategia è la seguente:

  1. Usa la ricerca binaria per capire la potenza di 2 più vicina a rin il minor numero di passaggi possibile.
  2. Se la soluzione più vicina a 2 trovata è la soluzione, fermati. Altrimenti continua a 3.
  3. Poiché la potenza di 2 che è stata trovata è la prima potenza di 2 presente nella sequenza, se segue quel valore è stato raggiunto eseguendo un'operazione (* 3 + 1). (Se fosse arrivato dopo un'operazione / 2, anche il numero precedente sarebbe stato una potenza di 2). Calcola il numero precedente nella sequenza eseguendo l'operazione inversa (-1 e quindi / 3)
  4. Se quel numero è il bersaglio, fermati. Altrimenti continua con 5.
  5. Dato il numero corrente noto dalla sequenza, è necessario tornare indietro e scoprire il numero precedente nella sequenza. Non è noto se il numero corrente sia stato raggiunto da un'operazione (/ 2) o (* 3 +1), quindi l'algoritmo li prova entrambi e vede quale produce un numero più vicino (come Collatz Distance) dal bersaglio .
  6. Se il numero appena scoperto è quello giusto, fermati.
  7. Utilizzando il numero appena scoperto, tornare al passaggio 5.

I risultati

L'esecuzione dell'algoritmo per tutti i numeri nell'intervallo 51-562 richiede circa un secondo su un normale PC e il punteggio totale è 38665.

Il codice

Provalo 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}"

Degno di nota. C'è un piccolo punto: credo che uno dei commenti non dovrebbe dire "quadrato perfetto".
Weijun Zhou,

1
@WeijunZhou Hai ragione. Fisso!
Cristian Lupascu,

Probabilmente dovresti includere il punteggio peggiore per tutti i casi, che è 196.
Weijun Zhou

3

punteggio peggiore: 11, punteggio somma: 3986

Tutte le ipotesi sono nel raggio [51,562].

Il mio algoritmo:

  1. Indovina per la prima volta 512 e mantieni una serie di possibili risultati vals, inizialmente la serie contiene tutti i numeri nell'intervallo [51,562].
  2. Ad ogni passaggio, procedi come segue:

    1. Trovare il valore della prossima ipotesi guessin serie [51,562]in modo tale che, quando i valori di vals(escluse guessstesso) è partizionato in 3 gruppi corrispondenti alle possibili risultati Closer, Samee Fartherla dimensione massima di tali insiemi 3 è minimo.
      Se ci sono più valori possibili per guesssoddisfare quanto sopra, scegli il più piccolo.
    2. Indovina il valore guess.
    3. Se la risposta è "Giusto", fatto (esci dal programma).
    4. Rimuovere tutti i valori nell'insieme in modo valstale che non possano dare quel risultato.

La mia implementazione di riferimento scritta in C ++ e Bash gira in circa 7,6 secondi sulla mia macchina e dà il punteggio peggiore / somma come descritto nel titolo.

Provare tutti i possibili valori della prima ipotesi richiederà circa 1,5 ore sulla mia macchina. Potrei considerare di farlo.


(P / S: sono consentiti invii senza codice. Se non ti fidi del mio punteggio, implementalo da solo e vedi)
user202729

Ma se vuoi davvero vederlo funzionare senza reimplementarlo per alcuni motivi, provalo online !
user202729

Aspetta un attimo perché non posso semplicemente lasciare che il mio programma produca un albero decisionale e segnalo: | sarebbe molto più veloce ...
user202729
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.