Haskell如何获取一个筛选过的整数列表?

3

场景: 如果有一个整数数组,我想得到一个整数数组作为返回值,使它们的总和不超过10。

我是Haskell的初学者,尝试了以下代码。如果有人能纠正一下,将不胜感激。

numbers :: [Int]
numbers = [1,2,3,4,5,6,7,8,9,10, 11, 12]

getUpTo :: [Int] -> Int -> [Int]
getUpTo (x:xs) max =
    if max <= 10
    then
            max = max + x
            getUpTo xs max
    else
            x

输入

getUpTo numbers 0

输出期望结果

[1,2,3,4]

1
请指定“整数数组”。连续的?允许空洞吗?唯一解决方案?最佳解决方案(关于“最佳”意味着什么)? - phimuemue
“getUpTo numbers” 中的 [4,6] 是一个可接受的答案吗?为什么或为什么不是? - dave4420
抱歉,我不确定你的问题是什么 :( - Randi Ratnayake
1
@Randy:你是在解决背包问题,还是函数只需要计算满足约束条件的第一个解(无论它是什么)? - Riccardo T.
1
我认为你的意思是“列表”,而不是“数组”。在Haskell中,它们是完全不同的,而且通常不使用数组。 - Paul Johnson
显示剩余2条评论
4个回答

4

注意:这不是背包问题的解决方案 :)

我想出了一个非常快速的解决方案,如下所示。当然,解决完整的背包问题可能更困难,但如果您只需要快速解决方案,这应该有效:

import Data.List (sort)

getUpTo :: Int -> [Int] -> [Int]
getUpTo max xs = go (sort xs) 0 []
    where
        go [] sum acc         = acc
        go (x:xs) sum acc
            | x + sum <= max  = go xs (x + sum) (x:acc)
            | otherwise       = acc

在进行任何其他操作之前,通过对数组进行排序,我可以逐个从顶部取出项目,直到超过最大值为止;然后返回到那一点构建的列表。
编辑:作为附注,我交换了前两个参数的顺序,因为这样对于部分应用程序应该更有用。

正是我想要的。我知道这不是背包问题的解决方案,这只是我为了理解问题并学习如何解决它而进行的小测试。非常感谢! - Randi Ratnayake

4

出于教育目的(并且因为我想解释一些东西:-),这里有一个使用更多标准函数的不同版本。由于它计算了许多总和,而没有保持一个运行总数,所以速度较慢。另一方面,我认为它很好地表达了如何分解问题。

getUpTo :: [Int] -> [Int]
getUpTo = last . filter (\xs -> sum xs <= 10) . Data.List.inits

