Леной привязка узла для 1-мерного динамического программирования

Несколько лет назад я взял курс алгоритмов, где мы задавали следующую проблему (или такую, как она):

Существует здание этажей n с лифтом, который может подниматься только на 2 этажа одновременно и вниз по 3 этажа за раз. Используя динамическое программирование, напишите функцию, которая будет вычислять количество шагов, которые требуется для подъема лифта от пола i до пола j.

Это, очевидно, легко, используя подход, основанный на состоянии, вы создаете массив n элементов длиной и заполняете его значениями. Вы даже можете использовать технически не-stateful подход, который предполагает накопление результата, рекурсивно передавая его. Мой вопрос заключается в том, как сделать это неконфессионально, используя ленивую оценку и привязывая узел.


Я думаю, что я разработал правильную математическую формулу:

f(i,j) = 0 when i is equal to j and f(i,j) = 1 + min of f(i+2,j) and f(i-3,j)

где i+2 и i-3 находятся в пределах допустимых значений.

К сожалению, я не могу закончить его. Если я сначала поставлю случай i+2, а затем выберите ровный пол, я получу его, чтобы оценить ровные этажи ниже целевого уровня, но это. Я подозреваю, что он стреляет прямо на самый высокий ровный пол для всего остального, падает на 3 уровня, затем повторяет, навсегда колеблется между верхними несколькими этажами.

Поэтому он, вероятно, сначала исследует бесконечное пространство (или конечное, но с петлями) в глубину. Я не могу придумать, как исследовать пространство в первом порядке, не используя множество структур данных между ними, которые эффективно имитируют подход с учетом состояния.


Хотя эта простая проблема неутешительно сложна, я подозреваю, что, увидев решение в 1 измерении, я мог бы заставить его работать для двумерного изменения проблемы.


EDIT: многие ответы пытались решить проблему по-другому. Сама проблема не интересна мне, вопрос в том, какой метод используется. Подход хаосматера к созданию функции 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 допустимыми. Как я уже сказал, это фактически не работает, оно просто демонстрирует метод , с помощью которого я хочу, чтобы эта проблема была решена. Другим способом ее решения является не.

Ответ 1

Поскольку вы пытаетесь решить это в двух измерениях и для других проблем, кроме описанных, давайте рассмотрим еще несколько общих решений. Мы пытаемся решить проблему кратчайший путь на ориентированных графах.

Наше представление графика в настоящее время похоже на a -> [a], где функция возвращает вершины, доступные из ввода. Любая реализация дополнительно потребует, чтобы мы могли сравнить, если две вершины одинаковы, поэтому нам понадобится Eq a.

Следующий график является проблематичным и представляет почти всю трудность при решении задачи в целом:

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

При попытке достичь 4 из 1 существует цикл с участием 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)

Повторное использование функции step от "Ответ" в качестве определения проблемы вашего примера, мы могли бы получить длину кратчайшего пути с 4 по 5 из 11-этажного здания на fmap length $ shortestPath (step 11) 4 5. Это возвращает Just 3.

Рассмотрим конечный граф с v вершинами и e ребрами. Граф с v вершинами и e ребрами может быть описан входом размера n ~ O (v + e). График наихудшего случая для этого алгоритма состоит в том, чтобы иметь одну недостижимую вершину, j, а остальные вершины и ребра, связанные с созданием наибольшего числа ациклических путей, начинающихся с i. Вероятно, это что-то вроде клики, содержащей все вершины, которые не являются i или j, с ребрами от i до каждой другой вершины, которая не является j. Число вершин в клике с e ребрами O (e ^ (1/2)), поэтому этот график имеет e ~ O (n), v ~ O (n ^ (1/2)). Этот график имел бы O ((n ^ (1/2))!) Пути для изучения до определения того, что j недоступен.

Память, требуемая этой функцией для этого случая, равна O ((n ^ (1/2))!), так как она требует только постоянного увеличения очереди для каждого пути.

Время, требуемое этой функцией для этого случая, равно O ((n ^ (1/2))! * n ^ (1/2)). Каждый раз, когда он расширяет путь, он должен проверить, что новый node еще не находится в пути, который принимает время O (v) ~ O (n ^ (1/2)). Это можно было бы улучшить до O (log (n ^ (1/2))), если бы мы имели Ord a и использовали Set a или подобную структуру для хранения посещенных вершин.

