求最小值“加入”序列操作

发布于 2024-10-08 13:22:31 字数 675 浏览 0 评论 0原文

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

-> array/list: [1;2;3;4;5;6]

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

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

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

基本示例:

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

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

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

Let's say, we have a list/an array of positive integers x1, x2, ... , xn.
We can do a join operation on this sequence, that means that we can replace two elements that are next to each other with one element, which is sum of these elements. For example:

-> array/list: [1;2;3;4;5;6]

  • we can join 2 and 3, and replace them with 5;
  • we can join 5 and 6, and replace them with 11;
  • we cannot join 2 and 4;
  • we cannot join 1 and 3 etc.

Main problem is to find minimum join operations for given sequence, after which this sequence will be sorted in increasing order.

Note: empty and one-element sequences are sorted in increasing order.

Basic examples:

  • for [4; 6; 5; 3; 9] solution is 1 (we join 5 and 3)

  • for [1; 3; 6; 5] solution is also 1 (we join 6 and 5)

What I am looking for, is an algorithm that solve this problem. It could be in pseudocode, C, C++, PHP, OCaml or similar (I mean: I would understand solution, if You wrote solution in one of these languages).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

萌面超妹 2024-10-15 13:22:31

这是使用动态编程解决的理想问题,@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 新手,要测试此代码,只需从任何镜像(Google“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。

This is an ideal problem to solve using Dynamic Programming, and the recurrence described by @lijie is exactly the right approach, with a few minor tweaks to ensure all possibilities are considered. There are two key observations: (a) Any sequence of join operations results in a set of non-overlapping summed subsequences of the original vector, and (b) For the optimal join-sequence, if we look to the right of any summed subsequence (m...n), that portion is an optimal solution to the problem: "find an optimal join-sequence for the sub-vector (n+1)...N such that the resulting final sequence is sorted, and all elements are >= sum(m...n).

Implementing the recurrence directly would of course result in an exponential time algorithm, but a simple tweak using Dynamic Programming makes it O(N^2), because essentially all (m,n) pairs are considered once. An easy way to implement the recurrence using Dynamic Programming is to have a data-structure indexed by (m,n) that stores the results of f(m,n) once they are computed, so that the next time we invoke f(m,n), we can lookup the previously saved results. The following code does this using the R programming language. I am using the formulation where we want to find the min-number of joins to get a non-decreasing sequence. For those new to R, to test this code, simply download R from any mirror (Google "R Project"), fire it up, and paste the two function definitions (f and solve) into the console, and then solve any vector using "solve(c(...))" as in the examples below.

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' )
}

Here are some sample runs:

> 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 

Note that the min number of joins for the sequence considered by @kotlinski is 30, not 32 or 33.

将军与妓 2024-10-15 13:22:31

贪心算法

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

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


那是错误的。

组合爆炸

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²) 动态编程< /a> 和 O(2n) 如下所示。

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

At each step, grab more elements until their sum is not less than the last. If you run out of elements, just join all the ones that remain to the prior group.


That was wrong.

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 ]

Just try every possible joining and take the minimum. I couldn't think of a smart heuristic to limit backtracking, but this should be O(n²) with dynamic programming, and O(2n) as written.

极致的悲 2024-10-15 13:22:31

动态规划方法:

设原数组为a[i], 0 <= i < N。

f(m, n) 定义为使 a[n..N-1] 排序所需的最小连接数,以便排序子列表中的所有元素是 > (或 >=,如果需要其他变体)a[m..n-1] 的总和(让空列表的总和为 -inf)。

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

递归为 f(m, n) = min_{n k <= N st sum(a[n..k-1]) > sum(a[m..n-1])} f(n, k) + kn-1.如果没有合适的 k 值,则让 f(m, n) = inf (任何 >= N 也可以,因为最多有 N-1 加入)。

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

那么,所需的答案是f(0,0)

编辑

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

A dynamic programming approach:

Let the original array be a[i], 0 <= i < N.

Define f(m, n) to be the minimum number of joins needed to make a[n..N-1] sorted, such that all elements in the sorted sublist are > (or >=, if another variant is desired) the sum of a[m..n-1] (let the sum of an empty list to be -inf).

The base case is f(m, N) = 0 (the sublist is empty).

The recursion is f(m, n) = min_{n < k <= N s.t. sum(a[n..k-1]) > sum(a[m..n-1])} f(n, k) + k-n-1. If no values of k are suitable, then let f(m, n) = inf (anything >= N will also work, because there are at most N-1 joins).

Calculate f(m,n) in decreasing order of m and n.

Then, the desired answer is f(0,0).

EDIT

Oops this is basically ephemient's second answer, I believe, although I am not familiar enough with Haskell to know exactly what it is doing.

沉鱼一梦 2024-10-15 13:22:31

一些 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

Some Haskell code:

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)

UPDATE: Made this work with 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]

...now we get:

> sortJoin test
[2,8,12,20,20,23,27,28,31,55]
> edits test
32
梦途 2024-10-15 13:22:31

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

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]))))

Hopefully keeping it simple. Here's some pseudo-code that's exponential time.

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++)

Here's an implementation on 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]))))
时光磨忆 2024-10-15 13:22:31

这是 F# 中的 pchalasani 代码,经过一些修改。记忆过程类似,我添加了一个 sumRange 函数生成器,用于在 O(1) 时间内求和,并将起始位置移动到 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

This is pchalasani code in F# with some modifications. The memoization is similar, I added a sumRange function generator for sums in O(1) time and moved the start position to f 1 0 to skip checking for n = 0 in minJoins.

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

Full code.

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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文