การล่าไข่ในสไตล์ Collatz


11

แรงบันดาลใจจากThe Great API Easter Egg Hunt!

สรุป

งานของคุณคือการค้นหาจำนวนเต็มที่กำหนดไว้ล่วงหน้าใน "พื้นที่ Collatz" (จะอธิบายในภายหลัง) โดยใช้ขั้นตอนน้อยที่สุดที่เป็นไปได้

บทนำ

ความท้าทายนี้มีพื้นฐานมาจากการคาดคะเน Collatz ที่มีชื่อเสียงซึ่งหวังว่าทุกคนที่นี่จะได้ยิน นี่คือสรุปที่นำมาจากพิมพ์ตัวเลขซูเปอร์ Collatz

Collatz ลำดับ (ที่เรียกว่าปัญหา 3x + 1) เป็นที่ที่คุณเริ่มต้นด้วยจำนวนเต็มบวกใด ๆ สำหรับตัวอย่างนี้เราจะใช้ 10 และใช้ชุดของขั้นตอนไปนี้:

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

ระยะ Collatz C(m,n)ระหว่างตัวเลขสองตัวmและnเพื่อจุดประสงค์ของการท้าทายนี้คือระยะห่างระหว่างตัวเลขสองตัวในกราฟ Collatz (เครดิตถึง @tsh สำหรับการบอกฉันเกี่ยวกับแนวคิดนี้) ซึ่งกำหนดไว้ดังนี้: (ใช้21และ13เป็นตัวอย่าง ):

เขียนลำดับ Collatz สำหรับm(ในกรณีนี้21):

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

เขียนลำดับ Collatz สำหรับn(ในกรณีนี้13):

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

ตอนนี้นับจำนวนที่ปรากฏในลำดับหนึ่งเท่านั้น นี้จะถูกกำหนดเป็นระยะทางระหว่าง Collatz และm nในกรณีนี้8คือ

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

ดังนั้นเราจึงมีระยะห่างระหว่าง Collatz 21และเป็น13C(21,13)=8

C(m,n) มีคุณสมบัติที่ดีดังต่อไปนี้:

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

หวังว่าคำจำกัดความของC(m,n)ตอนนี้ชัดเจน เริ่มล่าไข่กันในอวกาศ Collatz กัน!

ในช่วงเริ่มต้นของเกมผู้ควบคุมจะกำหนดตำแหน่งของไข่อีสเตอร์ซึ่งแสดงโดยพิกัดหนึ่งมิติ: จำนวนเต็มในช่วงเวลา[p,q](กล่าวอีกนัยหนึ่งคือจำนวนเต็มระหว่างpและqทั้งสองรวมอยู่ด้วย)

ตำแหน่งของไข่จะคงที่ตลอดทั้งเกม rเราจะแสดงพิกัดนี้เป็น

ตอนนี้คุณสามารถทำการเดาเริ่มต้นเป็น0และมันจะถูกบันทึกโดยผู้ควบคุม นี่คือรอบที่ 0 ของคุณ หากคุณโชคดีมากที่ได้อันดับแรก (เช่น0 = r) เกมจะจบลงและคะแนนของคุณคือ0(ยิ่งคะแนนยิ่งต่ำก็ยิ่งดี) มิฉะนั้นคุณป้อนรอบที่ 1 และให้คุณเดาใหม่1นี้ไปจนกว่าคุณจะได้รับมันขวาเช่นn = R, nและคะแนนของคุณจะ

