Эффективная таблица для динамического программирования на Haskell

Я написал проблему с рюкзаком 0-1 на Haskell. Я довольно горжусь ленью и уровнем общности, достигнутым до сих пор.

Я начну с предоставления функций для создания и работы с ленивой 2D-матрицей.

mkList f = map f [0..]
mkTable f = mkList (\i -> mkList (\j -> f i j))

tableIndex table i j = table !! i !! j

Затем я делаю специальную таблицу для данной задачи о рюкзаке.

knapsackTable = mkTable f
    where f 0 _ = 0
          f _ 0 = 0
          f i j | ws!!i > j = leaveI
                | otherwise = max takeI leaveI
              where takeI  = tableIndex knapsackTable (i-1) (j-(ws!!i)) + vs!!i
                    leaveI = tableIndex knapsackTable (i-1) j

-- weight value pairs; item i has weight ws!!i and value vs!!i
ws  = [0,1,2, 5, 6, 7] -- weights
vs  = [0,1,7,11,21,31] -- values

И завершите парой вспомогательных функций для просмотра таблицы.

viewTable table maxI maxJ = take (maxI+1) . map (take (maxJ+1)) $ table
printTable table maxI maxJ = mapM_ print $ viewTable table maxI maxJ

Это было довольно легко. Но я хочу сделать еще один шаг.

Мне нужна лучшая структура данных для таблицы. В идеале она должна быть

  • Unboxed (неизменяемый) [edit] не обращайте внимания на это
  • Ленивый
  • Неограниченный
  • O(1) пора строить
  • O(1) временная сложность поиска данной записи,
    (более реалистично, в худшем случае O(log n), где n равно i*j для поиска записи в строке i, столбце j)

Бонусные баллы, если вы сможете объяснить, почему/как ваше решение удовлетворяет этим идеалам.

Также бонусные баллы, если вы сможете еще больше обобщить knapsackTable и доказать, что это эффективно.

При улучшении структуры данных вы должны попытаться достичь следующих целей:

  • Если я попрошу решение, в котором максимальный вес равен 10 (в моем текущем коде это будет indexTable knapsackTable 5 10, 5 означает, что включают элементы 1-5), должен быть выполнен только минимальный объем необходимой работы. В идеале это означает отсутствие O(i*j) работы по принуждению корешка каждой строки таблицы к необходимой длине столбца. Вы могли бы сказать, что это не "истинный" DP, если вы считаете, что DP означает оценку всей таблицы.
  • Если я попрошу напечатать всю таблицу (что-то вроде printTable knapsackTable 5 10), значения каждой записи должны быть вычислены один раз и только один раз. Значения данной ячейки должны зависеть от значений других ячеек (стиль DP: идея заключается в том, чтобы никогда не пересчитывать одну и ту же подзадачу дважды)

Идеи:

Ответы, которые идут на компромисс с заявленными мною идеалами, будут проголосованы (в любом случае мной), если они информативны. Ответ с наименьшими компромиссами, вероятно, будет «принятым».


person Dan Burton    schedule 07.03.2011    source источник
comment
Обратите внимание, что unboxed означает нечто отличное от неизменяемого; нельзя распаковываться и лениться одновременно.   -  person Edward Z. Yang    schedule 07.03.2011
comment
@ Эдвард и ответившие до сих пор: спасибо; вопрос отредактирован, чтобы вычеркнуть без коробки.   -  person Dan Burton    schedule 08.03.2011
comment
Я нахожу mkTable f = mkList (mkList . f) более читаемым.   -  person yairchu    schedule 09.03.2011


Ответы (5)


Во-первых, ваш критерий для неупакованной структуры данных, вероятно, немного вводит в заблуждение. Неупакованные значения должны быть строгими и не имеют ничего общего с неизменностью. Решение, которое я собираюсь предложить, является неизменным, ленивым и упакованным. Кроме того, я не уверен, каким образом вы хотите, чтобы построение и запрос были O (1). Предлагаемая мной структура строится лениво, но поскольку она потенциально неограниченна, ее полное построение займет бесконечное время. Запрос структуры займет O(k) времени для любого конкретного ключа размера k, но, конечно, для вычисления значения, которое вы ищете, может потребоваться дополнительное время.

