Haskell-将列表分成两个具有最接近总和的子列表

时间:2018-11-07 14:13:10

标签: list haskell optimization

我是Haskell的初学者,试图通过解决一些在线测验/问题集来学习更多有关该语言的信息。

这个问题/问题相当冗长,但其中一部分需要代码才能找到将给定列表分为两个(几乎)相等(按总数)子列表的数字。

给出[1..10]

71+2+..7 = 28起,答案应为8+9+10 = 27

这是我实现它的方式

-- partitions list by y
partishner :: (Floating a) => Int -> [a] -> [[[a]]]
partishner 0 xs = [[xs],[]]
partishner y xs = [take y xs : [drop y xs]] ++ partishner (y - 1) xs


-- finds the equal sum
findTheEquilizer :: (Ord a, Floating a) => [a] -> [[a]]
findTheEquilizer xs = fst $ minimumBy (comparing snd) zipParty
  where party = (tail . init) (partishner (length xs) xs) -- removes [xs,[]] types
        afterParty = (map (\[x, y] -> (x - y) ** 2) . init . map (map sum)) party
        zipParty = zip party afterParty -- zips partitions and squared diff betn their sums

给出(last . head) (findTheEquilizer [1..10]) 输出:7


对于50k附近的数字,效果很好

λ> (last . head) (findTheEquilizer [1..10000])                                                   
   7071.0 

当我放入列表中包含超过70k个元素的列表时,麻烦就开始了。它需要永远的计算。


那么,我必须在代码中进行哪些更改以使其更好地运行,还是必须更改整个方法?我猜是晚了,但是我不确定该怎么做。

5 个答案:

答案 0 :(得分:2)