สำหรับแต่ละรอบหลังจากวันที่ 0 ผู้ควบคุมจะให้ข้อเสนอแนะข้อใดข้อหนึ่งต่อไปนี้เพื่อให้คุณสามารถคาดเดาได้ดีขึ้นตามข้อมูลที่ให้ สมมติว่าคุณอยู่ในnรอบ th และด้วยเหตุนี้การเดาของคุณคือn

  • "คุณพบมัน!" ถ้าn = R nซึ่งในกรณีที่เกมจะจบลงและคุณจะได้คะแนน
  • "คุณอยู่ใกล้ :)" ถ้า C (a , n , r) <C (a -1 , r)
  • "คุณกำลังหมุนวนรอบไข่" ถ้า C (a n , r) = C (a -1 , r)
  • "คุณอยู่ห่างออกไป :(" ถ้า C (a n , r)> C (a -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และผู้ที่มีคะแนนต่ำที่สุดจะชนะ ในกรณีที่เสมอกันอัลกอริทึมที่มีคะแนนเฉลี่ยที่ดีกว่าสำหรับrs (ซึ่งควรรวมอยู่ในคำตอบด้วย) ชนะ ไม่มีเบรกเกอร์เสมอกันและเราอาจมีผู้ชนะหลายคนในตอนท้าย

รายละเอียด

รางวัล (เพิ่มหลังจากคำตอบแรกถูกโพสต์)

โดยส่วนตัวฉันอาจเสนอเงินรางวัลให้กับคำตอบซึ่งการเดาทั้งหมดจะเกิดขึ้นภายในระยะ[51,562]ในขณะที่ยังมีคะแนนต่ำที่สุดที่สมเหตุสมผล


คุณมีตัวควบคุมหรือไม่?
user202729

ไม่ใช่หนึ่งเดียวที่เหมือนกับคำถามต้นฉบับ
Weijun Zhou

1
C (m, n) คือระยะห่างของ m, n บนกราฟ Collatz
tsh

ฉันมากับแนวคิดด้วยตนเองและไม่รู้กราฟของโคลลาตซ์ ขอบคุณที่บอกฉันว่า ฉันจะรวมข้อมูลในคำถาม
Weijun Zhou

คำตอบ:


5

ทับทิม, 196

นี่เป็นวิธีที่ยากกว่าที่ฉันคิดไว้ในตอนแรก ฉันต้องจัดการกับกรณีที่คลุมเครือจำนวนมากและจบลงด้วยรหัสที่น่าเกลียดมากมาย แต่มันสนุกมาก! :)

กลยุทธ์

ลำดับ Collatz ทุกครั้งจะจบลงด้วยลำดับของพลัง 2 (เช่น: [16, 8, 4, 2, 1]) ทันทีที่พบพลัง 2 เราจะหารด้วย 2 จนกระทั่งถึง 1 ลองเรียกกำลังแรกของ 2 ในลำดับpow2 ที่ใกล้เคียงที่สุด (เนื่องจากนี่คือกำลังที่ใกล้เคียงที่สุดของ 2 ถึงจำนวนของเราโดยใช้ระยะโคลลัตซ์ ) สำหรับช่วงที่กำหนด (51-562) ตัวเลขpow2 ที่ใกล้เคียงที่สุดที่เป็นไปได้ทั้งหมดคือ: [16, 64, 128, 256, 256, 512, 1024]

เวอร์ชั่นสั้น

อัลกอริทึมดำเนินการ:

  • การค้นหาแบบไบนารีเพื่อหากำลังที่ใกล้เคียงที่สุดของ 2 ถึงจำนวนปัจจุบัน
  • การค้นหาเชิงเส้นเพื่อหาองค์ประกอบก่อนหน้านี้ทั้งหมดในลำดับจนกระทั่งค้นพบหมายเลขเป้าหมาย

รุ่นโดยละเอียด

ให้เกมที่มีหมายเลขเป้าหมายrกลยุทธ์ดังต่อไปนี้:

  1. ใช้การค้นหาแบบไบนารีเพื่อหากำลังของ 2 ที่ใกล้เคียงที่สุดrในไม่กี่ขั้นตอนเท่าที่จะทำได้
  2. หากพลังที่ใกล้เคียงที่สุดของ 2 ที่พบคือทางออกให้หยุด มิฉะนั้นจะดำเนินต่อไปที่ 3
  3. เนื่องจากพลังงานของ 2 ที่พบเป็นพลังงานแรกของ 2 ที่เกิดขึ้นในลำดับหากเป็นไปตามค่าที่ได้มาถึงโดยการดำเนินการ (* 3 + 1) (ถ้ามันมาหลังจากการดำเนินการ / 2 หมายเลขก่อนหน้านี้ก็จะเป็นพลังของ 2) คำนวณจำนวนก่อนหน้าในลำดับโดยทำการย้อนกลับ (-1 และจากนั้น / 3)
  4. หากหมายเลขนั้นเป็นเป้าหมายให้หยุด มิฉะนั้นดำเนินการต่อถึง 5
  5. เมื่อรับหมายเลขปัจจุบันที่ทราบจากลำดับจึงจำเป็นต้องย้อนกลับและค้นหาหมายเลขก่อนหน้าในลำดับ ไม่ทราบว่าหมายเลขปัจจุบันมาถึงโดยการดำเนินการ (/ 2) หรือ (* 3 +1) ดังนั้นอัลกอริทึมจะลองทั้งคู่และดูว่าใครให้ผลตอบแทนที่ใกล้กว่า (ตามระยะโคลลัตซ์) จากเป้าหมาย .
  6. หากหมายเลขที่เพิ่งค้นพบนั้นเป็นหมายเลขที่ถูกต้องให้หยุด
  7. ใช้หมายเลขที่เพิ่งค้นพบใหม่กลับไปที่ขั้นตอนที่ 5