Структура данных — ленивая попытка. В своем коде я использую библиотеку MemoTrie Конала Эллиотта. Для универсальности он принимает функции вместо списков для весов и значений.

knapsack :: (Enum a, Num w, Num v, Num a, Ord w, Ord v, HasTrie a, HasTrie w) =>
            (a -> w) -> (a -> v) -> a -> w -> v
knapsack weight value = knapsackMem
  where knapsackMem = memo2 knapsack'
        knapsack' 0 w = 0
        knapsack' i 0 = 0
        knapsack' i w
          | weight i > w = knapsackMem (pred i) w
          | otherwise = max (knapsackMem (pred i) w)
                        (knapsackMem (pred i) (w - weight i)) + value i

По сути, он реализован как три с ленивым позвоночником и ленивыми значениями. Он ограничен только типом ключа. Поскольку все это лениво, его конструкция до форсирования запросов - O (1). Каждый запрос вызывает один путь вниз по дереву и его значение, поэтому O(1) для ограниченного размера ключа O(log n). Как я уже сказал, он неизменяемый, но не распакованный.

Он разделит всю работу в рекурсивных вызовах. На самом деле это не позволяет вам печатать trie напрямую, но что-то вроде этого не должно выполнять лишней работы:

mapM_ (print . uncurry (knapsack ws vs)) $ range ((0,0), (i,w))
person Jake McArthur    schedule 07.03.2011
comment
O (1) для ограниченного размера ключа — это мило, но на самом деле это O (log n), не так ли? Что лучше отметить, поскольку O (2 ^ n) также O (1) для ограниченного n :-) - person sclv; 08.03.2011
comment
Если вы хотите быть педантичным, это O (k), поскольку k - это размер ключа. Поскольку ключи в этом случае, вероятно, представляют собой машинные целые числа или что-то в этом роде, k на самом деле является константой, а не просто ограниченной. Кроме того, это O (1) в количестве элементов, если вы игнорируете размер ключа, как мы обычно измеряем контейнеры. Несправедливо говорить, что дерево равно O(log n), когда мы также говорим, что сбалансированное дерево равно O(log n). Если бы мы учитывали размер ключа, сбалансированное дерево на самом деле было бы O (k log n), и если бы мы считали коэффициент k логарифмическим по количеству элементов, как вы предлагаете, дерево было бы O (log n log n) . - person Jake McArthur; 08.03.2011
comment
В порядке. Моя перхоть сейчас :-). MemoTrie преобразует слова/целые/целые числа в списки битов с прямым порядком байтов. Функция битов создает список длиной O(log n). см.: codepad.org/BlRqzJKL. Возможны тройные представления, которые не похожи на это. Тот, который Конал использует по уважительной причине (например, для обработки неограниченных целых чисел), однако это так. - person sclv; 08.03.2011
comment
Однако я допускаю, что мы должны говорить в терминах k, а не n, хотя я считаю их альфа-эквивалентными. - person sclv; 08.03.2011
comment
И пока я настаиваю на этом, воспоминания по своей конструкции плотные, поэтому максимальный размер ключа k эквивалентен размеру контейнера n. - person sclv; 08.03.2011
comment
Вы меня зацепили последним комментарием. Ты прав. Поскольку они плотные, k = log n. - person Jake McArthur; 08.03.2011

Unboxed подразумевает строгость и ограниченность. Все, что на 100% не упаковано, не может быть ленивым или неограниченным. Обычный компромисс воплощается в преобразовании [Word8] в Data.ByteString.Lazy, где есть неупакованные фрагменты (strict ByteString), которые лениво связаны друг с другом неограниченным образом.

Гораздо более эффективный генератор таблиц (улучшенный для отслеживания отдельных элементов) можно сделать с помощью «scanl», «zipWith» и моего «takeOnto». Это эффективно позволяет избежать использования (!!) при создании таблицы:

import Data.List(sort,genericTake)

type Table = [ [ Entry ] ]

data Entry = Entry { bestValue :: !Integer, pieces :: [[WV]] }
  deriving (Read,Show)

data WV = WV { weight, value :: !Integer }
  deriving (Read,Show,Eq,Ord)

instance Eq Entry where
  (==) a b = (==) (bestValue a) (bestValue b)

instance Ord Entry where
  compare a b = compare (bestValue a) (bestValue b)

solutions :: Entry -> Int
solutions = length . filter (not . null) . pieces

