Lazily Tying the Knot for 1 Dimensional Dynamic Programming

前端 未结 4 901
春和景丽
春和景丽 2021-01-01 17:59

Several years ago I took an algorithms course where we were giving the following problem (or one like it):

There is a building of n floor

4条回答
  •  情歌与酒
    2021-01-01 18:56

    standing on the floor i of n-story building, find minimal number of steps it takes to get to the floor j, where

    step n i = [i-3 | i-3 > 0] ++ [i+2 | i+2 <= n]
    

    thus we have a tree. we need to search it in breadth-first fashion until we get a node holding the value j. its depth is the number of steps. we build a queue, carrying the depth levels,

    solution n i j = case dropWhile ((/= j).snd) queue
                       of []        -> Nothing
                          ((k,_):_) -> Just k
      where
        queue = [(0,i)] ++ gen 1 queue
    

    The function gen d p takes its input p from d notches back from its production point along the output queue:

        gen d _ | d <= 0 = []
        gen d ((k,i1):t) = let r = step n i1 
                           in map (k+1 ,) r ++ gen (d+length r-1) t
    

    Uses TupleSections. There's no knot tying here, just corecursion, i.e. (optimistic) forward production and frugal exploration. Works fine without knot tying because we only look for the first solution. If we were searching for several of them, then we'd need to eliminate the cycles somehow.

    • see also: https://en.wikipedia.org/wiki/Corecursion#Discussion

    With the cycle detection:

    solutionCD1 n i j = case dropWhile ((/= j).snd) queue
                        of []        -> Nothing
                           ((k,_):_) -> Just k
      where
        step n i visited =    [i2 | let i2=i-3, not $ elem i2 visited, i2 > 0] 
                           ++ [i2 | let i2=i+2, not $ elem i2 visited, i2 <=n]
        queue = [(0,i)] ++ gen 1 queue [i]
        gen d _ _ | d <= 0 = []
        gen d ((k,i1):t) visited = let r = step n i1 visited
                                   in map (k+1 ,) r ++ 
                                      gen (d+length r-1) t (r++visited)
    

    e.g. solution CD1 100 100 7 runs instantly, producing Just 31. The visited list is pretty much a copy of the instantiated prefix of the queue itself. It could be maintained as a Map, to improve time complexity (as it is, sol 10000 10000 7 => Just 3331 takes 1.27 secs on Ideone).


    Some explanations seem to be in order.

    First, there's nothing 2D about your problem, because the target floor j is fixed.

    What you seem to want is memoization, as your latest edit indicates. Memoization is useful for recursive solutions; your function is indeed recursive - analyzing its argument into sub-cases, synthetizing its result from results of calling itself on sub-cases (here, i+2 and i-3) which are closer to the base case (here, i==j).

    Because arithmetics is strict, your formula is divergent in the presence of any infinite path in the tree of steps (going from floor to floor). The answer by chaosmasttter, by using lazy arithmetics instead, turns it automagically into a breadth-first search algorithm which is divergent only if there's no finite paths in the tree, exactly like my first solution above (save for the fact that it's not checking for out-of-bounds indices). But it is still recursive, so indeed memoization is called for.

    The usual way to approach it first, is to introduce sharing by "going through a list" (inefficient, because of sequential access; for efficient memoization solutions see hackage):

    f n i j = g i
      where
        gs = map g [0..n]              -- floors 1,...,n  (0 is unused)
        g i | i == j = Zero
            | r > n  = Next (gs !! l)  -- assuming there's enough floors in the building
            | l < 1  = Next (gs !! r)
            | otherwise = Next $ minimal (gs !! l) (gs !! r)
          where r = i + 2
                l = i - 3
    

    not tested.

    My solution is corecursive. It needs no memoization (just needs to be careful with the duplicates), because it is generative, like the dynamic programming is too. It proceeds away from its starting case, i.e. the starting floor. An external accessor chooses the appropriate generated result.

    It does tie a knot - it defines queue by using it - queue is on both sides of the equation. I consider it the simpler case of knot tying, because it is just about accessing the previously generated values, in disguise.

    The knot tying of the 2nd kind, the more complicated one, is usually about putting some yet-undefined value in some data structure and returning it to be defined by some later portion of the code (like e.g. a back-link pointer in doubly-linked circular list); this is indeed not what my1 code is doing. What it does do is generating a queue, adding at its end and "removing" from its front; in the end it's just a difference list technique of Prolog, the open-ended list with its end pointer maintained and updated, the top-down list building of tail recursion modulo cons - all the same things conceptually. First described (though not named) in 1974, AFAIK.


    1 based entirely on the code from Wikipedia.

提交回复
热议问题