Collat​​zスタイルの卵狩り


11

グレートAPIイースターエッグハントに触発されました

概要

あなたのタスクは、可能な限り少ないステップを使用して、「コラッツ空間」(後述)で所定の整数を検索することです。

前書き

この挑戦は有名なCollat​​zの予想に基づいています。スーパーコラッツナンバーの印刷から抜粋しました

このCollat​​zシーケンスあなたは、任意の正の整数で始まるこの例のために、我々は10を使用し、それへのステップのセットを適用する場所(も3X + 1問題と呼ばれる)は、次のとおりです。

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

このCollat​​z距離C(m,n)の2つの数の間のmn、この課題の目的のためには、2つの数字の間の距離であるこのCollat​​zグラフ(使用して次のように定義され、(この概念についての私に告げるため@tshにクレジット)2113の例として):

m(この場合、21)のCollat​​zシーケンスを書き留めます。

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

n(この場合、13)のCollat​​zシーケンスを書き留めます。

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

次に、シーケンスの1つにのみ表示される数字の数をカウントします。これはmとの間のコラッツ距離として定義されnます。この場合、8つまり、

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

したがって、21との間のCollat​​z距離があり13ますC(21,13)=8

C(m,n) 次の素晴らしいプロパティがあります:

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

うまくいけば、の定義がC(m,n)明確になりました。Collat​​zスペースで卵狩りを始めましょう!

間隔の整数:ゲームの開始時に、コントローラは、一次元で表現されるイースターエッグの位置座標を決定する[p,q](言い換えると、の間の整数pq、両端を含みます)。

卵の位置はゲーム全体を通して一定のままです。この座標をとして示しますr

これで、初期推測を0にすることができ、コントローラーによって記録されます。これはあなたの0ラウンドです。幸運にも最初の場所(つまり0 = r)で正解した場合、ゲームは終了し、スコアは0(スコアが低いほど良い)です。それ以外の場合は、第1ラウンドに入り、新しい推測値を1にします。これは、正解、つまりn = r になるまで続き、スコアはになりますn

0回目以降の各ラウンドでは、コントローラーは以下のフィードバックのいずれかを提供するため、与えられた情報に基づいてより良い推測を行うことができます。あなたが現在nthラウンドにいると仮定してみましょう。したがって、推測はn

  • "あなたは見つけた!" もしN = R、場合のゲームが終了し、あなたは得点n
  • 「あなたは近い:)」C(a n、r)<C(a n-1、r)の場合
  • C(a n、r)= C(a n-1、r)の場合、「あなたは卵の周りを旋回しています」
  • 「あなたはもっと遠い:(」C(a n、r)> C(a n-1、r)の場合

いくつかのバイトを節約するために、上記の順序で、応答を「右」、「近い」、「同じ」、「より太い」と呼びます。

これがのサンプルゲームp=1,q=15です。

  • 0 = 10
  • 1 = 11、応答: "より近いです"
  • 2 = 13、応答: "遠く"
  • 3 = 4、応答: "遠く"
  • 4 = 3、応答: "より近いです"
  • 5 = 5、レスポンス: "同じ"
  • 6 = 7、レスポンス: "右"

スコア:6

チャレンジ

最高のスコアでゲームをプレイするための決定的戦略を設計しp=51, q=562ます。

回答では、アルゴリズムを詳細に説明する必要があります。アルゴリズムの解明に役立つコードを添付できます。これはcodegolfではないので、読みやすいコードを書くことをお勧めします。

回答には、考えられるすべてのケースで達成できる最悪のスコアを含める必要があり、最悪のスコアの勝者がr勝ちます。tieの場合、可能なすべてrのs(これも回答に含める必要があります)の平均スコアが高いアルゴリズムが勝ちます。タイブレーカーはこれ以上なく、最終的には複数の勝者になる可能性があります。

スペック

バウンティ(最初の回答が投稿された後に追加されます)

個人的には、範囲内ですべての推測が行われ、[51,562]それでもかなり低い最低スコアを保持している答えに賞金を提供する場合があります。


コントローラーはありますか?
user202729

元の質問のようなものではありません。
ウェイジュン周

1
C(m、n)は、コラッツグラフ上のm、nの距離です。
tsh

私は自分でコンセプトを思いつきましたが、Collat​​zグラフを知りませんでした。教えてくれてありがとう。質問に情報を含めます。
ウェイジュン周

回答:


5

ルビー、196

これは私が当初考えていたよりもずっと困難でした。私は多くのあいまいなケースを処理しなければならず、多くのいコードになりました。でもとても楽しかったです!:)

