Eiersuche im Collatz-Stil

11

Inspiriert von The Great API Easter Egg Hunt!

Zusammenfassung

Ihre Aufgabe ist es, im "Collatz-Raum" (der später erklärt wird) mit möglichst wenigen Schritten nach einer vorgegebenen Ganzzahl zu suchen.

Einführung

Diese Herausforderung basiert auf der berühmten Collatz-Vermutung, von der hoffentlich jeder hier zumindest gehört hat. Hier ist eine Zusammenfassung von Print the Super Collatz-Nummern .

In der Collatz-Sequenz (auch als 3x + 1-Problem bezeichnet) beginnen Sie mit einer positiven Ganzzahl. In diesem Beispiel verwenden wir 10 und wenden diese Schritte darauf an:

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

Der Collatz-Abstand C(m,n)zwischen den beiden Zahlen mund nfür die Zwecke dieser Herausforderung der Abstand zwischen zwei Zahlen im Collatz-Diagramm (Dank an @tsh, um mich über dieses Konzept zu informieren), der wie folgt definiert ist: (anhand 21und 13als Beispiele ):

Schreiben Sie die Collatz-Sequenz für m(in diesem Fall 21) auf:

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

Schreiben Sie die Collatz-Sequenz für n(in diesem Fall 13) auf:

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

Zählen Sie nun, wie viele Zahlen nur in einer der Sequenzen erscheinen. Dies ist definiert als der Collatz-Abstand zwischen mund n. In diesem Fall 8nämlich.

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

Wir haben also Collatz Abstand zwischen 21und 13als C(21,13)=8.

C(m,n) haben die folgenden schönen Eigenschaften:

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

Hoffentlich ist die Definition von C(m,n)jetzt klar. Beginnen wir mit der Eiersuche im Collatz-Raum!

Zu Beginn des Spiels entscheidet ein Controller über die Position eines Ostereies, die durch seine eindimensionale Koordinate ausgedrückt wird: Eine Ganzzahl im Intervall [p,q](mit anderen Worten eine Ganzzahl zwischen pund q, beide Enden einschließlich).

Die Position des Eies bleibt während des Spiels konstant. Wir bezeichnen diese Koordinate als r.

Sie können jetzt eine erste Schätzung einer 0 vornehmen , die vom Controller aufgezeichnet wird. Dies ist deine 0. Runde. Wenn Sie so viel Glück haben, dass Sie es auf den ersten Platz richtig gemacht haben (dh eine 0 = r), endet das Spiel und Ihre Punktzahl ist 0(Je niedriger die Punktzahl, desto besser). Andernfalls betreten Sie die 1. Runde und raten eine neue 1 , dies geht so lange weiter, bis Sie es richtig verstanden haben, dh a n = r, und Ihre Punktzahl wird sein n.

Für jede Runde nach dem 0. gibt Ihnen der Controller eine der folgenden Rückmeldungen, damit Sie anhand der angegebenen Informationen eine bessere Vermutung anstellen können. Nehmen wir an, Sie befinden sich derzeit in der ndritten Runde und daher ist Ihre Vermutung ein n

  • "Du hast es gefunden!" Wenn a n = r, endet das Spiel und Sie erzielen ein Tor n.
  • "Du bist näher :)" wenn C (a n , r) <C (a n-1 , r)
  • "Sie kreisen um das Ei", wenn C (a n , r) = C (a n-1 , r)
  • "Du bist weiter weg :(" wenn C (a n , r)> C (a n-1 , r)

Um einige Bytes zu sparen, werde ich die Antworten in der oben angegebenen Reihenfolge als "Richtig", "Näher", "Gleich", "Weiter" bezeichnen.

Hier ist ein Beispielspiel mit p=1,q=15.

  • a 0 = 10
  • a 1 = 11, Antwort: "Näher"
  • a 2 = 13, Antwort: "Weiter"
  • a 3 = 4, Antwort: "Weiter"
  • a 4 = 3, Antwort: "Näher"
  • a 5 = 5, Antwort: "Same"
  • a 6 = 7, Antwort: "Richtig"

Punktzahl : 6.

Herausforderung

Entwerfen Sie eine deterministische Strategie, um das Spiel p=51, q=562mit der besten Punktzahl zu spielen.

Die Antworten sollten die Algorithmen im Detail beschreiben. Sie können jeden Code anhängen, der zur Erläuterung des Algorithmus beiträgt. Dies ist kein Codegolf, daher sollten Sie lesbaren Code schreiben.

Die Antworten sollten die schlechteste Punktzahl enthalten, die sie in allen möglichen Fällen erreichen können r, und diejenige mit der niedrigsten schlechtesten Punktzahl gewinnt. Im Falle eines Unentschieden gewinnen die Algorithmen, die eine bessere durchschnittliche Punktzahl für alle möglichen rs haben (die auch in den Antworten enthalten sein sollten). Es gibt keine weiteren Tie Breaker und wir haben möglicherweise am Ende mehrere Gewinner.

Technische Daten

  • Um es noch einmal zu wiederholen, rliegt in der Pause [51,562].
  • Es gelten Standardlücken .

Kopfgeld (hinzugefügt, nachdem die erste Antwort veröffentlicht wurde)

Ich kann persönlich eine Prämie für eine Antwort anbieten, bei der alle Vermutungen innerhalb des Bereichs gemacht werden, [51,562]während ich immer noch eine einigermaßen niedrige schlechteste Punktzahl habe.

Weijun Zhou
quelle
Hast du einen Controller?
user202729
Nicht einer, der dem in der ursprünglichen Frage entspricht.
Weijun Zhou
1
C (m, n) ist der Abstand von m, n im Collatz-Diagramm .
tsh
Ich habe mir das Konzept selbst ausgedacht und kannte das Collatz-Diagramm nicht. Danke, dass du mir das erzählt hast. Ich werde die Informationen in die Frage aufnehmen.
Weijun Zhou

