我想找到换币的所有组合。 1,2,5,10,20,50,100和200.(1分,2分..) 如果硬币超过500(5欧元),它应该给-1.My代码与那些测试用例完美配合:numOfSplits 10(11)numOfSplits 20(41)numOfSplits 100(4563)。当我尝试使用numOfSplits 200或500的测试用例时,它会产生C堆栈溢出错误。我怎样才能使我的代码更好?
numOfSplits :: Integer -> Integer
numOfSplits a
| (abs a) > 500 = -1
| (abs a) == 0 = 0
| otherwise = intzahler (makeChange [200,100,50,20,10,5,2,1] (abs a) 200)
intzahler :: [[Integer]] -> Integer
intzahler array
| array == [] = 0
| otherwise = 1 + intzahler (tail array)
makeChange :: [Integer] -> Integer -> Integer -> [[Integer]]
makeChange coins amount maxCoins
| amount < 0 = []
| amount == 0 = [[]]
| null coins = []
| amount `div` maximum coins > maxCoins = [] -- optimisation
| amount > 0 =
do x <- coins
xs <- makeChange (filter (<= x) coins)
(amount - x)
(maxCoins - 1)
guard (genericLength (x:xs) <= maxCoins)
return (x:xs)
我将代码更改为此代码,我不再出现堆栈溢出错误,但现在我的代码工作得太慢了。例如:对于numOfSplits 500,它超过30分钟,我怎样才能更快地完成?
numOfSplits :: Integer -> Integer
numOfSplits a
| (abs a) > 500 = -1
| (abs a) == 0 = 0
| otherwise = fromIntegral . length $ makeChange [200,100,50,20,10,5,2,1] (abs a)
makeChange :: [Integer] -> Integer -> [[Integer]]
makeChange coins amount
| amount < 0 = []
| amount == 0 = [[]]
| null coins = []
| amount > 0 =
do x <- coins
xs <- makeChange (filter (<= x) coins) (amount - x)
return (x:xs)
答案 0 :(得分:7)
快速解决此问题,从而避免耗尽计算机的资源(如堆栈),需要重复使用先前计算的部分答案。
让我们假设我们想解决一个类似的问题,我们试图找出使用1,2或5美分硬币可以赚15美分的方法。我们将面临两个问题 - 第一个是解决问题正确。第二个是快速解决问题 。
为了正确解决问题,我们需要避免重新计算我们已计算过的硬币组合。例如,我们可以通过以下方式赚取15美分:
以上所有示例都使用相同的硬币组合。他们都使用2个5美分硬币和5个1美分硬币,按照不同的顺序计算。
我们可以通过始终以相同的顺序发行我们的硬币来避免上述问题。这表明我们可以通过多少种方式从硬币列表中进行一定程度的更改。我们可以使用第一种硬币中的一种,或者我们可以承诺再也不使用那种类型的硬币。
waysToMake 0 _ = 1
waysToMake x _ | x < 0 = 0
waysToMake x [] = 0
waysToMake x (c:cs) = waysToMake (x-c) (c:cs) + waysToMake x cs
前面的案例涵盖了边界条件。假设没有有问题的零或负值硬币,1
方式可以0
。有0
种方法可以做出负面(< 0
)更改量。如果你没有可以改变的硬币类型,就没有办法改变。
让我们看看如果我们尝试评估waysToMake 15 [1,2,5]
会发生什么。我们会评估每个waysToMake
每个步骤,以便缩短时间。
waysToMake 15 [1,2,5]
waysToMake 14 [1,2,5] + waysToMake 15 [2,5]
waysToMake 13 [1,2,5] + waysToMake 14 [2,5] + waysToMake 13 [2,5] + waysToMake 15 [5]
waysToMake 12 [1,2,5] + waysToMake 13 [2,5] + waysToMake 12 [2,5] + waysToMake 14 [5]
+ waysToMake 11 [2,5] + waysToMake 13 [5] + waysToMake 10 [5] + waysToMake 15 []
waysToMake 11 [1,2,5] + waysToMake 12 [2,5] + waysToMake 11 [2,5] + waysToMake 13 [5]
+ waysToMake 10 [2,5] + waysToMake 12 [5] + waysToMake 9 [5] + waysToMake 14 []
+ waysToMake 9 [2,5] + waysToMake 11 [5] + waysToMake 8 [5] + waysToMake 13 []
+ waysToMake 5 [5] + waysToMake 10 [] + 0
前三个步骤似乎不太可疑,但我们已经遇到waysToMake 13 [2,5]
两次。在第四步中,我们看到了waysToMake 12 [2, 5]
,waysToMake 11 [2, 5]
,waysToMake 13 [5]
我们之前看过的所有内容。我们可以看到,我们将重复生成的大多数其他表达式,这些表达式本身会生成重复表达式的表达式。啊;我有限的脑力计算机已经抱怨说有太多的工作要做。我们可以寻找一个更好的订单来使用硬币(有一个),但它仍然会重复子问题,这本身就会重复子问题等。
有一种更快的方法可以做到这一点。每一步,我们将使用较少硬币而不使用该硬币的数字加在一起。让我们制作一张表,并在计算时记录每个结果。每一步,我们都需要从表格中更左边的数字(使用第一枚硬币中的一张)和一张桌子下面的数字(不要使用任何第一枚硬币)。我们最终会探索整个表格。我们可以从填充边界条件所覆盖的左边和底边的数字开始。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1
[2,5] 1
[5] 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
现在我们将添加可以从表中已有的数字计算的所有数字。使用5美分硬币需要左边5个点,以及第一个点。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1
[2,5] 1
[5] 1 0 0 0 0 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
使用2美分硬币需要左边的2号体育项目,以及排名第一的点数。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1
[2,5] 1 0 1
[5] 1 0 0 0 0 1 0 0 0 0 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
使用1美分的硬币需要左边的1号位,以及第1点的位置。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1 1
[2,5] 1 0 1 0 1
[5] 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
我们再迈出一步。我们可以看到,在另外13个简单的步骤中,我们将计算顶行中15的数字,我们将完成。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1 1 2
[2,5] 1 0 1 0 1 1 1
[5] 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
这是所有步骤之后的表格。我的脑力计算机在计算waysToMake 15 [1,2,5] = 18
时没有困难。
Coins\Change 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[1,2,5] 1 1 2 2 3 4 5 6 7 8 10 11 13 14 16 18
[2,5] 1 0 1 0 1 1 1 1 1 1 2 1 2 1 2 2
[5] 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
[] 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
如果我们找到了更好的使用硬币的订单(有一个)我们不需要填写所有的表格,但它的工作量大致相同。
我们可以使用array package中Array
的{{1}}在Haskell中制作这样的表格。
使用表格的一般计划是 - 根据我们的函数Data.Array
制作表格。只要waysToMake
递归到自身,请在表格中查找结果。在途中我们有两个问题要处理。
第一个问题是waysToMake
要求数组中的索引是Ix
的实例。我们的硬币列表不能提供良好的数组索引。相反,我们可以用我们跳过的硬币数替换硬币列表。第一行为Array
,第二行为0
,最后一行为硬币列表的长度。
第二个问题是我们希望超越桌子的边缘。我们可以定义一个特殊的查找过程来填充表外的部分0,我们可以更改代码,使它永远不会看到表外,或者我们可以创建一个超大的表的两倍大的表。我将跳过所有这些路线,并检查该值是否属于表格的一部分责任。
1
waysToMake x coins = waysToMake' (x,0)
where
tabled = tableRange ((0,0),(x,length coins)) waysToMake'
waysToMake' (n, s) = waysToMake'' n (drop s coins)
where
waysToMake'' 0 _ = 1
waysToMake'' n _ | n < 0 = 0
waysToMake'' n [] = 0
waysToMake'' n (c:cs) = tabled (n-c, s) + tabled (n, s+1)
创建一个在某些范围内记忆其结果的函数。它构建了一个tableRange
,在这些边界内保存了一个函数的延迟评估结果。它返回的函数检查参数是否在边界范围内,如果有,则从表中查找结果,否则直接询问原始函数。
Array
tableRange :: Ix a => (a, a) -> (a -> b) -> (a -> b)
tableRange bounds f = lookup
where
lookup x = if inRange bounds x then table ! x else f x
table = makeArray bounds f
是一个便利函数,它使包含所提供函数makeArray
的数组应用于提供的f
中的每个索引。
bounds
我们的代码现在几乎可以立即运行,即使对于makeArray :: Ix i => (i, i) -> (i -> e) -> Array i e
makeArray bounds f = listArray bounds . map f $ range bounds
等更大的问题也是如此。
我们可以继续讨论如何制作这个&#34; tabling&#34;或者&#34;备忘录&#34;或者&#34;记忆&#34;或者&#34;动态编程&#34;递归函数的代码通用,但我认为讨论属于一个不同的问题。如果你想了解非常实际使用的固定功能点,这是一个很好的主题。
答案 1 :(得分:0)
您是在编译程序还是仅在其中运行ghci
?你还在什么平台上?
numOfSplits 200
编译时只需要6秒左右。
以下是您在ideaone.com上的代码:
对于输入180,它在不到5秒的时间内运行(这是该站点允许的最长运行时间。)
正如Andrew Lorente所指出的那样,您的intzahler
功能与genericLength
甚至length
相同,尽管在这种情况下它似乎无法实现很大的不同。