Code Golf:倒计时数字游戏

Posted

技术标签:

【中文标题】Code Golf:倒计时数字游戏【英文标题】:Code Golf: Countdown Number Game 【发布时间】:2011-06-02 23:25:59 【问题描述】:

挑战

这里是任务,灵感来自著名的英国电视游戏节目Countdown。即使没有任何游戏知识,挑战也应该很清楚,但请随时要求澄清。

如果您想观看此游戏的实际操作片段,请查看this YouTube clip。它以 1997 年出色的已故理查德·怀特利 (Richard Whitely) 为特色。

给您 6 个数字,从集合 1, 2, 3, 4, 5, 6, 8, 9, 10, 25, 50, 75, 100 中随机选择,以及一个介于 100 之间的随机目标数字和 999. 目的是使用六个给定的数字和四个常见的算术运算(加法、减法、乘法、除法;所有有理数)来生成目标 - 或尽可能接近任一侧。每个数字最多只能使用一次,而每个算术运算符可以使用任意次数(包括零)。请注意,使用多少个数字并不重要。

编写一个函数,接受目标数字和 6 个数字的集合(可以表示为列表/集合/数组/序列)并以任何标准数字符号(例如中缀、前缀、后缀)返回解。该函数必须始终将最接近的结果返回给目标,并且必须在标准 PC 上最多运行 1 分钟。请注意,在存在多个解决方案的情况下,任何一个解决方案都足够了。

例子:

50, 100, 4, 2, 2, 4,目标 203 例如100 * 2 + 2 + (4 / 4) (精确) 例如(100 + 50) * 4 * 2 / (4 + 2) (精确)

25, 4, 9, 2, 3, 10,目标 465 例如(25 + 10 - 4) * (9 * 2 - 3) (精确)

9, 8, 10, 5, 9, 7,目标 241 例如((10 + 9) * 9 * 7) + 8) / 5 (精确)

3, 7, 6, 2, 1, 7,目标 824 例如((7 * 3) - 1) * 6 - 2) * 7 (= 826; 减 2)

规则

除了问题陈述中提到的以外,没有其他限制。您可以用任何标准语言编写函数(不需要标准 I/O)。一如既往的目标是用最少的代码字符解决任务。

这么说,我可能不会简单地接受最短代码的答案。我还将研究代码的优雅性和算法的时间复杂度!

我的解决方案

当我找到空闲时间时,我正在尝试 F# 解决方案 - 有事时会在这里发布!


格式

为了便于比较,请按以下格式发布所有答案:

语言

字符数:???

完全混淆的函数:

(code here)

清除(最好是注释)功能:

(code here)

关于它所采用的算法/聪明快捷方式的任何注释。


【问题讨论】:

如何处理部门?整数除法还是浮点数? 我们曾经用一副纸牌玩这个。为数字发 4 张牌(J=11,Q=12,K=13)。然后为目标处理2个数字:target=10*t1+t2,所以最多可以达到13*11。你几乎总是可以这样做,而对于其他情况,我一直想要一个程序来验证没有解决方案。 @Noldorin 如果输入不能任意增长,那么大 O 表示法时间约束是没有意义的。 我认为可以证明没有O(n+k) 解决方案。我认为也没有多项式时间解决方案。这闻起来就像一个 NP Hard 问题。 我看不出有任何方法可以在多项式时间内准确地做到这一点。想象一下,我给了你一个固定的表达式,比如 1+2*3+4*5+6,你所要做的就是决定括号到哪里最接近目标数字,那仍然是 (n-1)!选择(执行操作的顺序),这不是多项式。原来的问题比这要糟糕得多。 【参考方案1】:

Python

字符数:548 482 425 421 416 413 408

from operator import *
n=len
def C(N,T):
 R=range(1<<n(N));M=[for i in R];p=1
 for i in range(n(N)):M[1<<i][1.*N[i]]="%d"%N[i]
 while p:
  p=0
  for i in R:
   for j in R:
    m=M[i|j];l=n(m)
    if not i&j:m.update((f(x,y),"("+s+o+t+")")for(y,t)in M[j].items()if y for(x,s)in M[i].items() for(o,f)in zip('+-*/',(add,sub,mul,div)))
    p|=l<n(m)
 return min((abs(x-T),e)for t in M for(x,e)in t.items())[1]

