Collat​​z风格的打蛋


11

受到伟大的API复活节彩蛋狩猎的启发

摘要

您的任务是使用尽可能少的步骤在“ Collat​​z空间”(稍后说明)中搜索预定整数。

介绍

这项挑战基于著名的Collat​​z猜想,希望这里至少每个人都听说过。这是“ 打印超级Collat​​z编号”的摘要。

在Collat​​z序列(也称为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

的在Collat​​z距离C(m,n)的两个数字之间mn,对于这一挑战的目的,是在两个数字之间的距离在Collat​​z图表(贷记@tsh告诉我关于这个概念),其被定义为如下:(使用2113作为例子):

写下了在Collat​​z序列m(在这种情况下,21):

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

写下了在Collat​​z序列n(在这种情况下,13):

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

现在计算仅在一个序列中出现多少个数字。定义为m和之间的Collat​​z距离n。在这种情况下8,即

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

所以我们之间在Collat​​z距离2113作为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](换句话说,介于p和之间的整数q,包括两端)。

鸡蛋的位置在整个游戏过程中保持不变。我们将此坐标表示为r

现在,您可以将初始猜测设为0,它将由控制器记录下来。这是您的第0轮。如果您很幸运,您一开始就把它弄对了(即0 = r),那么游戏结束,您的分数为0(分数越低越好)。否则,您进入第一个回合,然后重新猜测为1,直到猜对为止,即n = r,您的得分将为n

对于0号之后的每个回合,控制器都会为您提供以下反馈之一,以便您可以根据给定的信息做出更好的猜测。假设您当前处于n第30轮,因此您的猜测为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,响应: “终结者”
  • a 2 = 13,响应:“更远”
  • a 3 = 4,响应:“更远”
  • a 4 = 3,响应:“ Closer”
  • 一个5 = 5,响应: “相同”
  • a 6 = 7,响应:“对”

得分:6

挑战

设计确定性策略,p=51, q=562以最佳分数玩游戏。

答案应详细描述算法。您可以附加任何有助于阐明算法的代码。这不是代码高尔夫,所以鼓励您编写清晰的代码。

答案应该包括在的所有可能情况下可能达到的最差得分r,以及最差得分最低的那个得分。在平局的情况下,对于所有可能的rs(也应包括在答案中)具有更好平均分数的算法获胜。没有更多的决胜局,最终我们可能会有多个赢家。

眼镜

  • 重申r一下,就在区间内[51,562]
  • 默认漏洞适用。

赏金(在发布第一个答案后添加)

我个人可以悬赏一个答案,其中所有的猜测都在该范围内,[51,562]而最差得分仍然较低。


你有控制器吗?
user202729'4

没有一个像原始问题中的那个那样。

1
C(m,n)是Collat​​z图上m,n的距离。
tsh

我自己想出了这个概念,却不了解Collat​​z图。谢谢你告诉我。我将把信息包括在问题中。

Answers:


5

鲁比(196)

我最初以为这很难。我不得不处理许多晦涩难懂的案例,最后得到了很多丑陋的代码。但是真的很有趣!:)

战略

每个Collat​​z序列都以2的幂序列结束(例如:[16、8、4、2、1])。一旦遇到2的幂,我们将除以2直到达到1。让我们在最接近的pow2序列中调用2的第一个(因为这也是使用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距离) 。
  6. 如果新发现的号码是正确的号码,请停止。
  7. 使用新发现的号码,返回到步骤5。

结果

在普通PC上为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}"

令人印象深刻。有一个小问题:我认为其中一项评论不应说“完美的正方形”。

1
@WeijunZhou你是正确的。固定!
克里斯蒂安·卢帕斯库

你或许应该包括所有的情况最糟糕的成绩,这是196
魏郡周

3

最差得分:11,总得分:3986

所有的猜测都在范围之内[51,562]

我的算法:

  1. 第一次猜测512,并保持一组可能的结果vals,最初,该集合包含range中的所有数字[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'4

等一下,为什么我不能让我的程序输出决策树并给它评分:| 它会更快...
user202729'4
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.