Максимизация разности между числами в последовательности

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

вам дается последовательность N цифры a_1 ... a_n такое, что 0 = a_1 < a_2 < ... < a_n. Вы должны устранить максимум M из этих чисел таких, что минимальная разница a_i+1 - a_i между любыми двумя подряд чисел является максимальным.

вы не можете исключить первый и последний элементы, a_0 и a_n. Также вы должны устранить как можно меньше чисел: если убрать M - 1 вы получаете кратчайшее расстояние, чтобы быть D и исключения M вы все еще имеете такую же минимальную разницу, вы не должны исключить это последнее число.

Я не прошу полного решения этой проблемы. Только несколько рекомендаций относительно того, как может выглядеть алгоритм.

Edit: несколько пробных образцов. Имейте в виду, что может быть несколько допустимых решений.

Remove at most 7 from:
0 3 7 10 15 18 26 31 38 44 53 60 61 73 76 80 81 88 93 100

Solution:
0 7 15 26 31 38 44 53 60 73 80 88 93 100
Remove at most 8 from:
0 3 7 10 15 26 38 44 53 61 76 80 88 93 100

Solution:
0 15 38 53 76 88 100

4 ответов


[EDIT: я первоначально утверждал, что ElKamina это было неправильно, но теперь я убедил себя, что это не только правильно, это практически то же самое, что и мой (более поздний) ответ :-P все еще немного лаконичный на мой вкус!]

вот точно O (NM^2)-Время, O(NM) - пространство динамическое программирование алгоритм, который получает правильный ответ на все примеры OP в миллисекундах. Основные идеи что:

  1. всякий раз, когда мы вводим ограничение, что определенное число должно не быть удалены, он образует "забор" между двумя подзадачами таким образом, что решение каждой подзадачи оптимально гарантирует оптимальное решение общей проблемы с этим ограничением на месте, и
  2. каждое оптимальное решение должно начинаться с номера без удаления, за которым следует некоторое количество последовательных удаленных номеров, за которыми следует номер без удаления, а затем оптимальным решением оставшейся части задачи, которая начинается со второго номера без удаления и использует соответствующим образом уменьшенный M.

в следующем x[i] означает I-е число в списке с индексированием, начинающимся с 0.

рекурсия

пусть f (i, j) - оптимальный (самый большой минимальный) размер интервала, доступный из суффикса списка чисел, начинающегося с позиции 0

f(i, j) = max(g(i, j, d)) over all 0 <= d <= min(j, N-i-2)
g(i, j, d) = min(x[i+d+1] - x[i], f(i+d+1, j-d))

на min(j, N-i-2) есть ли вместо простого j, чтобы предотвратить удаление "за конец". Единственные базовые случаи, которые нам нужны:

f(N-1, 0) = infinity (this has the effect of making min(f(N-1), 0), z) = z)
f(N-1, j > 0) = 0 (this case only arises if M > N - 2)

как это работает

более подробно, чтобы вычислить f (i, j), мы делаем цикл над всеми возможными числами (включая ноль) последовательных удалений, начиная с позиции i+1, в каждом случае вычисляя минимум (а) интервала, образованного этим блоком удалений, и (Б) оптимального решения подзадачи, начинающейся с первого неотделенного числа справа от этого блока. важно указать, что первое число в блоке (x[i]) не удаляется, так что интервал предыдущей (родительской) подзадачи всегда "ограничен". это сложная часть, которая заняла у меня некоторое время, чтобы понять.

сделать это быстро

если код простой рекурсии выше он будет работать, но это займет время экспоненциально в M и N. На memoising f (), мы гарантируем, что его тело будет работать не более N * M раз (один раз за отдельную комбинацию параметров). Каждый раз, когда функция запускается, она выполняет O(M) сканирование работы через все более длинные блоки удалений в течение o(NM^2) времени в целом.

вы не можете создать больший разрыв, используя меньше удалений, поэтому общий самый большой минимальный размер интервала можно найти, просматривая M+1 результаты f (0, M), f(0, M-1),..., f (0, 0) для первого числа, меньшего, чем предыдущее число: это предыдущее число является ответом, а второй аргумент f () - минимальное количество необходимых удалений. Чтобы найти оптимальное решение (т. е. список конкретных чисел, удаленных), вы можете записать решения, принятые в отдельном массиве предшественников, так что p[i, j] дает значение d (которое может быть превращено в предыдущие значения i и j), что привело к оптимальному решению для f (i, Дж.) (Возможно, "предшественник" здесь запутан: он относится к подзадачам, которые решаются до текущая подзадача, хотя эти подзадачи появляются "после" (справа) суффикса, представляющего текущую подзадачу.) Затем эти ссылки можно использовать для восстановления принятых решений "удалить/не удалять".