戦略

すべてのCollat​​zシーケンスは、2の累乗のシーケンスで終了します(例:[16、8、4、2、1])。2のべき乗に達するとすぐに、1に達するまで2で除算します。シーケンスに最も近い 2のべき乗をpow2と呼びましょう(これはCollat​​z Distanceを使用した数値に最も近い2のべき乗でもあるため)。指定された範囲(51〜562)の場合、可能な最も近いすべてのpow2番号は次のとおりです。[ 16、64、128、256、512、1024 ]

短縮版

アルゴリズムは以下を実行します。

  • 現在の数に最も近い2のべき乗を計算するためのバイナリ検索
  • ターゲット番号が見つかるまで、シーケンス内のすべての以前の要素を把握する線形検索。

詳細バージョン

目標数のゲームを考えるrと、戦略は次のとおりです。

  1. バイナリ検索を使用して、rできるだけ少ないステップで最も近い2のべき乗を計算します。
  2. 見つかった最も近い2のべき乗が解である場合、停止します。それ以外の場合は3に進みます。
  3. 見つかった2のべき乗はシーケンスで最初に発生する2のべき乗であるため、次の場合、(* 3 + 1)演算を行うことでその値に達しました。(/ 2演算の後に来た場合、前の数値も2のべき乗になります)。逆の操作を行うことで、シーケンス内の前の数値を計算します(-1、次に/ 3)
  4. その番号がターゲットの場合、停止します。それ以外の場合は5に進みます。
  5. シーケンスから既知の現在の番号を考えると、戻ってシーケンス内の前の番号を発見する必要があります。現在の番号が(/ 2)または(* 3 +1)操作で到達したかどうかは不明であるため、アルゴリズムは両方を試行し、どちらがターゲットから(Collat​​z Distanceとして)近い番号を生成するかを確認します。
  6. 新しく発見された番号が正しい番号である場合、停止します。
  7. 新しく発見された番号を使用して、手順5に戻ります。

結果

51〜562の範囲のすべての数値に対してアルゴリズムを実行すると、通常のPCでは約1秒かかり、合計スコアは38665です。

コード

オンラインでお試しください!

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

印象的。ちょっとした点があります。コメントの1つが「完全な正方形」と言ってはいけないと思います。
Weijun周

1
@WeijunZhouあなたは正しいです。修繕!
クリスチャンルパスク

あなたはおそらく196あるすべてのケースのための最悪のスコア、含まれるべきである
Weijun周

3

最悪のスコア:11、合計スコア:3986

すべての推測は範囲内[51,562]です。

私のアルゴリズム:

  1. 512を最初に推測し、可能な結果のセットを維持valsします[51,562]。最初は、セットに範囲内のすべての数値が含まれます。
  2. 各ステップで、次を実行します。

    1. 次の推測値を見つけるguess範囲内[51,562]の値ように、vals(除くguess自体が)可能な結果に対応する3セットに分割されCloserSameおよびFarther、それらの3セットの最大サイズは最小です。上記
      guess満たす複数の可能な値がある場合、最小のものを選択します。
    2. 値を推測しますguess
    3. 答えが「正しい」の場合、完了です(プログラムを終了します)。
    4. valsその結果が得られないように、セット内のすべての値を削除します。

C ++およびBashで記述された参照実装は、マシン上で約7.6秒で実行され、タイトルで説明されているように最悪のスコア/合計スコアを示します。

すべての可能な最初の推測値を試すには、私のマシンで約1.5時間かかります。私はそれを検討するかもしれません。


(P / S:非コードの提出が許可されています。私のスコアを信用しない場合は、自分でスコアを実装して参照してください)
user202729

ただし、何らかの理由で再実装せずに機能することを本当に見たい場合は、オンライン試してみてください
user202729

なぜプログラムに決定木を出力させてそれを採点できないのか、ちょっと待ってください:| それははるかに高速になります
...-user202729
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.