Для не конечных графов эта функция должна заканчиваться не только тогда, когда не существует конечного пути от i до j, но существует не конечный путь от i до j.

Динамическое программирование

Решение динамического программирования не обобщает аналогичным образом; пусть исследуют, почему.

Для начала мы адаптируем решение хаосмасттера, чтобы иметь тот же интерфейс, что и наше решение поиска по ширине:

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 5 - 3. К сожалению, для нашей проблемной проблемы 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 100 является False и problematic 1 4 > fromInt 100 является True.

Далее, чтобы изучить динамическое программирование, нам нужно будет ввести некоторое динамическое программирование. Поскольку мы построим таблицу решений для всех под-проблем, нам нужно будет знать возможные значения, которые могут принимать вершины. Это дает нам несколько иной интерфейс:

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

Мы можем использовать это как shortestPath'' (step 11) (1,11) 4 5 или shortestPath'' problematic (1,4) 1 4 < fromInt 100. Это все еще не может обнаружить циклы...

Динамическое программирование и обнаружение циклов

Обнаружение цикла проблематично для динамического программирования, потому что подзадачи не совпадают, когда к ним обращаются с разных путей. Рассмотрим вариант нашей проблемы problematic.

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

Если мы пытаемся перейти от 1 к 4, у нас есть два варианта:

  • перейдите к 2 и возьмите кратчайший путь от 2 до 4
  • перейдите к 3 и возьмите кратчайший путь от 3 до 4

Если мы рассмотрим 2, мы столкнемся со следующей опцией:

  • перейдите к 3 и возьмите кратчайший путь от 3 до 4

Мы хотим объединить два исследования кратчайшего пути от 3 до 4 в одну и ту же запись в таблице. Если мы хотим избежать циклов, это действительно что-то немного более тонкое. Проблемы, с которыми мы столкнулись, были действительно:

  • перейдите к 2 и возьмите кратчайший путь от 2 до 4, который не посещает 1
  • перейдите к 3 и возьмите кратчайший путь от 3 до 4, который не посещает 1

После выбора 2

  • перейдите к 3 и возьмите кратчайший путь от 3 до 4, который не посещает 1 или 2

Эти два вопроса о том, как добраться от 3 до 4, имеют два немного разных ответа. Это две разные проблемы, которые не могут быть помещены в одно и то же место в таблице. Отвечая на первый вопрос, в конечном итоге необходимо определить, что вы не можете добраться до 4 от 2. Ответ на второй вопрос прост.

Мы могли бы создать кучу таблиц для каждого возможного набора ранее посещенных вершин, но это не очень эффективно. Я почти убедил себя, что мы не можем достичь достижимости как проблемы динамического программирования, используя только лень.

Обзор поиска по ширине

Во время работы над динамическим программным решением с возможностью обнаружения или обнаружения циклов я понял, что как только мы увидим node в параметрах, более поздний путь, посещающий этот node, может быть оптимальным, независимо от того, выполните node. Если мы передумаем problematic':

Если мы пытаемся перейти от 1 к 4, у нас есть два варианта:

  • перейдите к 2 и возьмите кратчайший путь от 2 до 4 без посещения 1, 2 или 3
  • перейдите к 3 и возьмите кратчайший путь от 3 до 4 без посещения 1, 2 или 3

Это дает нам алгоритм, позволяющий легко найти длину кратчайшего пути:

-- 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 5 - Just 3, а lengthShortestPath problematic 1 4 - Nothing.

В худшем случае generations требуется пространство, которое является O (v * log v), а время O (v * e * log v).

Ответ 2

Проблема заключается в том, что min необходимо полностью оценить оба вызова f, поэтому, если одна из них петли бесконечно min никогда не вернется. Таким образом, вам нужно создать новый тип, кодирующий, что число, возвращаемое f, равно нулю или преемнику нуля.

data Natural = Next Natural 
             | Zero

toNum :: Num n => Natural -> n
toNum Zero     = 0
toNum (Next n) = 1 + (toNum n)

minimal :: Natural -> Natural -> Natural
minimal Zero _            = Zero
minimal _ Zero            = Zero
minimal (Next a) (Next b) = Next $ minimal a b