你可以这样称呼它:

>>> print C([50, 100, 4, 2, 2, 4], 203)
((((4+2)*(2+100))/4)+50)

在旧 PC 上执行给定示例大约需要半分钟。

这是注释版本:

def countdown(N,T):
  # M is a map: (bitmask of used input numbers -> (expression value -> expression text))                  
  M=[ for i in range(1<<len(N))]

  # initialize M with single-number expressions                                                           
  for i in range(len(N)):
    M[1<<i][1.0*N[i]] = "%d" % N[i]

  # allowed operators                                                                                     
  ops = (("+",lambda x,y:x+y),("-",lambda x,y:x-y),("*",lambda x,y:x*y),("/",lambda x,y:x/y))

  # enumerate all expressions                                                                             
  n=0
  while 1:

    # test to see if we're done (last iteration didn't change anything)                                   
    c=0
    for x in M: c +=len(x)
    if c==n: break
    n=c

    # loop over all values we have so far, indexed by bitmask of used input numbers                       
    for i in range(len(M)):
      for j in range(len(M)):
        if i & j: continue    # skip if both expressions used the same input number                       
        for (x,s) in M[i].items():
          for (y,t) in M[j].items():
            if y: # avoid /0 (and +0,-0,*0 while we're at it)                                             
              for (o,f) in ops:
                M[i|j][f(x,y)]="(%s%s%s)"%(s,o,t)

  # pick best expression                                                                                  
  L=[]
  for t in M:
    for(x,e) in t.items():
      L+=[(abs(x-T),e)]
  L.sort();return L[0][1]

它通过详尽列举所有可能性来发挥作用。有点聪明的是,如果有两个具有相同值的表达式使用相同的输入数字,它会丢弃其中一个。它在考虑新组合方面也很聪明,使用 M 中的索引快速修剪所有共享输入数字的潜在组合。

【讨论】:

@Keith:干得好!我对缩小版本进行了一些修剪,希望你不介意:)(也许我也应该调整扩展版本中测试完成以反映缩小版本的代码) @Keith:太棒了!我还刮掉了一些字符。 有什么理由不对长度进行硬编码? o_o 不错的解决方案。我预计500以下的任何东西都会非常好! :) ..不知道为什么我自己不更新它,但有几个 cmets:缩进中的备用空格和制表符(S,T,TS,TT,TTS,...)。可能以反向抛光 (s+" "+t+o) 输出 - 但如果您将其保留为中缀,您可以只对其进行评估,并且您不必导入运算符(或者如果这也需要,则使用已经计算的数字对其进行评估长)。在其他地方初始化 p ——无论如何,你都会立即将它设置为零。【参考方案2】:

哈斯克尔

字符数:361 350 338 322

完全混淆的函数:

m=map
f=toRational
a%w=m(\(b,v)->(b,a:v))w
p[]=[];p(a:w)=(a,w):a%p w
q[]=[];q(a:w)=[((a,b),v)|(b,v)<-p w]++a%q w
z(o,p)(a,w)(b,v)=[(a`o`b,'(':w++p:v++")")|b/=0]
y=m z(zip[(-),(/),(+),(*)]"-/+*")++m flip(take 2 y)
r w=do((a,b),v)<-q w;o<-y;c<-o a b;c:r(c:v)
c t=snd.minimum.m(\a->(abs(fst a-f t),a)).r.m(\a->(f a,show a))

清除功能:

-- | add an element on to the front of the remainder list
onRemainder :: a -> [(b,[a])] -> [(b,[a])]
a`onRemainder`w = map (\(b,as)->(b,a:as)) w

-- | all ways to pick one item from a list, returns item and remainder of list
pick :: [a] -> [(a,[a])]
pick [] = []
pick (a:as) = (a,as) : a `onRemainder` (pick as)

-- | all ways to pick two items from a list, returns items and remainder of list
pick2 :: [a] -> [((a,a),[a])]
pick2 [] = []
pick2 (a:as) = [((a,b),cs) | (b,cs) <- pick as] ++ a `onRemainder` (pick2 as)    

