尝试优化此算法/代码以计算a和b之间相互互质整数的最大子集

时间:2017-08-14 16:12:39

标签: algorithm performance haskell optimization

我在Haskell中编写一个函数,它接受两个整数a和b,并计算[a,b]的最大子集的长度,使得所有元素互为互质。现在,其原因在于我认为调查这些值可能会产生产生微不足道的界限的手段(可能大到足以暗示各个猜测范围内的实际素数,例如连续的正方形)。很自然地,我试图为一些相当大的数字运行它。

不幸的是,下面的代码运行速度不够快,以至于我不能使用它。我认为这个缺陷是Haskell的懒惰政策引起的问题,但我既不知道语法,也不知道我需要强制执行以防止操作累积的地方。

subsets [] = [[]]
subsets (x:xs) = subsets xs ++ map (x:) (subsets xs)

divides a b = (mod a b == 0)

coprime a b = ((gcd a b) == 1)

mutually_coprime []  = True
mutually_coprime (x:xs) | (coprime_list x xs) = mutually_coprime xs
                        | otherwise = False

coprime_list _ [] = True
coprime_list a (x:xs) | (coprime a x) = coprime_list a xs
                      | otherwise = False

coprime_subsets a b = coprime_subsets_helper (subsets [a..b])

coprime_subsets_helper [] = []
coprime_subsets_helper (x:xs) | (mutually_coprime x) = [x] ++ (coprime_subsets_helper xs)
                              | otherwise = coprime_subsets_helper xs

coprime_subset_length a b = max_element (map list_length (coprime_subsets a b))

list_length [] = 0
list_length (x:xs) = 1 + list_length xs

max_element a = max_element_helper a 0

max_element_helper [] a = a
max_element_helper (x:xs) a | (x > a) = max_element_helper xs x
                            | otherwise = max_element_helper xs a

只是为了弄清楚这是什么类型的输入," coprime_subsets 100 120"我似乎永远不会停止。我实际上让它继续运行,起床,做了一些其他的事情,后来又回来了。 它仍在运行。我怀疑一个大瓶颈是立即计算所有子集。但是,我不想在生成的子集上设置人为的下限。这可能会掩盖所有的互质组合,让我一无所获。

到目前为止我尝试过:

  • 我用gcd替换了原来的coprime函数。最初这使用了模数和迭代检查所有整数。我认为gcd使用类似Euclid算法的东西,理论上应该运行得更快。

  • 我一直试图想出一种方法来将子集的生成构建到互质集合生成器中。到目前为止,我还没有能够想出任何东西。我也不确定这对任何事情是否真的有帮助。

  • 我一直在寻找Haskell的懒惰政策可能伤害我的任何地方。没什么是突出的,但我确定。

我也意识到这可能是我使用的环境(winhugs)的效率问题。我说这是问题所在;然而,当我问如何确定数学堆栈交换的最大子集(对于一般n大小的范围)时,我收到的答案表明,从计算上来说这是一个非常慢的计算方法。如果是这样,那没关系。我只是希望能够完成我感兴趣的一些范围而不需要它几乎永远。

我知道这个网站一般不允许效率;但是,我已经尝试了很多,而且我不仅仅是想在这里懒惰。我知道Haskell有很多奇怪的怪癖可以强迫它看起来效率低下。自从我编写了它以来已经有一段时间了,我怀疑我已经陷入其中一个怪癖中。

3 个答案:

答案 0 :(得分:5)

首先,我们必须找出什么是慢的。

使用数字时,应使用提供所需精度和范围的最快表示。您的代码没有顶级类型签名,因此GHC会将coprime_subsets的类型签名推断为

coprime_subsets :: Integral a => a -> a -> [[a]]

这允许GHC为您选择Integral,它会愉快地选择Integer,这比Int慢得多。使用Integer,程序花费大量时间来计算gcds。强制GHC使用Int使运行时间从6秒减少到1秒,因为GHC可以直接对机器整数进行整数运算。

注意:始终提供顶级类型签名也是一种好习惯。即使编译器没有从中受益,人类也经常这样做。

现在,问题的关键。运行

main = print . length $ coprime_subsets (100 :: Int) 120
main :: IO ()

启用了性能分析(Stack为stack build --profile)并将+RTS -p -h传递给可执行文件(时间-p和空间-h)给出了细分:

COST CENTRE            MODULE  SRC                            %time %alloc

subsets                Coprime src/Coprime.hs:(4,1)-(5,52)     52.5  100.0
coprime                Coprime src/Coprime.hs:11:1-26          25.5    0.0
coprime_list           Coprime src/Coprime.hs:(19,1)-(21,41)   18.5    0.0
coprime_subsets_helper Coprime src/Coprime.hs:(27,1)-(29,69)    1.8    0.0
mutually_coprime       Coprime src/Coprime.hs:(14,1)-(16,43)    1.7    0.0