Antworten:

5

Ruby, 196

Das war viel schwieriger, als ich ursprünglich gedacht hatte. Ich musste mit vielen obskuren Fällen umgehen und bekam viel hässlichen Code. Aber es hat viel Spaß gemacht! :) :)

Strategie

Jede Collatz-Sequenz endet mit einer Potenzfolge von 2 (Beispiel: [16, 8, 4, 2, 1]). Sobald eine Potenz von 2 angetroffen wird, teilen wir durch 2, bis wir 1 erreichen. Nennen wir die erste Potenz von 2 in einer Sequenz, die pow2 am nächsten kommt (da dies auch die Potenz von 2 ist, die unserer Zahl mit Collatz Distance am nächsten kommt). Für den angegebenen Bereich (51-562) sind alle möglichen nächsten pow2- Zahlen: [16, 64, 128, 256, 512, 1024]

Kurzfassung

Der Algorithmus führt Folgendes aus:

  • eine binäre Suche, um die der aktuellen Zahl am nächsten liegende Potenz von 2 herauszufinden
  • Eine lineare Suche, um jedes vorherige Element in der Sequenz herauszufinden, bis die Zielnummer entdeckt wird.

Detaillierte Version

Bei einem Spiel mit der Zielnummer rlautet die Strategie wie folgt:

  1. Verwenden Sie die binäre Suche, um rin so wenigen Schritten wie möglich die Potenz von 2 zu ermitteln, die am nächsten kommt .
  2. Wenn die nächste gefundene Potenz von 2 die Lösung ist, stoppen Sie. Ansonsten weiter mit 3.
  3. Da die gefundene Potenz von 2 die erste Potenz von 2 ist, die in der Sequenz auftritt, folgt, wenn folgt, dass dieser Wert durch Ausführen einer (* 3 + 1) -Operation erreicht wurde. (Wenn es nach einer / 2-Operation gekommen wäre, wäre die vorherige Zahl auch eine Potenz von 2 gewesen). Berechnen Sie die vorherige Zahl in der Sequenz, indem Sie die umgekehrte Operation ausführen (-1 und dann / 3).
  4. Wenn diese Zahl das Ziel ist, stoppen Sie. Ansonsten weiter mit 5.
  5. Angesichts der aktuellen Nummer, die aus der Sequenz bekannt ist, muss die vorherige Nummer in der Sequenz ermittelt werden. Es ist nicht bekannt, ob die aktuelle Zahl durch eine (/ 2) - oder (* 3 + 1) -Operation erreicht wurde, daher versucht der Algorithmus beide und sieht, welche eine Zahl ergibt, die näher (als Collatz-Entfernung) vom Ziel liegt .
  6. Wenn die neu entdeckte Nummer die richtige ist, stoppen Sie.
  7. Fahren Sie mit der neu entdeckten Nummer mit Schritt 5 fort.

Die Ergebnisse

Das Ausführen des Algorithmus für alle Zahlen im Bereich 51-562 dauert auf einem normalen PC ungefähr eine Sekunde und die Gesamtpunktzahl beträgt 38665.

Der Code

Probieren Sie es online aus!

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
quelle
Beeindruckend. Es gibt einen kleinen Punkt: Ich glaube, einer der Kommentare sollte nicht "perfektes Quadrat" sagen.
Weijun Zhou
1
@WeijunZhou Du hast recht. Fest!
Cristian Lupascu
Sie sollten wahrscheinlich die schlechteste Punktzahl für alle Fälle
angeben
3

schlechteste Punktzahl: 11, Gesamtpunktzahl: 3986

Alle Vermutungen sind in Reichweite [51,562].

Mein Algorithmus:

  1. Erraten Sie zum ersten Mal 512 und behalten Sie eine Reihe möglicher Ergebnisse bei vals. Zunächst enthält die Menge alle Zahlen im Bereich [51,562].
  2. Gehen Sie bei jedem Schritt wie folgt vor:

    1. Finden der Wert des nächsten guess guessin Reihe , [51,562]so dass, wenn die Werte in vals(ohne guessselbst) in 3 Sätzen partitioniert ist auf die möglichen Ergebnisse entspricht Closer, Sameund Fartherist die maximale Größe dieser 3 Sätze Minimum.
      Wenn es mehrere mögliche Werte gibt, guessdie die oben genannten erfüllen, wählen Sie den kleinsten.
    2. Errate den Wert guess.
    3. Wenn die Antwort "Richtig" lautet, fertig (Programm beenden).
    4. Entfernen Sie alle Werte in der Menge valsso, dass sie möglicherweise nicht zu diesem Ergebnis führen können.

Meine in C ++ und Bash geschriebene Referenzimplementierung läuft auf meinem Computer in ca. 7,6 Sekunden und liefert die schlechteste Punktzahl / Summenpunktzahl, wie im Titel beschrieben.

Das Ausprobieren aller möglichen First-Guess-Werte dauert auf meinem Computer ungefähr 1,5 Stunden. Ich könnte darüber nachdenken, das zu tun.

user202729
quelle
(P / S: Nicht-Code-Einreichungen sind erlaubt. Wenn Sie meiner Punktzahl nicht vertrauen, implementieren Sie sie einfach selbst und sehen Sie)
user202729
Aber wenn Sie wirklich möchten, dass es funktioniert, ohne es aus bestimmten Gründen erneut zu implementieren, probieren Sie es online aus !
user202729
Warten Sie eine Minute, warum kann ich mein Programm nicht einfach einen Entscheidungsbaum ausgeben lassen und ihn bewerten: | es wäre viel schneller ...
user202729