为啥这个的第二个版本在指数时间内运行?
Posted
技术标签:
【中文标题】为啥这个的第二个版本在指数时间内运行?【英文标题】:Why does the second version of this run in exponential time?为什么这个的第二个版本在指数时间内运行? 【发布时间】:2021-10-31 22:28:00 【问题描述】:我正在编写一个程序来确定两个字符串之间的 Levenshtein 距离是否在线性时间内正好是 2。
我有一个算法可以做到这一点。我使用简单的递归方法来扫描字符串,当它发现差异时,它会分支为 3 个选项,尝试删除、插入或替换。但是我做了一个修改,如果我们的总数超过 2,我们就放弃那个分支。这将分支的总数限制为 9,并使算法线性化。
这是我的初始实现:
diffIs2 x y =
2 == go 0 x y
where
go total xs@(x1:xs') ys@(y1:ys')
| x1 == y1
=
go total xs' ys'
| total < 2
=
minimum $ zipWith (go $ total + 1)
[ xs', xs , xs' ]
[ ys , ys', ys' ]
go total xs ys =
total + length xs + length ys
测试证实这在线性时间内运行。
我还有第二个类似的程序,据我所知,它也应该是线性的。
这里的想法是使用短路和惰性评估来限制评估的分支数量。我们总是允许分支,但是,当我们分支时,我们取每个和 3 的最小值。这样,如果分支总数超过 3,短路应该会阻止进一步的评估。
我们必须为部分求值实现一个自然数类型。
data Nat
= Zero
| Succ Nat
deriving
( Eq
, Ord
)
natLength :: [a] -> Nat
natLength [] =
Zero
natLength (a : b) =
Succ (natLength b)
nat3 =
Succ $ Succ $ Succ Zero
diffIs2 x y =
Succ (Succ Zero) == go x y
where
go xs@(x1:xs') ys@(y1:ys')
| x1 == y1
=
go xs' ys'
| otherwise
=
Succ $
minimum $
map (min nat3) $
zipWith go
[ xs', xs , xs' ]
[ ys , ys', ys' ]
go xs ys =
natLength $ xs ++ ys
对此进行测试表明它不会在线性时间内运行。有些东西使它成指数,但我不知道是什么。短路确实按预期工作。我们可以通过以下程序来展示这一点,该程序由于min
的短路而在有限时间内停止:
f = Succ f
main =
print $ (Succ Zero) == min (Succ Zero) f
但是当把它放在算法中时,它似乎并没有像我预期的那样工作。
为什么第二个代码是指数的?我的误解是什么?
【问题讨论】:
f = Succ f
是Succ f
的无限递归,所以Succ (Succ (Succ (Succ (...))))))
@WillemVanOnsem 是的,这就是意图。由于惰性求值,程序会在有限时间内停止,而如果它是严格的,它将永远循环。
我不确定,但我猜minimum
不够懒惰以启用短路。 minimum ys
在所有情况下都会完全扫描ys
。最重要的是minimum [Succ f, Succ f]
不是Succ f
(即f
),其中f
是您的无穷大值。您可能需要一个自定义的minimum
,它会在可能的情况下(即没有找到Zero
时)在深入挖掘值之前尽早返回Succ
。
Succ Zero `min` (Succ undefined)
不同,尽管我们知道它应该是Succ Zero
。会不会是您的nat3
实际上将分支因子设置为 4?会不会是这样,给您带来了如此大的恒定因素放缓,或者您实际上绝对肯定地检测到了指数增长?
@WillNess 我不能确定很多渐近复杂度,但是增长更符合指数复杂度而不是线性复杂度,我可以说运行了带有分支的算法 1 的版本因子 4 比下面的算法快得多。
【参考方案1】:
虽然默认的 min
对于问题末尾的简单示例来说已经足够懒惰了,但它并不像我们希望的那样懒惰:
ghci> let inf = Succ inf
ghci> inf `min` inf `min` Zero == Zero
^CInterrupted.
要修复它,我们需要一个懒惰的min
:
min' :: Nat -> Nat -> Nat
min' Zero _ = Zero
min' _ Zero = Zero
min' (Succ x) (Succ y) = Succ (min' x y)
最大的不同是现在我们可以在完全评估参数之前得到部分结果:
min (Succ undefined) (Succ undefined) === undefined
min' (Succ undefined) (Succ undefined) === Succ (min' undefined undefined)
现在我们可以如下使用它:
diffIs2 :: Eq a => [a] -> [a] -> Bool
diffIs2 x y = Succ (Succ Zero) == go x y
where
go xs@(x1:xs') ys@(y1:ys')
| x1 == y1 = go xs' ys'
| otherwise = Succ $ go xs' ys `min'` go xs ys' `min'` go xs' ys'
go xs ys = natLength $ xs ++ ys
请注意,您甚至不需要额外的min' nat3
,因为无论如何***比较只会强制前三个Succ
s。
【讨论】:
我看到min
没有像它可能的那样懒惰,我看到修复是如何工作的,但我仍然在努力理解为什么我的代码不起作用。在提供的代码中,由于min nat3
映射到整个列表中,因此在两个输入可能都长于 3 的任何情况下,代码都不会采用 min
。如示例所示,min nat3
不会评估整个表达式.
@ÉamnOlive 这是一个好问题。我相信这是因为每个分支都会自己分支,并且在该分支中,3 的限制被重置。并且Succ
s 不会立即传播,仅传播单个Succ
,并且仅传播单个级别。因此,一旦到达输入的末尾,它只会达到 3 的限制。很高兴展示它在实践中的评估方式,但它很快就会变得非常混乱。以上是关于为啥这个的第二个版本在指数时间内运行?的主要内容,如果未能解决你的问题,请参考以下文章
为啥使用第一个阅读器 read() 运行第二个阅读器比在自己的阅读器上运行它运行得更快?
在 UINavigationController 中运行 UIViewController 作为 UISplitViewController 的第二个视图