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
是如何分解pick
和pick2
之间的结构相似性的。
高尔夫版本的驱动程序:
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:倒计时数字游戏的主要内容,如果未能解决你的问题,请参考以下文章