-- | a value, and how it was computed
type Item = (Rational, String)

-- | a specification of a binary operation
type OpSpec = (Rational -> Rational -> Rational, String)

-- | a binary operation on Items
type Op = Item -> Item -> Maybe Item

-- | turn an OpSpec into a operation
-- applies the operator to the values, and builds up an expression string
-- in this context there is no point to doing +0, -0, *0, or /0
combine :: OpSpec -> Op
combine (op,os) (ar,as) (br,bs)
    | br == 0   = Nothing
    | otherwise = Just (ar`op`br,"("++as++os++bs++")")

-- | the operators we can use
ops :: [Op]
ops = map combine [ ((+),"+"), ((-), "-"), ((*), "*"), ((/), "/") ]
        ++ map (flip . combine) [((-), "-"), ((/), "/")]

-- | recursive reduction of a list of items to a list of all possible values
-- includes values that don't use all the items, includes multiple copies of
-- some results          
reduce :: [Item] -> [Item]
reduce is = do
    ((a,b),js) <- pick2 is
    op <- ops
    c <- maybe [] (:[]) $ op a b
    c : reduce (c : js)

-- | convert a list of real numbers to a list of items
items :: (Real a, Show a) => [a] -> [Item]
items = map (\a -> (toRational a, show a))

-- | return the first reduction of a list of real numbers closest to some target
countDown:: (Real a, Show a) => a -> [a] -> Item
countDown t is = snd $ minimum $ map dist $ reduce $ items is
    where dist is = (abs . subtract t' . fst $ is, is)
          t' = toRational t

关于它所采用的算法/聪明快捷方式的任何注释:

在打高尔夫球的版本中,z 在列表 monad 中返回,而不是像 ops 那样返回 Maybe。 虽然这里的算法是蛮力的,但由于 Haskell 的惰性,它在小的、固定的线性空间中运行。我编写了出色的 @keith-randall 算法,但它几乎在同一时间运行,并在 Haskell 中占用了 1.5G 的内存。 reduce 多次生成一些答案,以便轻松包含具有较少术语的解决方案。 在 Golf'd 版本中,y 部分根据自身定义。 使用Rational 值计算结果。如果使用Double 计算,Golf'd 代码将缩短 17 个字符,速度更快。 注意函数onRemainder 是如何分解pickpick2 之间的结构相似性的。

高尔夫版本的驱动程序:

main = do
    print $ c 203 [50, 100, 4, 2, 2, 4]
    print $ c 465 [25, 4, 9, 2, 3, 10]
    print $ c 241 [9, 8, 10, 5, 9, 7]
    print $ c 824 [3, 7, 6, 2, 1, 7]

运行,计时(每个结果仍然不到一分钟):

[1076] : time ./Countdown
(203 % 1,"(((((2*4)-2)/100)+4)*50)")
(465 % 1,"(((((10-4)*25)+2)*3)+9)")
(241 % 1,"(((((10*9)/5)+8)*9)+7)")
(826 % 1,"(((((3*7)-1)*6)-2)*7)")

real    2m24.213s
user    2m22.063s
sys     0m 0.913s

【讨论】:

【参考方案3】:

Ruby 1.9.2

字符数:404

我现在放弃了,只要有一个准确的答案,它就可以工作。如果没有,则枚举所有可能性需要很长时间。

完全混淆

def b a,o,c,p,r
o+c==2*p ?r<<a :o<p ?b(a+['('],o+1,c,p,r):0;c<o ?b(a+[')'],o,c+1,p,r):0
end
w=a=%w+ - * /
4.timesw=w.product a
b [],0,0,3,g=[]
*n,l=$<.read.split.map(&:to_f)
h=
catch(0)w.product(g).each|c,f|k=f.zip(c.flatten).each|o|o.reverse! if o[0]=='(';n.permutation|m|h[x=eval(d=m.zip(k)*'')]=d;throw 0 if x==l
c=h[k=h.keys.min_by|i|(i-l).abs]
puts c.gsub(/(\d*)\.\d*/,'\1')+"=#k"

解码

Coming soon

测试脚本

#!/usr/bin/env ruby
[
  [[50,100,4,2,2,4],203],
  [[25,4,9,2,3,10],465],
  [[9,8,10,5,9,7],241],
  [[3,7,6,2,1,7],824]
].each do |b|
  start = Time.now
  puts "[#b[0]*', '] #b[1]  gives #`echo "#b[0]*' ' #b[1]" | ruby count-golf.rb`.strip in #Time.now-start"
end

输出

→ ./test.rb
[50, 100, 4, 2, 2, 4] 203  gives 100+(4+(50-(2)/4)*2)=203.0 in 3.968534736
[25, 4, 9, 2, 3, 10] 465  gives 2+(3+(25+(9)*10)*4)=465.0 in 1.430715549
[9, 8, 10, 5, 9, 7] 241  gives 5+(9+(8)+10)*9-(7)=241.0 in 1.20045702
[3, 7, 6, 2, 1, 7] 824  gives 7*(6*(7*(3)-1)-2)=826.0 in 193.040054095

详情

用于生成括号对 (b) 的函数基于此函数:Finding all combinations of well-formed brackets

【讨论】:

【参考方案4】:

Ruby 1.9.2 第二次尝试

字符数:492 440(426)

再次存在不准确答案的问题。这次这很容易足够快,但由于某种原因,它最接近 824 的是 819 而不是 826。

我决定把它放在一个新的答案中,因为它使用了一种与我上次尝试完全不同的方法。

删除输出的总和(因为规范没有要求)是 -14 个字符。

完全混淆

def r d,c;d>4?[0]:(k=c.pop;a=[];r(d+1,c).each|b|a<<[b,k,nil];a<<[nil,k,b];a)end
def f t,n;[0,2].each|a|Array===t[a] ?f(t[a],n): t[a]=n.popend
def d t;Float===t ?t:d(t[0]).send(t[1],d(t[2]))end
def o c;Float===c ?c.round: "(#o c[0]#c[1]#o c[2])"end
w=a=%w+ - * /
4.timesw=w.product a
*n,l=$<.each(' ').map(&:to_f)
h=
w.each|y|r(0,y.flatten).each|t|f t,n.dup;h[d t]=o t
puts h[k=h.keys.min_by|i|(l-i).abs]+"=#k.round"

解码

Coming soon

测试脚本

#!/usr/bin/env ruby
[
  [[50,100,4,2,2,4],203],
  [[25,4,9,2,3,10],465],
  [[9,8,10,5,9,7],241],
  [[3,7,6,2,1,7],824]
].each do |b|
  start = Time.now
  puts "[#b[0]*', '] #b[1]  gives #`echo "#b[0]*' ' #b[1]" | ruby count-golf.rb`.strip in #Time.now-start"
end

输出

→ ./test.rb
[50, 100, 4, 2, 2, 4] 203  gives ((4-((2-(2*4))/100))*50)=203 in 1.089726252
[25, 4, 9, 2, 3, 10] 465  gives ((10*(((3+2)*9)+4))-25)=465 in 1.039455671
[9, 8, 10, 5, 9, 7] 241  gives (7+(((9/(5/10))+8)*9))=241 in 1.045774539
[3, 7, 6, 2, 1, 7] 824  gives ((((7-(1/2))*6)*7)*3)=819 in 1.012330419

详情

这构造了一组三叉树,表示 5 个运算符的所有可能组合。然后它遍历并将输入数字的所有排列插入这些树的叶子中。最后,它简单地遍历这些可能的方程,将它们存储到一个哈希中,并将结果作为索引。然后很容易从哈希中选择最接近所需答案的值并显示它。

【讨论】:

您应该编辑您的其他帖子,而不是发布新帖子。

以上是关于Code Golf:倒计时数字游戏的主要内容,如果未能解决你的问题,请参考以下文章

unity游戏倒计时结束游戏失败

获取验证码倒计时

倒计时60s短信 [记录]

如何实现游戏中的在线计时器和离线计时器

一个简单的游戏倒计时

原生游戏中的倒计时效果