簡體   English   中英

如何迭代總和為0的一組數字的所有子集

[英]How to iterate over all subsets of a set of numbers that sum to around 0

現在,我已經沒有將自己應用於函數式編程了,近20年,當我們沒有比寫因子和文本更進一步時,所以我真的很想吸引社區尋求解決方案。

我的問題是:

“鑒於一組交易對象,我想找到所有交易的組合,凈值為零+/-一些容差。”

我十歲的首發是:

let NettedOutTrades trades tolerance = ...

讓我們假設我的起點是先前構造的元組數組(交易,價值)。 我想要的是一個數組(或列表,無論如何)的交易數組。 從而:

let result = NettedOutTrades [| (t1, -10); (t2, 6); (t3, 6); (t4; 5) |] 1

會導致:

   [| 
     [| t1; t2; t4 |]
     [| t1; t3; t4 |]
   |]

我認為這可以通過尾遞歸構造實現,使用兩個累加器 - 一個用於結果,一個用於交易值的總和。 但是如何把它們放在一起......?

我確信我可以使用c#刪除一些程序性的東西,但它只是感覺不適合這項工作的工具 - 我相信使用功能范例會有一個優雅,簡潔,高效的解決方案......我目前還沒有很好地練習識別它!

這是編寫所需函數的一種功能方法。 這是一個簡單的功能實現,沒有使用列表的任何聰明的優化。 它不是尾遞歸的,因為它需要為每個交易遞歸調用兩次:

let nettedOutTrades trades tolerance =
  // Recursively process 'remaining' trades. Currently accumulated trades are
  // stored in 'current' and the sum of their prices is 'sum'. The accumulator
  // 'result' stores all lists of trades that add up to 0 (+/- tolerance)
  let rec loop remaining sum current result =
    match remaining with 
    // Finished iterating over all trades & the current list of trades
    // matches the condition and is non-empty - add it to results
    | [] when sum >= -tolerance && sum <= tolerance &&
              current <> [] -> current::result
    | [] -> result // Finished, but didn't match condition
    | (t, p)::trades -> 
      // Process remaining trades recursively using two options:
      // 1) If we add the trade to current trades
      let result = loop trades (sum + p) (t::current) result
      // 2) If we don't add the trade and skip it
      loop trades sum current result
  loop trades 0 [] [] 

該函數遞歸地處理所有組合,因此它不是特別有效(但可能沒有更好的方法)。 它僅在第二次loop調用中是尾遞歸的,但為了使其完全是尾遞歸,你需要連續性 ,這會使示例更復雜一些。

由於@Tomas已經提供了一個直接的解決方案,我想我會提出一個解決方案,它突出了具有高階函數的組合作為函數式編程中常用的強大技術; 這個問題可以分解為三個不連續的步驟:

  1. 生成一組元素的所有組合。 這是問題中最困難和可重復使用的部分。 因此,我們將問題的這一部分隔離成一個獨立的函數,該函數返回給定一個通用元素列表的一系列組合。
  2. 給定(交易,價值)清單,過濾掉不在給定容差范圍內的價值總和的所有組合。
  3. 將每個組合從(貿易,價值)列表映射到交易清單。

我解除了@ Tomas用於計算集合的所有(期望空)組合的基礎算法,但是使用遞歸序列表達式而不是使用累加器的遞歸函數(我發現這稍微更容易讀取和寫入)。

let combinations input =
    let rec loop remaining current = seq {
        match remaining with 
        | [] -> ()
        | hd::tail -> 
            yield  hd::current
            yield! loop tail (hd::current)
            yield! loop tail current
    }
    loop input []

let nettedOutTrades tolerance trades =
    combinations trades
    |> Seq.filter
        (fun tradeCombo -> 
            tradeCombo |> List.sumBy snd |> abs <= tolerance)
    |> Seq.map (List.map fst)

我在你提出的函數簽名中交換了tradestolerance的順序,因為它更容易通過容差和管道(交易,價值)列表進行咖喱,這是F#社區中使用的典型樣式,並且通常由F#庫鼓勵。 例如:

[("a", 2); ("b", -1); ("c", -2); ("d", 1)] |> nettedOutTrades 1

這很有趣。 我發現有兩種延續:構建器延續和處理延續。

無論如何; 這與子集和問題非常相似,它是NP完全的。 因此,可能沒有比枚舉所有可能性更快的算法,並選擇那些符合標准的算法。

但是,實際上並不需要從生成的組合中構建數據結構。 如果用每個結果調用一個函數更有效。

/// Takes some input and a function to receive all the combinations
/// of the input.
///   input:    List of any anything
///   iterator: Function to receive the result.
let iterCombinations input iterator =
  /// Inner recursive function that does all the work.
  ///   remaining: The remainder of the input that needs to be processed
  ///   builder:   A continuation that is responsible for building the
  ///              result list, and passing it to the result function.
  ///   cont:      A normal continuation, just used to make the loop tail
  ///              recursive.
  let rec loop remaining builder cont =
    match remaining with
    | [] ->
        // No more items; Build the final value, and continue with
        // queued up work.
        builder []
        cont()
    | (x::xs) ->
        // Recursively build the list with (and without) the current item.
        loop xs builder <| fun () ->
          loop xs (fun ys -> x::ys |> builder) cont
  // Start the loop.
  loop input iterator id

/// Searches for sub-lists which has a sum close to zero.
let nettedOutTrades tolerance items =
  // mutable accumulator list
  let result = ref []
  iterCombinations items <| function
    | [] -> () // ignore the empty list, which is always there
    | comb ->
        // Check the sum, and add the list to the result if
        // it is ok.
        let sum = comb |> List.sumBy snd
        if abs sum <= tolerance then
          result := (List.map fst comb, sum) :: !result
  !result

例如:

> [("a",-1); ("b",2); ("c",5); ("d",-3)]
- |> nettedOutTrades 1
- |> printfn "%A"

[(["a"; "b"], 1); (["a"; "c"; "d"], 1); (["a"], -1); (["b"; "d"], -1)]

使用構建器連續而不是累加器的原因是,您獲得的結果與傳入的順序相同,而不必反轉它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM