【问题标题】:Find min. "join" operations for sequence找到最小值。序列的“加入”操作
【发布时间】:2011-05-28 00:43:00
【问题描述】:

假设我们有一个由正整数 x1、x2、...、xn 组成的列表/数组。 我们可以对这个序列做一个 join 操作,这意味着我们可以用一个元素替换相邻的两个元素,这个元素是这些元素的总和。例如:

-> 数组/列表:[1;2;3;4;5;6]

  • 我们可以加入 2 和 3,并将它们替换为 5;
  • 我们可以加入 5 和 6,并用 11 替换它们;
  • 我们不能 加入 2 和 4;
  • 我们不能 加入 1 和 3 等

主要问题是找到给定序列的最小连接操作,之后该序列将按升序排序。

注意:空序列和单元素序列按升序排序。

基本示例:

  • 对于 [4; 6; 5个; 3; 9] 解决方案是 1(我们加入 5 和 3)

  • 对于 [1; 3; 6; 5] 解也是1(我们加入 6和5)

我正在寻找的是解决这个问题的算法。它可以是伪代码、C、C++、PHP、OCaml 或类似的(我的意思是:如果你用其中一种语言编写解决方案,我会理解解决方案)。

【问题讨论】:

  • 这是作业吗?你现在在课堂上学习什么主题?
  • 有趣的问题,是作业吗,如果是,请标记它。如果没有,用例会很有趣。
  • 作业,但几分钟前有一个截止日期(解决方案应该在线提交),所以我对解决方案感兴趣,因为我找不到任何(解决方案不会在课堂上提出,我很感兴趣如何解决这个问题)。所以,我认为我不需要使用 homework 标签,对吗?
  • @utyle:你在课堂上学习了什么主题?老师在布置作业时可能会考虑到一些特殊的学习经验,如果我们可以向您展示您的老师想要的解决方案,这可能对您更好。
  • @utyle:请告诉你是否得到了答案……这不是一个容易的问题,如果它应该在非指数时间内解决。

标签: algorithm sorting sequence


【解决方案1】:

这是使用动态编程解决的理想问题,@lijie 描述的递归正是正确的方法,只需进行一些小调整以确保考虑所有可能性。有两个关键的观察结果:(a) 任何连接操作序列都会导致原始向量的一组不重叠的求和子序列,以及 (b) 对于最优连接序列,如果我们看在任何求和子序列 (m...n) 的右侧,该部分是问题的最佳解决方案:“找到子向量 (n+1)...N 的最佳连接序列,使得结果最终序列已排序,所有元素>= sum(m...n)。

直接实现递归当然会导致指数时间算法,但使用动态编程的简单调整使其 O(N^2),因为基本上所有 (m,n) 对都被考虑一次。使用动态编程实现递归的一种简单方法是拥有一个由 (m,n) 索引的数据结构,一旦计算出 f(m,n) 的结果就会存储它们,这样下次我们调用 f(m ,n),我们可以查找之前保存的结果。以下代码使用 R 编程语言执行此操作。我正在使用我们想要找到最小连接数以获得非递减序列的公式。对于那些刚接触 R 的人,要测试此代码,只需从任何镜像(谷歌“R项目”)下载R,启动它,并将两个函数定义(f和solve)粘贴到控制台中,然后使用“solve(c(...))”解决任何向量在下面的例子中。

f <- function(m,n) {
  name <- paste(m,n)
  nCalls <<- nCalls + 1 
  # use <<- for global assignment
  if( !is.null( Saved[[ name ]] ) ) {
    # the solution for (m,n) has been cached, look it up
    nCached <<- nCached + 1
    return( Saved[[ name ]] )
  }
  N <- length(vec) # vec is global to this function
  sum.mn <- -Inf 
  if(m >= 1)
    sum.mn <- sum( vec[m:n] )
  if(n == N) { # boundary case: the (m,n) range includes the last number
    result <- list( num = 0, joins = list(), seq = c())
  } else
  {
    bestNum <- Inf
    bestJoins <- list()
    bestSeq <- c()
    for( k in (n+1):N ) {
      sum.nk <- sum( vec[ (n+1):k ] )
      if( sum.nk < sum.mn ) next
      joinRest <- f( n+1, k )
      numJoins <- joinRest$num + k-n-1
      if( numJoins < bestNum ) {
        bestNum <- numJoins
        if( k == n+1 )
          bestJoins <- joinRest$joins else
        bestJoins <- c( list(c(n+1,k)), joinRest$joins )
        bestSeq <- c( sum.nk, joinRest$seq)
      }
    }  
    result <- list( num = bestNum, joins = bestJoins, seq = bestSeq )
  }
  Saved[[ name ]] <<- result
  result
}

