最大化序列中数字之间的差异

时间:2013-03-14 20:44:03

标签: algorithm language-agnostic recurrence

我需要一些帮助来找到算法的一般思路来解决以下问题。在任务中给了我这个问题。看起来它应该可以通过贪婪的方法解决,但我无法找到一个简单的解决方案。这是问题描述:

您将获得 N 数字a_1 ... a_n的序列,以便0 = a_1 < a_2 < ... < a_n。您必须消除这些数字的最多 M ,以便最大化任意两个连续数字之间的最小差异a_i+1 - a_i

您可能无法消除第一个和最后一个元素a_0a_n。此外,您必须消除尽可能少的数字:如果消除M - 1您获得D的最短距离并消除M您仍然具有相同的最小差异,则您不能消除此最后一个号。

我不是要求完全解决这个问题。关于算法的外观,只有一些指导。

编辑:部分测试样本。请记住,可能有多种有效的解决方案。

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 个答案:

答案 0 :(得分:5)

[编辑:我最初声称ElKamina's answer错了,但我现在已经说服自己,这不仅是正确的,而且与我(后来的)几乎相同回答:-P虽然我的口味还有点简洁!]

这是一个精确的 O(NM ^ 2) - 时间,O(NM) - 空间 dynamic programming算法,可以在所有OP&#39;以毫秒为单位的示例。基本的想法是:

  1. 每当我们施加约束时,某个特定数字应该被删除,它就会形成一个&#34; fence&#34;两个子问题之间的解决方案是,解决每个子问题最佳地保证了对该约束的整体问题的最佳解决方案,并且
  2. 每个最佳解决方案必须以未删除的数字开头,后跟一些连续删除的数字,后跟未删除的数字,然后是从第二个非开始的问题的其余部分的最佳解决方案删除号码并使用适当减少的M。
  3. 在下文中,x [i]表示列表中的第i个数字,索引从0开始。

    递归

    令f(i,j)是从位置0 <= i <1开始的数字列表的后缀可获得的最佳(最大最小)间隔大小。 N通过保持这个(即第i个)数并准确删除后来(不一定是连续的)数字的j。以下递归显示了如何计算:

    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来防止删除&#34;超过结束&#34;。我们需要的唯一基本情况是:

    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开始的连续删除的所有可能数(包括零),在每种情况下计算(a)的最小值由这个删除块形成的间隔和(b)从该块右边的第一个未删节数开始的子问题的最优解。 指定块(x [i])中的第一个数字不被删除是很重要的,这样前一个(父)子问题的间隔总是&#34;上限&#34 ; 这是一个棘手的部分,花了我一段时间来弄明白。

    加快速度

    如果您对上面的普通递归进行编码,它将起作用,但在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,j)的最优解。 (也许&#34;前任&#34;在这里令人困惑:它指的是当前子问题之前解决的子问题,尽管这些子问题出现在&#34;之后#34;(在右边) of)代表当前子问题的后缀。)然后可以遵循这些链接以恢复删除/不删除决定。

    工作C ++代码

    http://ideone.com/PKfhDv

    附录:早期失误

    有了这样一个棘手的问题,看看错误的方法并确切地看出他们出错的地方会有所帮助......:/ /我以为我解决了这个问题,但我没有注意到要求返回尽可能少删除的解决方案,而我最初尝试解决这个问题并不起作用。

    首先,我尝试将f(i,j)定义为从位置0&lt; = i&lt; 0开始的数字列表的后缀可获得的最佳(最大最小)间隔大小。 N通过保持该(即第i个)数字并删除从后到(不一定是连续)数字的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删除。

答案 1 :(得分:4)

使用动态编程。

线索X(i,j)包含与第一个i元素的最小距离,其中j被选中(即未删除)。

这将为您提供准确的解决方案。复杂度= O(MN ^ 2),因为对于每个i值,只有m个有效值为j,并且每次调用该函数都需要进行O(M)工作。

让元素为A1,A2,...,An

更新公式为:

X(i + 1,j + 1)=最大(Min(A(i + 1)-Ak,Xkj),k <= i)

[由j_random_hacker编辑以添加评论中的信息]

答案 2 :(得分:1)

我想我得到了解决方案。它至少适用于两个样本集。它不一定返回与答案相同的集合,但它返回的集合具有相同的最小值。它也是迭代和贪婪的,这很好,并且有很多方法可以优化它。看起来像是MLog(N)。

重要的是要意识到数字无关紧要 - 只有它们之间的差异才有意义。当你“删除一个数字”时,你实际上只是将两个相邻的差异结合起来。我的算法将关注于差异。回到哪些项目导致这些差异并随时删除是一件简单的事情。

算法

  1. 创建每个数字之间差异的有序列表/数组。
  2. 找出最低差异 x 。如果计算 x &gt;剩下的M,停下来。你已经处于最佳状态。
  3. 对于从最左边开始的 x 的每个值,将该差异与较低的邻居组合(并删除 x )。如果邻居具有相同的价值,您的决定是任意的。如果只有一个邻居的值为 x ,则与另一个邻居组合。 (如果您别无选择,例如[1,1,...],请与匹配的 X 结合使用,但如果可以,请避免使用。)
  4. 返回第2步,直到用完 M
  5. 算法说明

    第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] M = 5

    接下来,删除5:[7,8,11,12,6,9,8,15,12,12] M = 4

    接下来,删除6:[7,8,11,12,15,8,15,12,12] M = 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] M = 2 //注意任意添加到右边

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

    我们会删除5的下一个,但只允许删除1,并且有3,所以我们在这里停止。我们的最低差异是5,与解决方案相匹配。

    注意:根据SauceMaster提供的1,29,30,31,59案例编辑,将相同的 X 值组合起来以避免这样做。 / p>

答案 3 :(得分:1)

我希望不使用全组合方法,但经过多次尝试后,似乎是将我的结果与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.


哈斯克尔代码:

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