рабочий код C++

http://ideone.com/PKfhDv

добавление: ранние оплошности

С a такая сложная проблема, как это, может быть полезно посмотреть на неправильные подходы и увидеть, где именно они пошли не так... :- /Я думал, что решил проблему, но я не заметил требования вернуть решение, которое использует как можно меньше удалений, и мои первоначальные попытки исправить это не сработали.

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

Problem: M = 1, X = [10 15 50 55].

f(2, 0) = (5, 0) (leaving [50 55])
f(1, 1) = (40, 1) (delete 50 to leave [15 55]); *locally* this appears better
          than not deleting anything, which would leave [15 50 55] and yield
          a min-gap of 5, even though the latter would be a better choice for
          the overall problem)
f(0, 1) = max(min(5, f(1, 1)), min(40, f(2, 0))
        = max(min(5, 40), min(40, 5))
        = (5, 1) (leaving either [10 15 55] or [10 50 55])

Я не показал работу для второго элемента пары, возвращаемой f (0, 1), потому что трудно выразить кратко, но, очевидно, это будет 1, потому что обе альтернативы пытались удалить 1.


использовать динамическое программирование.

Clue X (i,j) содержит минимальное расстояние с первыми I элементами и среди них J выбран (т. е. не удален).

Это даст вам точное решение. Сложность = O(MN^2), потому что для каждого значения i существует только M допустимых значений j, и каждый вызов функции должен выполнять O (M) работу.

пусть элементы A1, A2,...Ан

формула для обновления:

X (i+1,j+1) = Max(Min(A (i+1)-Ak, Xkj) для k

[отредактировано j_random_hacker для добавления информации из комментариев]


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

важно понять, что числа не имеют значения - только различия между ними имеют значение. Когда вы "удаляете число", вы на самом деле просто объединяете два соседних отличия. Тогда мой алгоритм сфокусируется на различиях. Это простой вопрос, чтобы вернуться к тому, какие элементы вызвали эти различия и удалить, как вы идете.

алгоритм

  1. создайте упорядоченный список / массив различий между каждым числом.
  2. найти наименьшую разницу x. Если граф x > оставшийся м, стоп. Ты уже в лучшем случае.
  3. для каждого значения x начиная с самого левого, объедините эту разницу с тем, какой сосед ниже (и удалите это x). Если соседи имеют равные значения, ваше решение является произвольным. Если только один сосед имеет значение x, совместить с другим соседом. (Если у вас нет выбора, например, [1, 1, ...], затем объедините с соответствующим X, но избегайте этого, если сможете.)
  4. вернитесь к Шагу 2, пока не закончится M.

заметки по алгоритму

Шаг 3 имеет точку, которую я обозначил как произвольное решение. Вероятно, это не должно быть, но вы попадаете в Эджи достаточно случаев, что вопрос в том, сколько сложности вы хотите добавить. Этот произвол-это то, что позволяет получить несколько разных правильных ответов. Если вы видите двух соседей, которые имеют одинаковое значение, в этот момент я говорю произвольно выбрать один. В идеале, вы, вероятно, должны проверить пара соседей, которые находятся на расстоянии 2, затем 3 и т. д., и предпочитают Нижний. Я не уверен, что делать, если вы нажмете край при расширении. В конечном счете, чтобы сделать эту часть идеально, вам может потребоваться рекурсивно вызвать эту функцию и посмотреть, какая оценка лучше.

прогулка по образцам данных

второе:

удалить не более 8 из: 0 3 7 10 15 26 38 44 53 61 76 80 88 93 100

[3, 4, 3, 5, 11, 12, 6, 9, 8, 15, 4, 8, 5, 7] M = 8

удалите 3. Удаления по краям можно добавлять только в одном направлении:

[7, 3, 5, 11, 12, 6, 9, 8, 15, 4, 8, 5, 7] M = 7

[7, 8, 11, 12, 6, 9, 8, 15, 4, 8, 5, 7] M = 6

далее снять 4: [7, 8, 11, 12, 6, 9, 8, 15, 12, 5, 7] м = 5

далее снять 5: [7, 8, 11, 12, 6, 9, 8, 15, 12, 12] М = 4

далее снять 6: [7, 8, 11, 12, 15, 8, 15, 12, 12] м = 3

далее удалите 7: [15, 11, 12, 15, 8, 15, 12, 12] M = 2

далее удалите 8: [15, 11, 12, 15, 23, 12, 12] M = 1 // Обратите внимание, произвольное решение о направлении добавления

наконец, удалите 11: [15, 23, 15, 23, 12, 12]

обратите внимание, что в ответе наименьшая разница равна 12.

первый

удалить не более 7 из: 0 3 7 10 15 18 26 31 38 44 53 60 61 73 76 80 81 88 93 100

[3, 4, 3, 5, 3, 8, 5, 7, 6, 9, 7, 1, 12, 3, 4, 1, 7, 5, 7] M = 7

снять 1-это:

[3, 4, 3, 5, 3, 8, 5, 7, 6, 9, 8, 12, 3, 4, 1, 7, 5, 7] M = 6

[3, 4, 3, 5, 3, 8, 5, 7, 6, 9, 8, 12, 3, 5, 7, 5, 7] M = 5

осталось 4 3, поэтому мы можем удалить их:

[7, 3, 5, 3, 8, 5, 7, 6, 9, 8, 12, 3, 5, 7, 5, 7] M = 4

[7, 8, 3, 8, 5, 7, 6, 9, 8, 12, 3, 5, 7, 5, 7] M = 3

[7, 8, 11, 5, 7, 6, 9, 8, 12, 3, 5, 7, 5, 7] м = 2 // внимание произвольное добавление справа

[7, 8, 11, 5, 7, 6, 9, 8, 12, 8, 5, 7, 5, 7] M = 1

мы удалили бы следующие 5, но разрешено удалять только 1 и иметь 3, поэтому мы останавливаемся здесь. Наша самая низкая разница 5, соответствуя решению.

Примечание: отредактировано из идеи объединения же X значения, чтобы избежать этого, для случая 1, 29, 30, 31, 59, представленного SauceMaster.


Я надеялся не использовать подход всех комбинаций, но после нескольких попыток, это казалось единственным способом сопоставить мои результаты с j_random_hacker. (Некоторые из комментариев ниже относятся к более ранним версиям этого ответа.) Я впечатлен тем, как лаконично алгоритм j_random_hacker/ElKamina выражен в Haskell ('jrhMaxDiff'). Его функция, compareAllCombos, ищет различия в результатах наших двух методов:

*Main> compareAllCombos 7 4 4
Nothing


в алгоритм:

1. Group the differences: [0, 6, 11, 13, 22] => [[6],[5],[2],[9]]

2. While enough removals remain to increase the minimum difference, extend the 
   minimum difference to join adjacent groups in all possible ways:

   [[6],[5],[2],[9]] => [[6],[5,2],[9]] and [[6],[5],[2,9]]...etc.

   Choose the highest minimum difference and lowest number of removals.


Haskell-кода:

import Data.List (minimumBy, maximumBy, groupBy, find)
import Data.Maybe (fromJust)

extendr ind xs = 
  let splitxs = splitAt ind xs
      (y:ys) = snd splitxs
      left = snd y
      right = snd (head ys)
  in fst splitxs ++ [(sum (left ++ right), left ++ right)] ++ tail ys

extendl ind xs = 
  let splitxs = splitAt ind xs
      (y:ys) = snd splitxs
      right = snd y
      left = snd (last $ fst splitxs)
  in init (fst splitxs) ++ [(sum (left ++ right), left ++ right)] ++ tail (snd splitxs)

extend' m xs =
  let results = map (\x -> (fst . minimumBy (\a b -> compare (fst a) (fst b)) $ x, x)) (solve xs)
      maxMinDiff = fst . maximumBy (\a b -> compare (fst a) (fst b)) $ results
      resultsFiltered = filter ((==maxMinDiff) . fst) results
  in minimumBy (\a b -> compare (sum (map (\x -> length (snd x) - 1) (snd a))) (sum (map (\x -> length (snd x) - 1) (snd b)))) resultsFiltered
   where 
     solve ys = 
       let removalCount = sum (map (\x -> length (snd x) - 1) ys)
           lowestElem = minimumBy (\a b -> compare (fst a) (fst b)) ys
           lowestSum = fst lowestElem
           lowestSumGrouped = 
             map (\x -> if (fst . head $ x) == 0 
                           then length x 
                           else if null (drop 1 x) 
                                   then 1 
                                   else if odd (length x)
                                           then div (length x + 1) 2
                                           else div (length x) 2)
             $ filter ((==lowestSum) . fst . head) (groupBy (\a b -> (fst a) == (fst b)) ys)
           nextIndices = map snd . filter ((==lowestSum) . fst . fst) $ zip ys [0..]
           lastInd = length ys - 1
       in if sum lowestSumGrouped > m - removalCount || null (drop 1 ys)
             then [ys]
             else do
               nextInd <- nextIndices          
               if nextInd == 0
                  then solve (extendl (nextInd + 1) ys)
                  else if nextInd == lastInd
                          then solve (extendr (nextInd - 1) ys)
                          else do 
                            a <- [extendl nextInd ys, extendr nextInd ys]
                            solve a

pureMaxDiff m xs = 
  let differences = map (:[]) $ tail $ zipWith (-) xs ([0] ++ init xs)
      differencesSummed = zip (map sum differences) differences
      result = extend' m differencesSummed
      lowestSum = fst result
      removalCount = sum (map (\x -> length (snd x) - 1) (snd result))
  in if null (filter ((/=0) . fst) differencesSummed)
        then (0,0)
        else (removalCount, lowestSum)

-- __j_random_hacker's stuff begins here

-- My algorithm from http://stackoverflow.com/a/15478409/47984.
-- Oddly it seems to be much faster when I *don't* try to use memoisation!
-- (I don't really understand how memoisation in Haskell works yet...)
jrhMaxDiff m xs = fst $ fromJust $ find (\(x, y) -> snd x > snd y) resultPairsDesc
  where
    inf = 1000000
    n = length xs
    f i j =
      if i == n - 1
         then if j == 0
                 then inf
                 else 0
         else maximum [g i j d | d <- [0 .. min j (n - i - 2)]]
    g i j d = min ((xs !! (i + d + 1)) - (xs !! i)) (f (i + d + 1) (j - d))
    resultsDesc = map (\i -> (i, f 0 i)) $ reverse [0 .. m]
    resultPairsDesc = zip resultsDesc (concat [(tail resultsDesc), [(-1, -1)]])

-- All following code is for looking for different results between my and groovy's algorithms.
-- Generate a list of all length-n lists containing numbers in the range 0 - d.
upto 0 _ = [[]]
upto n d = concat $ map (\x -> (map (\y -> (x : y)) (upto (n - 1) d))) [0 .. d]

-- Generate a list of all length-maxN or shorter lists containing numbers in the range 0 - maxD.
generateAllDiffCombos 1 maxD = [[x] | x <- [0 .. maxD]]
generateAllDiffCombos maxN maxD =
  (generateAllDiffCombos (maxN - 1) maxD) ++ (upto maxN maxD)

diffsToNums xs = scanl (+) 0 xs

generateAllCombos maxN maxD = map diffsToNums $ generateAllDiffCombos maxN maxD

-- generateAllCombos causes pureMaxDiff to produce an error with (1, [0, 0]) and (1, [0, 0, 0]) among others,
-- so filter these out to look for more "interesting" differences.
--generateMostCombos maxN maxD = filter (\x -> length x /= 2) $ generateAllCombos maxN maxD
generateMostCombos maxN maxD = filter (\x -> length x > 4) $ generateAllCombos maxN maxD

-- Try running both algorithms on every list of length up to maxN having gaps of
-- size up to maxD, allowing up to maxDel deletions (this is the M parameter).
compareAllCombos maxN maxD maxDel =
  find (\(x, maxDel, jrh, grv) -> jrh /= grv) $ map (\x -> (x, maxDel, jrhMaxDiff maxDel x, pureMaxDiff maxDel x)) $ generateMostCombos maxN maxD
--  show $ map (\x -> (x, jrhMaxDiff maxDel x, pureMaxDiff maxDel x)) $ generateMostCombos maxN maxD