solve <- function(input) {
  vec <<- input
  nCalls <<- 0
  nCached <<- 0
  Saved <<- c()
  result <- f(0,0)
  cat( 'Num calls to f = ', nCalls, ', Cached = ', nCached, '\n')
  cat( 'Min joins = ', result$num, '\n')
  cat( 'Opt summed subsequences: ')
  cat( do.call( paste, 
                lapply(result$joins, 
                       function(pair) paste(pair[1], pair[2], sep=':' ))),
       '\n')
  cat( 'Final Sequence: ', result$seq, '\n' )
}

以下是一些示例运行:

> solve(c(2,8,2,2,9,12))
Num calls to f =  22 , Cached =  4 
Min joins =  2 
Opt summed subsequences: 2:3 4:5 
Final Sequence:  2 10 11 12 

> solve(c(1,1,1,1,1))
Num calls to f =  19 , Cached =  3 
Min joins =  0 
Opt summed subsequences:  
Final Sequence:  1 1 1 1 1 

> solve(c(4,3,10,11))
Num calls to f =  10 , Cached =  0 
Min joins =  1 
Opt summed subsequences: 1:2 
Final Sequence:  7 10 11 

> solve(c (2, 8, 2, 2, 8, 3, 8, 9, 9, 2, 9, 8, 8, 7, 4, 2, 7, 5, 9, 4, 6, 7, 4, 7, 3, 4, 7, 9, 1, 2, 5, 1, 8, 7, 3, 3, 6, 3, 8, 5, 6, 5))
Num calls to f =  3982 , Cached =  3225 
Min joins =  30 
Opt summed subsequences: 2:3 4:5 6:7 8:9 10:12 13:16 17:19 20:23 24:27 28:33 34:42 
Final Sequence:  2 10 10 11 18 19 21 21 21 21 26 46 

请注意,@kotlinski 考虑的序列的最小连接数是 30,而不是 32 或 33。

【讨论】:

  • 这不是一个 O(N^4) 算法,因为有 O(N^2) 个状态并且每个状态都有 O(N^2) 个工作?你可以通过更加小心函数f中的for循环来获得O(N^2 log N)。
  • 这是 O(N^2),只要您不计算调用 sum。以他的最后一个示例为例,Num 调用 f = 3982,Cached = 3225。3982 - 3225 = 757 对 f 的无缓存调用。该集合中有 42 个元素。 757 大约是 42^2 (1764) 的一半。
【解决方案2】:

Greedy algorithm!

import Data.List (inits)

joinSequence :: (Num a, Ord a) => [a] -> Int
joinSequence (x:xs) = joinWithMin 0 x xs
  where joinWithMin k _ [] = k
        joinWithMin k x xs =
          case dropWhile ((< x) . snd) $ zip [0..] $ scanl1 (+) xs
            of (l, y):_ -> joinWithMin (k + l) y $ drop (l+1) xs
               _ -> k + length xs
joinSequence _ = 0

在每一步,抓取更多的元素,直到它们的总和不小于最后一个。如果您的元素用完了,只需将所有剩余的元素加入前一组即可。


那是错误的。

Combinatorial explosion!

joinSequence :: (Num a, Ord a) => [a] -> Int
joinSequence = joinWithMin 0 0
  where joinWithMin k _ [] = k
        joinWithMin k m xs =
            case dropWhile ((< m) . snd) $ zip [0..] $ scanl1 (+) xs
              of [] -> k + length xs
                 ys -> minimum [ joinWithMin (k+l) y $ drop (l+1) xs 
                               | (l, y) <- ys ]

尝试所有可能的加入,并尽量减少。我想不出一个聪明的启发式来限制回溯,但这应该是 O(n²) 和 dynamic programming 和 O(2n) 写的。

【讨论】:

  • 我没有 haskell 编译器,但根据您的描述,我相信它在@Anon 在下面的评论中给出的相同示例上会中断:[4, 3, 10, 11]
  • 好吧,坦率地说,我对 Haskell 的理解并不那么流利,无法理解您的代码。我的意思是 - 我不知道 Haskell,但我前段时间用一种函数式编程语言(Caml)进行了编程。有没有可能,你会用 simpler 版本的 haskell 或 c、cpp、caml 重写你的代码(对于那些对 Haskell 不友好的人)? (用户 kotlinski 也在 haskell 中写过解决方案,但他的代码对我来说是可以理解的)。
  • @marcog:嗯。我没有看到其他解决方案(它被删除了吗?)但你是对的,贪婪会在那里失败。 @utyle:代码不重要,概念很重要。而且,正如 marcog 指出的那样,这个概念是错误的……
  • 好的,我理解这个概念,但是请 - 用 simpler Haskell 或其他语言(c、cpp、php、js、java、caml)向我展示解决方案的代码。我只想知道,如何自己编码解决方案。非常感谢您的帮助。
  • @ephemient:如果可以,请用其他语言编写适当的解决方案,因为我不确定我是否正确理解代码。谢谢
【解决方案3】:

一种动态规划方法:

设原数组为a[i], 0 &lt;= i &lt; N

f(m, n) 定义为使a[n..N-1] 排序所需的最小连接数,这样排序子列表中的所有元素都是&gt;(或&gt;=,如果需要另一个变体)@ 的总和987654326@(让一个空列表的总和为-inf)。

基本情况是f(m, N) = 0(子列表为空)。

递归是f(m, n) = min_{n &lt; k &lt;= N s.t. sum(a[n..k-1]) &gt; sum(a[m..n-1])} f(n, k) + k-n-1。如果没有合适的 k 值,则让 f(m, n) = inf (任何 &gt;= N 也可以,因为最多有 N-1 连接)。

按照mn的降序计算f(m,n)

那么,想要的答案是f(0,0)

编辑

我相信这基本上是 ehemient 的第二个答案,虽然我对 Haskell 不够熟悉,无法确切知道它在做什么。

【讨论】:

  • 我写的不是DP,但这是我提到的想法。唔。我必须考虑这在存在负整数的情况下是否正常工作,但如果不是,不妨从 0 而不是 -inf 开始。
  • +1。我发现这是一个有用的解释。仍然无法避免在某处存在 O(N) 解决方案的感觉......也许可以根据输入序列的“偏离排序”的某种度量来参数化问题?
【解决方案4】:

一些 Haskell 代码:

sortJoin (a:b:c:xs)
    | a <= b    = a : sortJoin (b:c:xs)  
    | a+b <= c  = a+b : sortJoin (c:xs)  
    | otherwise = sortJoin (a:b+c:xs)    
sortJoin (a:b:[]) = if a <= b then [a,b] else [a+b]
sortJoin a@_ = a

edits xs = length xs - length (sortJoin xs)

更新:使用 test = [2, 8, 2, 2, 8, 3, 8, 9, 9, 2, 9, 8, 8, 7, 4, 2, 7, 5, 9, 4, 6, 7, 4, 7, 3, 4, 7, 9, 1, 2, 5, 1, 8, 7, 3, 3, 6, 3, 8, 5, 6, 5]

...现在我们得到:

> sortJoin test
[2,8,12,20,20,23,27,28,31,55]
> edits test
32

