the Smallest Free Number

Pearl 1: 给定一个自然数的有限集X, 计算不属于X的最小自然数. X表示为不包含重复元素的无序列表. 时间复杂度要求\(O(n)\).

Type: minfree :: [Int] -> Int(也可以额外的定义自然数类型, 不过这不是我们的重点)

"Pearls of Functional Algorithm Design"的第一章, 其描述了一个分治的算法和一个基于array的算法, 这里按个人的思路讲解一下基于分治的算法, 基于array的算法具体可以查阅原文. 首先拿到这个问题, 我觉得最直接的想法就是

Base Solution: minfree xs = head $ [0..] \\ xs

然而这和要求的线性时间复杂度不符. 第二个想法就是设计一个fold的函数遍历一遍列表, 这样时间复杂度符合要求. 但是越来越多的边界条件让我意识到思路不对. 看了原文才发现忽略了解题的一个重要条件.

Fact: [0..n]中的所有自然数不可能都在X(xs)中, 其中n = length xs.

这也很容易证明, 因为\(n + 1 = length\ [0..n] > n\), 因此不属于集合X的最小自然数就是[0..n]中不属于X的最小自然数. 至此,该问题很容易解决, 只需要一个marked的array来表示[0,,n]中的自然数是否在X中即可. 下面描述基于分治的算法, 首先给出一个基本的结论.

Theorem: (as ++ bs) \\ (us ++ vs) == (as \\ us) ++ (bs \\ vs), 如果as \\ vs == as && bs \\ us == bs.

这显然是符合集合论的. 即[0..n]可以拆分为两个不相交的集合[0..b-1]以及[b..n], 因此基础解中的[0..] \\ xs可以拆分为

([0..b-1] \\ us) ++ ([b..] \\ vs) where
  (us, vs) = partition (< b) xs

minfree则可以改写为

minfree xs = if null $ [0..b-1] \\ us
             then head $ [b..] \\ vs
             else head $ [0..b-1] \\ us
             where (us, vs) = partition (< b) xs
                   b        = 

很容易发现null $ [0..b-1] \\ us等价于length us == b, 后者更加高效. 同时, 我们也可以进一步的抽象minfree, 因为我们在上面限制了从0开始:

minfrom :: Int -> [Int] -> Int
minfrom a xs = head $ [a..] \\ xs

至此, 我们的minfree可以改为:

minfree = minfrom 0

minfrom :: Int -> [Int] -> Int
minfrom a xs | null xs            = a
             | length us == b - a = minfrom b vs
             | otherwise          = minfrom a us
               where (us, vs) = partition (< b) xs
                     b        = 

接下来的问题是b应该是多少, 显然b可以是\((a, n=length\ xs)\)中的任意一个自然数, 不过b的选择应该使得usvs的长度尽可能的小, 否则的会导致算法在最坏情况下开销的增加. 因此比较理想的取值是b = a + 1 + n `div` 2, 这样如果length us < b - a的, 那么length us < b - a < n `div` 2 + 1 <= n `div` 2, 而如果length us == b - a, 那么length vs = n - b + a = n - n `div` 2 - 1 <= n `div` 2. 此时可以看到算法的复杂度是\(O(n)\)的. 在minfree的最终版本, 为了避免重复计算, 我们可以传入(length xs, xs).

Final Solution:

minfree :: [Int] -> Int
minfree xs = minfrom 0 (length xs, xs)

minfrom :: Int -> (Int, [Int]) -> Int
minfrom a (n, xs) | n == 0     = a
                  | m == b - a = minfrom b (n - m, vs)
                  | otherwise  = minfrom a (m, us)
                    where (us, vs) = partition (< b) xs
                          b        = a + 1 + n `div` 2
                          m        = length us

后记

整个pearl看下来给我最大的感受就是首先给出一个比较naive的解, 然后利用分治的思想一步步的分解问题并优化解. 同时, 虽然函数式的算法总是会比相应的命令式的算法差一个对数阶(因为函数式的算法中无法保证array的更新是常数级的, 通常是一个对数级的), 但是在这个pearl上, 作者通过不断的迭代算法缩小了这个差距.

posted @ 2021-03-29 00:25  Christophe1997  阅读(49)  评论(0编辑  收藏  举报