如何优化 Haskell 代码以通过 HackerRanks 超时测试用例(不是为了任何正在进行的比赛,只是我在练习)
Posted
技术标签:
【中文标题】如何优化 Haskell 代码以通过 HackerRanks 超时测试用例(不是为了任何正在进行的比赛,只是我在练习)【英文标题】:How to optimise Haskell code to pass HackerRanks timed out test cases (Not for any ongoing competition, just me practicing) 【发布时间】:2021-11-26 14:45:48 【问题描述】:我已经学习 Haskell 大约 4 个月了,我不得不说,学习曲线肯定很难(也很可怕:p)。
在解决了大约 15 个简单的问题后,今天我转到 HackerRank https://www.hackerrank.com/challenges/climbing-the-leaderboard/problem 上的第一个中等难度问题。
这是 10 个测试用例,我能够通过其中的 6 个,但其余的因超时而失败,现在有趣的是,我已经可以看到一些具有性能提升潜力的部分,例如,我是使用nub
从[Int]
中删除重复项,但我仍然无法为算法性能建立心智模型,主要原因是不确定Haskell 编译器会改变我的代码以及懒惰如何在这里发挥作用。
import Data.List (nub)
getInputs :: [String] -> [String]
getInputs (_:r:_:p:[]) = [r, p]
findRating :: Int -> Int -> [Int] -> Int
findRating step _ [] = step
findRating step point (x:xs) = if point >= x then step else findRating (step + 1) point xs
solution :: [[Int]] -> [Int]
solution [rankings, points] = map (\p -> findRating 1 p duplicateRemovedRatings) points
where duplicateRemovedRatings = nub rankings
main :: IO ()
main = interact (unlines . map show . solution . map (map read . words) . getInputs . lines)
GHCI 中的测试用例
:l "solution"
let i = "7\n100 100 50 40 40 20 10\n4\n5 25 50 120"
solution i // "6\n4\n2\n1\n"
我的具体问题:
duplicateRemovedRankings
变量会计算一次,还是在 map 函数调用的每次迭代中计算一次。
就像在命令式编程语言中一样,我可以使用某种打印机制来验证上述问题,是否有一些等效的方法可以用 Haskell 做同样的事情。
根据我目前的理解,这个算法的复杂度是,我知道nub
是O(n^2)
findRating
是 O(n)
getInputs
是 O(1)
solution
是 O(n^2)
我如何对此进行推理并建立一个绩效心理模型。
如果这违反了社区准则,请发表评论,我将删除它。谢谢你的帮助:)
【问题讨论】:
回答简单问题:一次,然后Debug.Trace.trace
。
nub
太可怕了,只在你事先知道非常小的列表上使用它。
使用 Data.IntSet
和 Data.Map.Strict
来自 containers
包(包含在 ghc 中),您的时间复杂度下降到 min(n * log n, m)
其中 n
是 ranked
长度和 @987654344 @ 是 player
长度。
@user1984 你的意思是 min(n * log n, m * log n) 吗?我们得到 O(n+m) 的列表,这可能更好。我在评论below 中描述了它。 (除非我遗漏了什么)
【参考方案1】:
首先,回答您的问题:
-
是的,
duplicateRemovedRankings
只计算一次。无需重复计算。
要调试跟踪,您可以使用trace
及其朋友(有关示例和说明,请参阅文档)。是的,它甚至可以在纯的非 IO 代码中使用。但显然,不要将其用于“正常”输出。
是的,您对复杂性的理解是正确的。
现在,如何通过 HackerRank 的棘手测试。
首先,是的,你说得对,nub
是 O(N^2)。但是,在这种特殊情况下,您不必满足于此。您可以使用排名预先排序的事实来获得nub
的线性版本。您所要做的就是在元素等于下一个元素时跳过它们:
betterNub (x:y:rest)
| x == y = betterNub (y:rest)
| otherwise = x : betterNub (y:rest)
betterNub xs = xs
对于betterNub
本身,这给了您 O(N),但对于 HackerRank 来说仍然不够好,因为整体解决方案仍然是 O(N*M) - 对于您正在迭代所有排名的每个游戏。没有bueno。
但是在这里,您可以通过观察排名是排序来获得另一个改进,并且在排序列表中搜索不必是线性的。您可以改用二分搜索!
要做到这一点,您必须让自己获得恒定时间索引,这可以通过使用 Array
而不是列表来实现。
这是我的实现(请不要粗暴判断;我意识到我可能过度设计了极端情况,但是嘿,它有效!):
import Data.Array (listArray, bounds, (!))
findIndex arr p
| arr!end' > p = end' + 1
| otherwise = go start' end'
where
(start', end') = bounds arr
go start end =
let mid = (start + end) `div` 2
midValue = arr ! mid
in
if midValue == p then mid
else if mid == start then (if midValue < p then start else end)
else if midValue < p then go start mid
else go mid end
solution :: [[Int]] -> [Int]
solution [rankings, points] = map (\p -> findIndex duplicateRemovedRatings p + 1) points
where duplicateRemovedRatings = toArr $ betterNub rankings
toArr l = listArray (0, (length l - 1)) l
有了这个,你得到 O(log N) 的搜索本身,使整体解决方案 O(M * log N)。这对于 HackerRank 来说似乎已经足够了。
(请注意,我在findIndex
的结果中加了 1 - 这是因为练习需要基于 1 的索引)
【讨论】:
为什么会这样?描述指出“玩家的分数,player
,按升序顺序排列。” (强调我的)。我们只需要在排行榜排名列表ranked
上向前 将其反转,同时将其转换为 RLE 格式,直到我们获得第一场比赛的得分;然后随着我们玩得更多,我们通过弹出 head 元素同时更新当前的 head 条目来将它 back。 reverse&rle
是 O(n),toArray
也是。弹出的总成本为 O(m+n)。我错过了什么吗?
哦,我完全错过了球员也被排序的事实。哦,好吧......不过,即使 O(M * log N) 也通过了所有测试?
betterNub = map head . group
是的,但我想展示这个想法以及它是如何工作的。【参考方案2】:
我相信 Fyodor 的回答非常适合您的前两个半问题。对于后半部分,“我怎样才能为性能建立一个心智模型?”,我可以说 SPJ 绝对是一位以聪明但无知的读者可以理解的方式撰写高技术论文的大师。实现书Implementing lazy functional languages on stock hardware 非常好,可以作为心智执行模型的基础。还有 Okasaki 的论文Purely functional data structures,它讨论了一种互补且显着更高级别的方法来进行渐近复杂性分析。 (实际上,我读过他的书,其中显然包含了一些额外的内容,因此在自行决定此建议时请记住这一点。)
请不要被它们的长度吓倒。我个人发现它们实际上非常有趣。他们涵盖的主题很大,不能压缩为简短/快速的答案。
【讨论】:
赞成只是因为我的回答很棒。我超级自负:-)以上是关于如何优化 Haskell 代码以通过 HackerRanks 超时测试用例(不是为了任何正在进行的比赛,只是我在练习)的主要内容,如果未能解决你的问题,请参考以下文章