在我看来,实现非常混乱。例如,partishner似乎构造了a列表列表的列表,其中,据我所知,其中的外部列表包含具有每个两个元素的列表:“左侧”元素的列表,以及位于“右侧”的元素列表。结果,这需要 O(n 2 来构造列表。

通过使用超过2个元组的列表,这也是非常“不安全的”,因为列表可以(虽然在这里可能是不可能的)不包含元素,一个元素或两个以上元素。如果您在其中一个功能中犯了错误,将很难发现该错误。

在我看来,实现“扫描算法”可能会更容易:我们首先计算列表中所有元素的总和。如果我们决定在该特定点进行拆分,则这是“右侧”的值,接下来我们开始从左向右移动,每次从右侧的总和中减去该元素,然后将其添加到左侧的总和中。我们每次都可以评估分数的差异,例如:

import Data.List(unfoldr)

sweep :: Num a => [a] -> [(Int, a, [a])]
sweep lst = x0 : unfoldr f x0
    where x0 = (0, sum lst, lst)
          f (_, _, []) = Nothing
          f (i, r, (x: xs)) = Just (l, l)
              where l = (i+1, r-2*x, xs)

例如:

Prelude Data.List> sweep [1,4,2,5]
[(0,12,[1,4,2,5]),(1,10,[4,2,5]),(2,2,[2,5]),(3,-2,[5]),(4,-12,[])]

因此,如果我们选择在第一个分割点处分割(在第一个元素之前),那么右边的总和要比左边的总和高12,如果我们在第一个元素之后进行分割,则总和右侧(11)比左侧(10)高1

然后我们可以使用minimumBy :: (a -> a -> Ordering) -> [a] -> a获得这些分割的最小值:

import Data.List(minimumBy)
import Data.Ord(comparing)

findTheEquilizer :: (Ord a, Num a) => [a] -> ([a], [a])
findTheEquilizer lst = (take idx lst, tl)
    where (idx, _, tl) = minimumBy (comparing (abs . \(_, x, _) -> x)) (sweep lst)

然后我们获得[1..10]的正确值:

Prelude Data.List Data.Ord Data.List> findTheEquilizer [1..10]
([1,2,3,4,5,6,7],[8,9,10])

或为7万:

Prelude Data.List Data.Ord Data.List> head (snd (findTheEquilizer [1..70000]))
49498

上面的方法并不理想,可以更好地实现,但我将其保留为练习。

答案 1 :(得分:1)

好吧,首先,让我们分析为什么它永远运行(...实际上不是永远运行,只是速度很慢),看一看partishner函数:

partishner y xs = [take y xs : [drop y xs]] ++ partishner (y - 1) xs

其中take y xsdrop y xs是线性时间,即O(N),以此类推

[take y xs : [drop y xs]]

也是O(N)。

但是,它在给定列表的每个元素上一次又一次地递归运行。现在假设给定列表的长度为M,partishner函数的每次调用都花费O(N)次,以完成计算需求:

O(1+2+...M) = (M(1+M)/2) ~ O(M^2)

现在,列表中有70k个元素,它至少需要70k ^ 2步。那么为什么挂起。

您可以使用线性方式对列表求和,而不是使用partishner函数:

sumList::(Floating a)=>[a]->[a]
sumList xs = sum 0 xs
    where sum _ [] = []
          sum s (y:ys) = let s' = s + y in s' : sum s' ys

和findEqilizer只是从左到右(leftSum)和从右到左(rightSum)对给定列表求和,并将结果作为原始程序进行处理,但是整个过程仅花费线性时间。

findEquilizer::(Ord a, Floating a) => [a] -> a
findEquilizer [] = 0 
findEquilizer xs = 
    let leftSum  = reverse $ 0:(sumList $ init xs)
        rightSum = sumList $ reverse $ xs
        afterParty = zipWith (\x y->(x-y) ** 2) leftSum rightSum
    in  fst $ minimumBy (comparing snd) (zip (reverse $ init xs) afterParty)

答案 2 :(得分:1)

我假设所有列表元素都不为负,并使用“乌龟与野兔”方法。野兔遍历列表,添加元素。乌龟也做同样的事情,但是它的总和保持了一倍,并且仔细地确保了它只走了一步,而这一步并没有使它领先于野兔。

approxEqualSums
  :: (Num a, Ord a)
  => [a] -> (Maybe a, [a])
approxEqualSums as0 = stepHare 0 Nothing as0 0 as0
  where
    -- ht is the current best guess.
    stepHare _tortoiseSum ht tortoise _hareSum []
      = (ht, tortoise)
    stepHare tortoiseSum ht tortoise hareSum (h:hs)
      = stepTortoise tortoiseSum ht tortoise (hareSum + h) hs

    stepTortoise tortoiseSum ht [] hareSum hare
      = stepHare tortoiseSum ht [] hareSum hare
    stepTortoise tortoiseSum ht tortoise@(t:ts) hareSum hare
      | tortoiseSum' <= hareSum
      = stepTortoise tortoiseSum' (Just t) ts hareSum hare
      | otherwise
      = stepHare tortoiseSum ht tortoise hareSum hare
      where tortoiseSum' = tortoiseSum + 2*t

使用中:

> approxEqualSums [1..10]
(Just 6,[7,8,9,10])

6是超过一半的最后一个元素,而7是其后的第一个元素。

答案 3 :(得分:1)

我在评论中问,OP说[1..n]并未真正定义问题。是的,我猜想问的是像[1 -> n]这样的随机升序的[1,3,7,19,37,...,1453,...,n]

但是..!即使按照给出的答案,对于[1..n]之类的列表,我们实际上也根本不需要执行任何列表操作。

  • [1..n]的总和为n*(n+1)/2
  • 这意味着我们需要为m找到n*(n+1)/4
  • 哪个意思是m(m+1)/2 = n*(n+1)/4
  • 因此,如果n == 100,那么m^2 + m - 5050 = 0

我们需要的是 enter image description here 公式,其中a = 1b = 1c = -5050得出合理的根为70.565⇒71(四舍五入)。让我们检查。 71*72/2 = 25565050-2556 = 2494表示2556 - 2494 = 62的最小差异(<71)。是的,我们必须在71分。所以像result = [[1..71],[72..100]]那样做吧!!!

但是当涉及到随后的上升时,那是另一种动物。必须先找到总和,然后像跳二进制搜索一样,通过跳到列表的中途并比较和以确定相应地是后跳还是前跳来完成。我稍后再实现。

答案 4 :(得分:0)

这是一个empirically的代码,其性能优于线性代码,即使在被解释时也能在1秒钟内达到2,000,000:

Sample  Marker1a Marker1b Marker2a Marker2b Marker3a Marker3b             
Sample1 230      250      301      302      140      150          
Sample2 233      255      304      306      143      158       
Sample3 221      250      304      310      140      152 

通过使用g :: (Ord c, Num c) => [c] -> [(Int, c)] g = head . dropWhile ((> 0) . snd . last) . map (take 2) . tails . zip [1..] . (\xs -> zipWith (-) (map (last xs -) xs) xs) . scanl1 (+) g [1..10] ==> [(6,13),(7,-1)] -- 0.0s g [1..70000] ==> [(49497,32494),(49498,-66502)] -- 0.09s g [70000,70000-1..1] ==> [(20502,66502),(20503,-32494)] -- 0.09s g [1..100000] ==> [(70710,75190),(70711,-66232)] -- 0.11s g [1..1000000] ==> [(707106,897658),(707107,-516556)] -- 0.62s g [1..2000000] ==> [(1414213,1176418),(1414214,-1652010)] -- 1.14s n^0.88 g [1..3000000] ==> [(2121320,836280),(2121321,-3406362)] -- 1.65s n^0.91 运行部分和并将总和作为其scanl1 (+)来工作,因此对于每个部分和,从总和中减去就得出了第二部分的和。分裂。

该算法假定输入列表中的所有数字严格为正,因此部分和列表单调递增。关于数字没有其他假设。

必须从该对中选择值(last的值),以使其第二部分的绝对值在两者之间较小。

这是通过g实现的。


说明:在下面的评论中,有些关于“复杂性”的困惑,但是答案完全没有说复杂性,而是使用了特定的经验度量。您无法与经验数据争论(除非您误解了其含义)。

答案并没有声称其“ 比线性更好”,它说“它比线性更好” [在测试中问题大小范围],经验数据无疑会显示出来。

最后,an appeal to authorityRobert Sedgewick是算法授权。随他去吧。

(当然,该算法还处理无序数据和有序数据)。

由于OP的代码效率低下的原因:minimumBy (comparing (abs . snd)) . g不禁会成为二次方,但是等效的map sum . inits是线性的。根本的改进来自前者的大量重复计算,而后者却避免了。 (可以在here上看到它的另一个示例。)