我是Haskell的初学者,试图通过解决一些在线测验/问题集来学习更多有关该语言的信息。
这个问题/问题相当冗长,但其中一部分需要代码才能找到将给定列表分为两个(几乎)相等(按总数)子列表的数字。
给出[1..10]
自7
和1+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
个元素的列表时,麻烦就开始了。它需要永远的计算。
那么,我必须在代码中进行哪些更改以使其更好地运行,还是必须更改整个方法?我猜是晚了,但是我不确定该怎么做。
答案 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 xs
和drop 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
我们需要的是
公式,其中a = 1
,b = 1
和c = -5050
得出合理的根为70.565⇒71(四舍五入)。让我们检查。 71*72/2 = 2556
和5050-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 authority。 Robert Sedgewick是算法授权。随他去吧。
(当然,该算法还处理无序数据和有序数据)。
由于OP的代码效率低下的原因:minimumBy (comparing (abs . snd)) . g
不禁会成为二次方,但是等效的map sum . inits
是线性的。根本的改进来自前者的大量重复计算,而后者却避免了。 (可以在here上看到它的另一个示例。)