一尘不染

懒惰地打结一维动态规划

algorithm

几年前,我参加了一个算法课程,我们提出了以下问题(或类似的问题):

有一个n带有电梯的楼层建筑,该电梯一次只能上2层,而一次只能下3层。使用动态编程写它来计算步数所花费的电梯摆脱地面的功能i地板j

使用有状态方法显然很容易,您可以创建一个n个元素长的数组,并用值填充它。您甚至可以使用技术上无状态的方法,该方法涉及在递归传递结果时累积结果。我的问题是如何通过使用惰性评估并打结以无状态方式执行此操作。


我想我已经设计出正确的数学公式:

当i等于j且f(i,j)= 1 + f(i + 2,j)和f(i-3,j)的最小值时,f(i,j)=
0

i+2i-3在允许值内。

不幸的是我无法终止它。如果我先放i+2案例,然后选择一个均匀的地板,我可以让它评估低于目标水平的均匀地板,仅此而已。我怀疑它会直射到其他所有楼层的最高平均楼层,下降3个级别,然后重复,直到在最高的几层之间不断振荡。

因此,它可能以深度优先的方式探索无限空间(或有限但具有循环)。我想不出如何在不使用有效地模仿有状态方法的情况下使用大量数据结构的情况下,以广度优先的方式探索空间的方法。


尽管这个简单的问题令人难以置信地令人难以置信,但我怀疑看到一维解决方案后,我也许可以使它适用于问题的二维变化。


编辑:很多答案试图以不同的方式解决问题。问题本身对我来说并不有趣,问题在于所使用的方法。Chaosmatter创建minimal可以比较潜在无限数的函数的方法可能是朝正确方向迈出的一步。不幸的是,如果我尝试创建一个代表有100层建筑物的列表,那么结果将花费很长时间来计算,因为子问题的解决方案无法重复使用。

我尝试使用自引用数据结构,但是它不会终止,正在进行某种无限循环。我将发布我的代码,以便您了解我要做什么。如果有人可以使用自定义数据结构上的动态编程真正解决问题,并且使用惰性来避免多次计算,那么我将更改接受的答案。

levels = go [0..10]
  where
    go [] = []
    go (x:xs) = minimum
      [ if i == 7
          then 0
          else 1 + levels !! i
        | i <- filter (\n -> n >= 0 && n <= 10) [x+2,x-3] ]
      : go xs

您将看到如何1 + levels !! i尝试引用先前计算的结果以及如何filter (\n -> n >= 0 && n <= 10) [x+2,x-3]尝试将的值限制i为有效值。正如我所说的,这并不实际工作,它只是证明了 方法
由我希望看到这个问题就解决了。解决它的其他方法对我而言 并不 有趣。


阅读 272

收藏
2020-07-28

共1个答案

一尘不染

由于您尝试从两个维度解决此问题,并且除了上述问题之外,还针对其他问题,让我们探索一些更通用的解决方案。我们正在尝试解决有向图上的最短路径问题

目前a -> [a],我们对图形的表示类似于,其中函数返回从输入可到达的顶点。另外,任何实现都需要我们进行比较以查看两个顶点是否相同,因此我们需要Eq a

下图是有问题的,并大致介绍了解决问题的几乎所有困难:

problematic 1 = [2]
problematic 2 = [3]
problematic 3 = [2]
problematic 4 = []

当试图从1达到4时,必须检测到一个涉及2和3的循环,以确定没有从1到4的路径。

广度优先搜索

如果应用于有限图的一般问题,将提出的算法将具有在时间和空间上都不受限制的最坏情况下的性能。我们可以通过添加循环检测来修改他的解决方案,以解决仅包含有限路径和有限循环的图的一般问题。他的原始解决方案和此修改都将在无限图中找到有限路径,但都无法可靠地确定无限图中两个顶点之间没有路径。

acyclicPaths :: (Eq a) => (a->[a]) -> a -> a -> [[a]]
acyclicPaths steps i j = map (tail . reverse) . filter ((== j).head) $ queue
  where
    queue = [[i]] ++ gen 1 queue
    gen d _ | d <= 0 = []
    gen d (visited:t) = let r = filter ((flip notElem) visited) . steps . head $ visited 
                        in map (:visited) r ++ gen (d+length r-1) t

shortestPath :: (Eq a) => (a->[a]) -> a -> a -> Maybe [a]
shortestPath succs i j = listToMaybe (acyclicPaths succs i j)

重用stepWill的答案中的函数作为示例问题的定义,我们可以得到11层建筑中从4楼到5楼的最短路径的长度fmap length $ shortestPath (step 11) 4 5。这返回Just 3

让我们考虑一个具有v个顶点和e个边的有限图。可以通过大小为n〜O(v +
e)的输入来描述具有v个顶点和e个边的图。此算法的最坏情况图是具有一个不可达的顶点,j其余的顶点和边专门用于创建从开始的最大数量的非循环路径i。这可能就像一个派系,包含所有不是i或的顶点j,且边缘i到并非所有的另一个顶点j。具有e个边的小群中的顶点数为O(e
^(1/2)),因此此图具有e〜O(n),v〜O(n ^(1/2))。在确定该图j不可达之前,该图将具有O((n ^(1/2))!)条路径进行探索。

在这种情况下,此函数所需的内存为O((n ^(1/2))!),因为它只需要为每个路径不断增加队列。