addItem :: Entry -> WV -> Entry
addItem e wv = Entry { bestValue = bestValue e + value wv, pieces = map (wv:) (pieces e) }

-- Utility function for improve
takeOnto :: ([a] -> [a]) -> Integer -> [a] -> [a]
takeOnto endF = go where
  go n rest | n <=0 = endF rest
            | otherwise = case rest of
                            (x:xs) -> x : go (pred n) xs
                            [] -> error "takeOnto: unexpected []"

improve oldList wv@(WV {weight=wi,value = vi}) = newList where
  newList | vi <=0 = oldList
          | otherwise = takeOnto (zipWith maxAB oldList) wi oldList
  -- Dual traversal of index (w-wi) and index w makes this a zipWith
  maxAB e2 e1 = let e2v = addItem e2 wv
                in case compare e1 e2v of
                     LT -> e2v
                     EQ -> Entry { bestValue = bestValue e1
                                 , pieces = pieces e1 ++ pieces e2v }
                     GT -> e1

-- Note that the returned table is finite
-- The dependence on only the previous row makes this a "scanl" operation
makeTable :: [Int] -> [Int] -> Table
makeTable ws vs =
  let wvs = zipWith WV (map toInteger ws) (map toInteger vs)
      nil = repeat (Entry { bestValue = 0, pieces = [[]] })
      totW = sum (map weight wvs)
  in map (genericTake (succ totW)) $ scanl improve nil wvs

-- Create specific table, note that weights (1+7) equal weight 8
ws, vs :: [Int]
ws  = [2,3, 5, 5, 6, 7] -- weights
vs  = [1,7,8,11,21,31] -- values

t = makeTable ws vs

-- Investigate table

seeTable = mapM_ seeBestValue t
  where seeBestValue row = mapM_ (\v -> putStr (' ':(show (bestValue v)))) row >> putChar '\n'

ways = mapM_ seeWays t
  where seeWays row = mapM_ (\v -> putStr (' ':(show (solutions v)))) row >> putChar '\n'

-- This has two ways of satisfying a bestValue of 8 for 3 items up to total weight 5
interesting = print (t !! 3 !! 5) 
person Chris Kuklewicz    schedule 07.03.2011

Ленивые сохраняемые векторы: http://hackage.haskell.org/package/storablevector

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

person sclv    schedule 07.03.2011

Для запоминания функций я рекомендую такую ​​библиотеку, как memo combinators Люка Палмера. Библиотека использует попытки, которые не ограничены и имеют поиск O (размер ключа). (В общем, вы не можете сделать лучше, чем поиск O (размер ключа), потому что вам всегда нужно касаться каждого бита ключа.)

knapsack :: (Int,Int) -> Solution
knapsack = memo f
    where
    memo    = pair integral integral
    f (i,j) = ... knapsack (i-b,j) ...


Внутри комбинатор integral, вероятно, создает бесконечную структуру данных.

data IntTrie a = Branch IntTrie a IntTrie

integral f = \n -> lookup n table
     where
     table = Branch (\n -> f (2*n)) (f 0) (\n -> f (2*n+1))

Поиск работает следующим образом:

lookup 0 (Branch l a r) = a
lookup n (Branch l a r) = if even n then lookup n2 l else lookup n2 r
     where n2 = n `div` 2

Есть и другие способы создания бесконечных попыток, но этот популярен.

person Heinrich Apfelmus    schedule 08.03.2011

Почему бы вам не использовать Data.Map, вставив в него другой Data.Map? Насколько я знаю, это довольно быстро. Хотя было бы не лень.

Более того, вы можете реализовать класс типов Ord для своих данных.

data Index = Index Int Int

и поместите двумерный индекс непосредственно в качестве ключа. Вы можете добиться лени, создав эту карту в виде списка, а затем просто используя

fromList [(Index 0 0, value11), (Index 0 1, value12), ...] 
person pechenie    schedule 07.03.2011
comment
Ну, а если быть точным, то это был бы позвоночник строгий, но ленивый в элементах. - person sclv; 07.03.2011
comment
И в этом отношении вам действительно нужно только Map (Int, Int) a, и если ваши максимальные i и j равны ‹ sqrt (maxBound :: Int), вы можете спроецировать их на один Int обычным способом и использовать IntMap... - person sclv; 07.03.2011