当我们使用Integer时,绝大多数(约78%)的时间用于coprime测试。现在大多数人都在构建powerset,所以让我们先看一下。

接下来,我们必须理解为什么它很慢。

通常有三种策略可以加快速度:

  1. 提高其渐近复杂度。
  2. 改善其常数因素。
  3. 少打电话。
  4. 其中哪些可能适用于subsets? (1)是一个明显的选择。构建powerset是O(2^n),因此这里的任何渐近改进确实非常有用。我们能找到吗?从理论上讲,我们应该能够。正如丹尼尔所说,这个问题等同于最大团体问题,这也是指数问题。但是,最大派系有一个较小基数的解,这意味着我们应该能够找到这个问题的渐近改进。

    减少其渐近复杂度(以及我们称之为它的次数)的关键洞察力是我们生成绝大多数子集只是为了稍后在我们检查它们的互相性时抛弃它们。如果我们可以避免最初生成错误的子集,我们将生成更少的子集并执行更少的检查以实现互动性。如果修剪结果的数量与整个计算树的大小成比例,则这将产生渐近的改进。这种修剪在功能算法优化中很常见;你可以在Richard Bird's sudoku solver中看到一个有启发性的例子。事实上,如果我们可以编写一个只生成非互质子集的函数,我们就能解决整个问题!

    最后,我们已经准备好解决它了!

    我们可以通过修改原始生成器subsets来过滤掉非互质术语:

    coprimeSubsets [] = [[]]
    coprimeSubsets (x:xs)
      = coprimeSubsets xs ++ map (x :) (coprimeSubsets (filter (coprime x) xs))
    

    (我们可以在这里使用一些聪明的折叠,如果我们考虑它真的很难但显式递归也很好。)

    一旦我们这样做,我们可以在~0.1秒内找到[100..120]的互质子集,这是一个数量级的改进。成本中心报告讲述了这个故事:

    COST CENTRE    MODULE          SRC                            %time %alloc
    
    MAIN           MAIN            <built-in>                      42.9    0.5
    coprimeSubsets Coprime         src/Coprime.hs:(33,1)-(35,75)   28.6   67.4
    CAF            GHC.IO.Encoding <entire-module>                 14.3    0.1
    coprime        Coprime         src/Coprime.hs:13:1-26          14.3   31.1
    

    现在我们实际上花了大部分时间做IO等。此外,我们coprime测试的呼叫数量现在只有3,848,这是几个数量级的改进。我们还可以在3秒内找到[100..150]的互质子集,相比之下......好吧,我没有等到它完成,但至少还有几分钟。

    下一个寻找加速的地方可能是记住coprime函数,因为这个问题涉及多次为相同的参数计算它。

答案 1 :(得分:2)

我建议两个主要变化:

  1. 为每对数字计算一次gcd,而不是重复计算。
  2. 只扩展“可接受的”子集,而不是预先构建所有子集。
  3. (这两项建议都与懒惰无关;我认为你的猜测是一个红色的鲱鱼。)下面我实施这两个建议;我觉得很简洁:

    coprime_subsets' [] = [[]]
    coprime_subsets' (x:xs)
        = coprime_subsets' xs
        ++ map (x:) (coprime_subsets' (filter ((1==) . gcd x) xs))
    

    我们可以检查这是否在ghci中计算相同的答案:

    > coprime_subsets 10 20 == coprime_subsets' [10..20]
    True
    > coprime_subsets 100 120 == coprime_subsets' [100..120]
    True
    

    显然我的计算机比你的计算机快得多:coprime_subsets 100 120在不到16秒的时间内完成,即使在ghci中也是如此。当然,即使在ghci中,我的版本也需要0.02秒,所以... =)

    如果你只关心最大的互质子集,你可以让它更快。这里的主要变化是在递归前面添加了filter

    maximal_coprime_subsets [] = [[]]
    maximal_coprime_subsets (x:xs)
        = filter (any ((>1) . gcd x)) (maximal_coprime_subsets xs)
        ++ map (x:) (maximal_coprime_subsets (filter ((1==) . gcd x) xs))
    

    即使这个重复gcd更多,即使在ghci中也可以在0.01秒内完成[100..120],所以这比以前的实现更快

    这里的时间差异似乎是输出coprime_subsets'打印时间更长,哈哈!

答案 2 :(得分:1)

我不确定这是否可以有效地解决问题,但即使在GHCI中,它也会在0.02秒内返回所提供的两个整数值中的所有子集。

.lnk