[英]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已经提供了一个直接的解决方案,我想我会提出一个解决方案,它突出了具有高阶函数的组合作为函数式编程中常用的强大技术; 这个问题可以分解为三个不连续的步骤:
我解除了@ 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)
我在你提出的函数签名中交换了trades
和tolerance
的顺序,因为它更容易通过容差和管道(交易,价值)列表进行咖喱,这是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.