ผลที่ได้

การรันอัลกอริทึมสำหรับตัวเลขทั้งหมดในช่วง 51-562 นั้นใช้เวลาประมาณวินาทีในพีซีปกติและคะแนนรวมคือ 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}"

ประทับใจ มีประเด็นย่อยคือ: ฉันเชื่อว่าหนึ่งในความคิดเห็นที่ไม่ควรพูดว่า "ตารางที่สมบูรณ์แบบ"
Weijun Zhou

1
@ WeijunZhou คุณพูดถูก แก้ไขแล้ว!
Cristian Lupascu

คุณควรรวมคะแนนที่แย่ที่สุดสำหรับทุกกรณีซึ่งก็คือ 196
Weijun Zhou

3

คะแนนที่แย่ที่สุด: 11, คะแนนรวม: 3986

[51,562]คาดเดาทั้งหมดที่อยู่ในช่วง

อัลกอริทึมของฉัน:

  1. ครั้งแรกคิดว่า 512 และบำรุงรักษาชุดของผลที่เป็นไปได้ต้นชุดมีตัวเลขทั้งหมดอยู่ในช่วงvals[51,562]
  2. ในแต่ละขั้นตอนให้ทำดังนี้

    1. หาค่าของการคาดเดาต่อไปguessในช่วง[51,562]ดังกล่าวว่าเมื่อค่าในvals(ไม่รวมguessตัวเอง) จะถูกแบ่งออกเป็น 3 ชุดที่สอดคล้องกับผลลัพธ์ที่เป็นไปCloser, SameและFartherขนาดสูงสุดของผู้ที่ 3 ชุดคือขั้นต่ำ
      หากมีค่าที่เป็นไปได้หลายอย่างที่สามารถguessตอบสนองด้านบนได้ให้เลือกค่าที่น้อยที่สุด
    2. guessเดาค่า
    3. หากคำตอบคือ "ถูกต้อง" เสร็จแล้ว (ออกจากโปรแกรม)
    4. ลบค่าทั้งหมดในชุดvalsเพื่อไม่ให้ผลลัพธ์นั้น

การใช้งานอ้างอิงของฉันที่เขียนใน C ++ และ Bash นั้นใช้เวลาประมาณ 7.6 วินาทีในเครื่องของฉันและให้คะแนน / คะแนนรวมที่แย่ที่สุดตามที่อธิบายไว้ในชื่อเรื่อง

การพยายามคาดเดาค่าที่เป็นไปได้ทั้งหมดจะใช้เวลาประมาณ 1.5 ชั่วโมงในเครื่องของฉัน ฉันอาจพิจารณาทำเช่นนั้น


(P / S: อนุญาตให้ส่งแบบไม่ใช้รหัสได้หากคุณไม่ไว้วางใจคะแนนของฉันเพียงใช้ด้วยตนเองและดู)
user202729

แต่ถ้าคุณจริงๆต้องการที่จะเห็นมันทำงานได้โดยไม่ต้อง reimplementing มันด้วยเหตุผลบางอย่างลองออนไลน์ !
user202729

รอสักครู่เหตุใดฉันจึงไม่สามารถปล่อยให้โปรแกรมเอาท์พุทต้นไม้ตัดสินใจและทำคะแนนได้: | มันจะเร็วขึ้นมาก ...
user202729
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.