f i j | i == j = Zero
      | otherwise = Next $ minimal (f l j) (f r j)
      where l = i + 2
            r = i - 3

Этот код действительно работает.

Ответ 3

стоящего на полу i здания n, найдите минимальное количество шагов, которые требуется, чтобы добраться до пола j, где

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

таким образом, мы имеем дерево. нам нужно искать его по ширине, пока мы не получим node, удерживая значение j. его глубина - это количество шагов. мы создаем очередь, несущую уровни глубины,

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

Функция gen d p берет свой вход p из d отрезками от своей производственной точки вдоль очереди вывода:

    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

Использует TupleSections. Здесь нет узлового узла, просто corecursion, т.е. (оптимистичное) форвардное производство и экономное исследование. Работает отлично без привязки узлов, потому что мы ищем только первое решение. Если бы мы искали несколько из них, нам нужно было бы как-то устранить циклы.

С обнаружением цикла:

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)

например. solution CD1 100 100 7 запускается мгновенно, создавая Just 31. Список visited в значительной степени является копией созданного префикса самой очереди. Его можно сохранить как карту, чтобы улучшить временную сложность (как это, sol 10000 10000 7 => Just 3331 занимает 1,27 секунды на Ideone).


Некоторые объяснения выглядят в порядке.

Во-первых, в вашей проблеме нет ничего 2D, потому что целевой пол j исправлен.

То, что вам кажется нужным, - это memoization, как показывает ваше последнее изменение. Воспоминание полезно для рекурсивных решений; ваша функция действительно рекурсивна, анализируя ее аргумент в подсерии, синтезируя его результат от результатов вызова себя в подсериях (здесь i+2 и i-3), которые ближе к базовому случаю (здесь i==j).

Поскольку арифметика является строгой, ваша формула расходится в присутствии любого бесконечного пути в дереве шагов (от пола до пола). Ответ chaosmasttter, используя вместо этого ленивую арифметику, автоматически превращает его в алгоритм поиска в ширину, который расходится только в том случае, если в дереве нет конечных путей, мое первое решение выше (за исключением того факта, что он не проверяет индексы вне пределов). Но он все еще рекурсивный, так что действительно требуется мемуаризация.

Обычный способ приблизиться к нему в первую очередь заключается в том, чтобы вводить общий доступ путем "перехода через список" (неэффективно из-за последовательного доступа, для эффективных решений для memoization см. 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 enough floors in the building
        | l < 1  = Next (gs !! r)
        | otherwise = Next $ minimal (gs !! l) (gs !! r)
      where r = i + 2
            l = i - 3

не проверен.

Мое решение является корекурсивным. Он не нуждается в memoization (просто нужно быть осторожным с дубликатами), потому что он генеративный, как и динамическое программирование. Он отходит от начального случая, то есть начального этажа. Внешний аксессор выбирает соответствующий сгенерированный результат.

Он связывает узел - он определяет queue, используя его - queue находится по обе стороны от уравнения. Я считаю это более простым случаем связывания узлов, потому что он просто обращается к ранее сгенерированным значениям, замаскированным.

Связывание узлов второго рода, более сложного, обычно связано с добавлением некоторого еще undefined значения в некоторой структуре данных и возвратом его для определения некоторой более поздней частью кода (например, указатель ссылки в двусвязном круговом списке); это действительно не то, что делает мой код 1. То, что он делает, это генерация очереди, добавление в ее конце и "удаление" с ее фронта; в конце концов это просто метод списка разметки Prolog, открытый список с его конечным указателем, поддерживаемый и обновляемый, строковое построение списка хвостовая рекурсия modulo cons - все это концептуально. Сначала описано (хотя и не указано) в 1974 году, AFAIK.


1 полностью основан на коде из Wikipedia.

Ответ 4

Другие ответили на ваш прямой вопрос о динамическом программировании. Однако для такого рода проблем я считаю, что жадный подход работает лучше всего. Реализация очень проста.

f i j :: Int -> Int -> Int
f i j = snd $ until (\(i,_) -> i == j) 
                    (\(i,x) -> (i + if i < j then 2 else (-3),x+1))
                    (i,0)