我将解决方案编写为函数“pipeline”;如果您将getUpTo应用于数字列表,则首先对列表应用Data.List.inits,然后对结果应用filter (\xs -> sum xs <= 10),最后对该结果的最后一个应用last
现在,让我们看看这三个函数各自的作用。首先,Data.List.inits按长度递增的顺序返回列表的初始段。例如,Data.List.inits [2,3,4,5,6]返回[[],[2],[2,3],[2,3,4],[2,3,4,5],[2,3,4,5,6]]。正如您所见,这是一个整数列表的列表。
接下来,filter (\xs -> sum xs <= 10)按顺序遍历这些整数列表,如果它们的总和小于10,则保留它们,否则丢弃它们。 filter的第一个参数是谓词,给定一个列表xs,如果xs的总和小于10,则返回True。这可能有点令人困惑,因此我认为需要一个带有更简单谓词的示例。例如,filter even [1,2,3,4,5,6,7]返回[2,4,6],因为这些是原始列表中的偶数值。在前面的示例中,列表[][2][2,3][2,3,4]的总和都小于10,但[2,3,4,5][2,3,4,5,6]不是,因此应用于[2,3,4,5,6]filter (\xs -> sum xs <= 10) . Data.List.inits的结果是[[],[2],[2,3],[2,3,4]],同样是一个整数列表的列表。
最后一步最容易:我们只需返回整数列表的列表的最后一个元素。从原则上讲,这是不安全的,因为空列表的最后一个元素应该是什么?在我们的情况下,我们可以继续进行,因为inits始终首先返回空列表[],其总和为0,小于10,因此我们正在取最后一个元素的列表中至少有一个元素。我们将last应用于包含原始列表的初始段的列表,这些初始段的总和小于10且按长度排序。换句话说:我们返回总和小于10的最长初始段-这就是您想要的!
如果您的数字列表中有负数,则按此方式操作可能会返回您不期望的内容:getUpTo [10,4,-5,20]返回[10,4,-5],因为这是[10,4,-5,20]的最长初始段,其总和低于10;即使[10,4]超过了10。如果这不是您想要的行为,并且期望[10],则必须使用takeWhile替换filter-这实际上会在遇到谓词返回False的第一个元素时停止过滤。例如。takeWhile [2,4,1,3,6,8,5,7]计算结果为[2,4]。因此,在我们的情况下,使用takeWhile会在总和超过10的那一刻停止,而不会尝试更长的段。
通过将getUpTo编写为函数组合,可以轻松更改算法的各个部分:如果要获取总和恰好为10的最长初始段,则可以使用last.filter(\xs -> sum xs == 10).Data.List.inits。或者如果您想查看尾部段,请使用head . filter (\xs -> sum xs <= 10) . Data.List.tails;或者考虑所有可能的子列表(即低效的背包解决方案!):last . filter(\xs -> sum xs <= 10) . Data.List.sortBy(\xs ys -> length xscomparelength ys) . Control.Monad.filterM (const [False,True]) -但我不打算在这里解释那个。

1
让这个过程更有效率的简单方法是使用 scanl1 (+) 获取部分和,然后将其与原始列表一起使用 zip,再使用 takeWhilegetUpTo xs = map fst . takeWhile ((<= 10) . snd) . zip xs $ scanl1 (+) xs - hammar
@hammar:我曾考虑过解释类似的东西(但笨拙地压缩了scanl (+) 0 xsinits xs),但我想保持基本。相反,我选择展示我最喜欢的Haskell魔法(使用filterM生成所有子列表)。嗯,我的版本或多或少是无点风格的 :-) - yatima2975

3

虽然有一种快速的解决方案,但我认为展示如何最小化修改你的代码以使其按照你的期望工作也是很有教益的。

numbers :: [Int]
numbers = [1,2,3,4,5,6,7,8,9,10, 11, 12]

getUpTo :: [Int] -> Int -> [Int]
getUpTo (x:xs) max =
    if max < 10 -- (<), not (<=)
    then
            -- return a list that still contains x;
            -- can't reassign to max, but can send a
            -- different value on to the next
            -- iteration of getUpTo
            x : getUpTo xs (max + x)
    else
            [] -- don't want to return any more values here

嗨,这返回一个空列表作为结果。不太确定为什么 :( - Randi Ratnayake
@Randy 你是否使用 getUpTo numbers 0,就像你的问题所述,作为输入?;-) - Daniel Wagner

0

我对Haskell还比较陌生。几个小时前我才开始接触它,因此每个问题都是一种挑战,帮助我摆脱命令式思维方式,也是练习递归思维的机会 :)

我认真思考了这个问题,并想出了这个或许有点天真的解决方案:

upToBound :: (Integral a) => [a] -> a -> [a]
upToBound (x:xs) bound = 
        let
                summation _ []  = []
                summation n (m:ms)  
                        | n + m <= bound = m:summation (n + m) ms 
                        | otherwise = []
        in
                summation 0 (x:xs)                

我知道已经有更好的答案了,但我只是为了好玩而这样做。

我有一种印象,我改变了原始调用的签名,因为我认为在外部函数调用中提供初始零是毫无意义的,因为我只能假设它最初只能为零。因此,在我的实现中,我将种子隐藏在调用者之前,并提供了最大边界,这更可能会发生变化。

upToBound [1,2,3,4,5,6,7,8,9,0] 10

输出结果为:[1,2,3,4]


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接