在这种情况下,此函数所需的时间为O((n ^(1/2))!* n ^(1/2))。每次扩展路径时,都必须检查新节点是否不在路径中,这需要O(v)〜O(n
^(1/2))时间。如果我们拥有Ord a并使用一个Set a或类似的结构来存储访问的顶点,则可以将其改进为O(log(n ^(1/2)))。

对于非有限的图形,这个功能应该只是不能确切地终止时不存在从有限的路径ij但确实存在,从一个非限定路径ij

动态编程

动态编程解决方案不能以相同的方式推广。让我们探究原因。

首先,我们将采用chaosmasttter的解决方案,使其具有与广度优先搜索解决方案相同的界面:

instance Show Natural where
    show = show . toNum

infinity = Next infinity

shortestPath' :: (Eq a) => (a->[a]) -> a -> a -> Natural
shortestPath' steps i j = go i
    where
        go i | i == j = Zero
             | otherwise = Next . foldr minimal infinity . map go . steps $ i

这很好地工作电梯的问题,shortestPath' (step 11) 4 53。不幸的是,由于我们的问题,shortestPath' problematic 1 4堆栈溢出了。如果我们为Natural数字添加更多代码:

fromInt :: Int -> Natural
fromInt x = (iterate Next Zero) !! x

instance Eq Natural where
    Zero == Zero         = True
    (Next a) == (Next b) = a == b
    _ == _ = False

instance Ord Natural where
    compare Zero Zero         = EQ
    compare Zero _            = LT
    compare _ Zero            = GT
    compare (Next a) (Next b) = compare a b

我们可以问最短路径是否短于某个上限。我认为,这确实显示了惰性评估正在发生的情况。problematic 1 4 < fromInt 100False并且problematic 1 4 > fromInt 100True

接下来,要探索动态编程,我们将需要介绍一些动态编程。由于我们将为所有子问题建立一个解决方案表,因此我们需要知道顶点可以采用的可能值。这给我们一个稍微不同的界面:

shortestPath'' :: (Ix a) => (a->[a]) -> (a, a) -> a -> a -> Natural
shortestPath'' steps bounds i j = go i
    where
        go i = lookupTable ! i
        lookupTable = buildTable bounds go2
        go2 i | i == j = Zero
              | otherwise = Next . foldr minimal infinity . map go . steps $ i

-- A utility function that makes memoizing things easier
buildTable :: (Ix i) => (i, i) -> (i -> e) -> Array i e
buildTable bounds f = array bounds . map (\x -> (x, f x)) $ range bounds

我们可以使用like shortestPath'' (step 11) (1,11) 4 5shortestPath'' problematic (1,4) 1 4 < fromInt 100。这仍然无法检测到周期…

动态编程和循环检测

循环检测对于动态编程是有问题的,因为从不同路径进入子问题时,子问题并不相同。考虑我们problematic问题的一个变体。

problematic' 1 = [2, 3]
problematic' 2 = [3]
problematic' 3 = [2]
problematic' 4 = []

如果我们试图从获取14,我们有两个选择:

  • 2和走的最短路径2,以4
  • 3和走的最短路径3,以4

如果我们选择探索2,我们将面临以下选择:

  • 3和走的最短路径3,以4

我们希望将从3到的最短路径的两种探索结合4到表中的同一条目中。如果我们想避免循环,那实际上是有些微妙的事情。我们面临的问题确实是:

  • 2,并采取从最短路径2,以4不参观1
  • 3,并采取从最短路径3,以4不参观1

选择之后 2

  • 3和走的最短路径3,以4不登陆12

关于如何获得这两个问题34有两个略有不同的答案。它们是两个不同的子问题,它们不能放在表的同一位置。回答第一个问题最终需要确定你不能去42。回答第二个问题很简单。

我们可以为每个可能的先前访问的顶点集创建一堆表,但这听起来不是很有效。我几乎说服了我自己,仅靠懒惰就无法解决动态编程问题。

广度优先搜索Redux

在研究具有可达性或周期检测的动态编程解决方案时,我意识到,一旦我们看到了选项中的一个节点,无论我们是否跟随该节点,访问该节点的后续路径都永远不是最优的。如果我们重新考虑problematic'

如果我们试图从获取14,我们有两个选择:

  • 2,并采取从最短路径24无需访问123
  • 3,并采取从最短路径34无需访问123

这为我们提供了一种算法,可以很容易地找到最短路径的长度:

-- Vertices first reachable in each generation
generations :: (Ord a) => (a->[a]) -> a -> [Set.Set a]
generations steps i = takeWhile (not . Set.null) $ Set.singleton i: go (Set.singleton i) (Set.singleton i)
    where go seen previouslyNovel = let reachable = Set.fromList (Set.toList previouslyNovel >>= steps)
                                        novel = reachable `Set.difference` seen
                                        nowSeen = reachable `Set.union` seen
                                    in novel:go nowSeen novel

lengthShortestPath :: (Ord a) => (a->[a]) -> a -> a -> Maybe Int
lengthShortestPath steps i j = findIndex (Set.member j) $ generations steps i

不出所料,lengthShortestPath (step 11) 4 5Just 3lengthShortestPath problematic 1 4Nothing

在最坏的情况下,generations需要的空间为O(v * log v),时间为O(v * e * log v)。

2020-07-28