【讨论】:

  • @kotlinski:你确定这适用于 every 数组吗?什么输出为 [2; 8个; 2; 2; 8个; 3; 8个; 9; 9; 2; 9; 8个; 8个; 7; 4; 2; 7; 5个; 9; 4; 6; 7; 4; 7; 3; 4; 7; 9; 1个; 2; 5个; 1个; 8个; 7; 3; 3; 6; 3; 8个; 5个; 6; 5]? (我现在不能运行你的程序,所以我不能针对这个示例数组测试它)。我相信,正确的解决方案是 32。
  • 这不太行。例如,考虑:[4, 3, 10, 11].
  • @kotlinski:您的解决方案不适用于 [1;1;1;1;1] 和类似情况。
  • @utyle:在 [1;1;1;1;1] 的情况下应该发生什么?你想要他们&lt;? (无论哪种方式都很容易修复。
  • 反例:对于
【解决方案5】:

希望保持简单。这是一些指数时间的伪代码。

Function "join" (list, max-join-count, join-count) ->
    Fail if join-count is greater than max-join-count.
    If the list looks sorted return join-count.
    For Each number In List
        Recur (list with current and next number joined, max-join-count, join-count + 1)

Function "best-join" (list) ->
    max-join-count = 0
    while not join (list, max-join-count++)

这是 Clojure 上的一个实现:

(defn join-ahead [f i v]
  (concat (take i v)
          [(f (nth v i) (nth v (inc i)))]
          (drop (+ 2 i) v)))

(defn sort-by-joining
  "Sort a list by joining neighboring elements with `+'"
  ([v max-join-count join-count]
     (if (or (nil? max-join-count)
             (<= join-count max-join-count))
       (if (or (empty? v)
               (= v (sort v)))
         {:vector v :join-count join-count}
         (loop [i 0]
           (when (< (inc i) (count v))
             (let [r (sort-by-joining (join-ahead + i v)
                                      max-join-count
                                      (inc join-count))]
               (or r (recur (inc i)))))))))
  ([v max-join-count]
     (sort-by-joining v max-join-count 0))
  ([v]
     (sort-by-joining v nil 0)))

(defn fewest-joins [v]
  (loop [i 0]
    (if (sort-by-joining v i)
      i
      (recur (inc i)))))

(deftest test-fewest-joins
  (is (= 0 (fewest-joins nil)))
  (is (= 1 (fewest-joins [4 6 5 3 9])))
  (is (= 6 (fewest-joins [1 9 22 90 1 1 1 32 78 13 1]))))

【讨论】:

    【解决方案6】:

    这是 F# 中的 pchalasani 代码,经过一些修改。记忆是相似的,我在 O(1) 时间内添加了 sumRange 函数生成器,并将起始位置移动到 f 1 0 以跳过在 minJoins 中检查 n = 0。

    let minJoins (input: int array) =
        let length = input.Length
        let sum = sumRange input
    
        let rec f = memoize2 (fun m n ->
            if n = length then
                0
            else
                let sum_mn = sum m n 
    
                {n + 1 .. length}
                |> Seq.filter (fun k -> sum (n + 1) k >= sum_mn)
                |> Seq.map (fun k -> f (n + 1) k + k-n-1)
                |> Seq.append {length .. length}
                |> Seq.min
            )
        f 1 0
    

    完整代码。

    open System.Collections.Generic
    
    // standard memoization
    let memoize2 f = 
        let cache = new Dictionary<_, _>()
        (fun x1 x2 -> 
            match cache.TryGetValue((x1, x2)) with
            | true, y -> y
            | _ -> 
                let v = f x1 x2
                cache.Add((x1, x2), v)
                v)
    
    // returns a function that takes two integers n,m and returns sum(array[n:m])
    let sumRange (array : int array) =
        let forward = Array.create (array.Length + 1) 0
    
        let mutable total = 0
        for i in 0 .. array.Length - 1 do
            total <- total + array.[i]
            forward.[i + 1] <- total
    
        (fun i j -> forward.[j] - forward.[i - 1])
    
    // min joins to sort an array ascending
    let minJoins (input: int array) =
        let length = input.Length
        let sum = sumRange input
    
        let rec f = memoize2 (fun m n ->
            if n = length then
                0
            else
                let sum_mn = sum m n 
    
                {n + 1 .. length}
                |> Seq.filter (fun k -> sum (n + 1) k >= sum_mn)
                |> Seq.map (fun k -> f (n + 1) k + k-n-1)
                |> Seq.append {length .. length} // if nothing passed the filter return length as the min
                |> Seq.min
            )
        f 1 0
    
    let input = [|2;8;2;2;8;3;8;9;9;2;9;8;8;7;4;2;7;5;9;4;6;7;4;7;3;4;7;9;1;2;5;1;8;7;3;3;6;3;8;5;6;5|]
    let output = minJoins input
    printfn "%A" output
    // outputs 30
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-07-28
      • 2016-06-10
      • 1970-01-01
      • 2017-05-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多