虽然这是一个糟糕的算法(应该采用记忆化,我将在第二个部分中介绍),但忽略这一点...
使用O(1)原语而不是O(n)
一个问题是你使用了很多O(n)的列表调用(haskell列表是单向链表)。更好的数据结构会给你O(1)操作,我使用了vector:
import qualified Data.Vector as V
editDistance :: Eq a => [a] -> [a] -> Int
editDistance s1 s2 = editDistance' 1 1 1 (V.fromList s1) (V.fromList s2)
editDistance' :: Eq a => Int -> Int -> Int -> V.Vector a -> V.Vector a -> Int
editDistance' del sub ins s1 s2
| V.null s2 = ins * V.length s1
| V.null s1 = ins * V.length s2
| V.last s1 == V.last s2 = editDistance' del sub ins (V.init s1) (V.init s2)
| otherwise = minimum [ editDistance' del sub ins s1 (V.init s2) + del
, editDistance' del sub ins (V.init s1) (V.init s2) + sub
, editDistance' del sub ins (V.init s1) s2 + ins
]
对于列表而言,O(n) 操作包括 init、length 和 last(尽管init至少可以懒惰地执行)。但是在使用 Vector 时,所有这些操作都是 O(1)。
虽然真正的基准测试应该使用Criterion,但以下是一个快速而简单的基准测试:
str2 = replicate 15 'a' ++ replicate 25 'b'
str1 = replicate 20 'a' ++ replicate 20 'b'
main = print $ editDistance str1 str2
这段代码展示了向量版本所需的时间为0.09秒,而字符串版本需要1.6秒,因此我们省下了一个数量级,甚至还没有看你的editDistance
算法。
现在考虑记忆化结果怎么办?
更大的问题显然是需要进行记忆化。我把这当作学习monad-memo包的机会 - 天哪,真是太棒了!只需要满足额外的限制(你需要Ord a
),你就可以轻松地获得记忆化功能。代码如下:
import qualified Data.Vector as V
import Control.Monad.Memo
editDistance :: (Eq a, Ord a) => [a] -> [a] -> Int
editDistance s1 s2 = startEvalMemo $ editDistance' (1, 1, 1, (V.fromList s1), (V.fromList s2))
editDistance' :: (MonadMemo (Int, Int, Int, V.Vector a, V.Vector a) Int m, Eq a) => (Int, Int, Int, V.Vector a, V.Vector a) -> m Int
editDistance' (del, sub, ins, s1, s2)
| V.null s2 = return $ ins * V.length s1
| V.null s1 = return $ ins * V.length s2
| V.last s1 == V.last s2 = memo editDistance' (del, sub, ins, (V.init s1), (V.init s2))
| otherwise = do
r1 <- memo editDistance' (del, sub, ins, s1, (V.init s2))
r2 <- memo editDistance' (del, sub, ins, (V.init s1), (V.init s2))
r3 <- memo editDistance' (del, sub, ins, (V.init s1), s2)
return $ minimum [ r1 + del
, r2 + sub
, r3 + ins
]
你看到了记忆化需要一个单一的"键"(参见MonadMemo类)吗?我将所有参数打包成一个又大又丑的元组。它还需要一个"value",即你得到的
Int
。然后,只需使用"memo"函数插入和运行你想要记忆化的值即可。
为了进行基准测试,我使用了一个更短但距离较远的字符串:
$ time ./so
12
real 0m0.003s
$ time ./so3
12
real 1m33.122s
不要考虑运行非记忆化的字符串版本,我估计至少需要15分钟。至于我,现在我爱上了monad-memo - 感谢Eduard的这个包!
编辑:在记忆化版本中,String和Vector之间的差异并不大,但当距离达到200左右时,差异仍会增长到2倍左右,因此仍然值得使用。
编辑:也许我应该解释一下为什么“显然”更大的问题是记忆化结果。那么,如果您看一下原始算法的核心:
[ editDistance' ... s1 (V.init s2) + del
, editDistance' ... (V.init s1) (V.init s2) + sub
, editDistance' ... (V.init s1) s2 + ins]
很明显调用editDistance' s1 s2
会导致3个调用editDistance'
...每个都会再次调用editDistance'
三次...接着又是三次...指数级的爆炸!幸运的是大部分调用是相同的!例如(使用-->
表示“调用”,eD
表示editDistance'
):
eD s1 s2 --> eD s1 (init s2) -- The parent
, eD (init s1) s2
, eD (init s1) (init s2)
eD (init s1) s2 --> eD (init s1) (init s2) -- The first "child"
, eD (init (init s1)) s2
, eD (init (init s1)) (init s2)
eD s1 (init s2) --> eD s1 (init (init s2))
, eD (init s1) (init s2)
, eD (init s1) (init (init s2))
仅考虑父节点和两个直接子节点,我们可以看到调用ed (init s1) (init s2)
三次。 另一个子节点也与父节点共享调用,并且所有子节点彼此之间(以及它们的子节点)共享许多调用,这启示了蒙提·派森(Monty Python)幽默短剧。
制作类似于runMemo
的函数,返回使用了缓存结果的数量,可能是一项有趣、有益的练习。
editDist s1 (init s2)
),该函数最终将计算editDist (init s1) (init s2)
。这是调用者列表中的第二个元素和被调用者列表中的第三个元素! - Thomas M. DuBuisson