Optimization 如何迭代一组总和约为0的数字的所有子集
现在,我已经有将近20年没有应用于函数式编程了,那时我们只写阶乘和fib,所以我真的呼吁社区帮我找到解决方案 我的问题是: “给定一组交易对象,我想找出净公差为零+/-一定公差的所有交易组合。” 我的第一个十年计划是:Optimization 如何迭代一组总和约为0的数字的所有子集,optimization,math,f#,functional-programming,Optimization,Math,F#,Functional Programming,现在,我已经有将近20年没有应用于函数式编程了,那时我们只写阶乘和fib,所以我真的呼吁社区帮我找到解决方案 我的问题是: “给定一组交易对象,我想找出净公差为零+/-一定公差的所有交易组合。” 我的第一个十年计划是: let NettedOutTrades trades tolerance = ... 让我们假设我的起点是先前构造的元组数组(trade,value)。我想要的是一个数组(或者列表,随便什么)的交易数组。因此: let result = NettedOutTrades [| (
let NettedOutTrades trades tolerance = ...
让我们假设我的起点是先前构造的元组数组(trade,value)。我想要的是一个数组(或者列表,随便什么)的交易数组。因此:
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 [] []
让nettedOutTrades交易容差=
//递归处理“剩余”交易。目前累计交易为
//存储在“当前”中,其价格总和为“总和”。蓄能器
//“结果”存储总计为0(+/-容差)的所有交易列表
让rec循环剩余和当前结果=
与剩余的匹配
//完成了对所有交易和当前交易列表的迭代
//匹配条件且为非空-将其添加到结果
|[]当总和>=-公差和总和电流::结果
|[]->结果//已完成,但不符合条件
|(t,p)::交易->
//使用两个选项递归处理剩余交易:
//1)如果我们将交易添加到当前交易中
让结果=循环交易(总和+p)(t::当前)结果
//2)如果我们不添加交易并跳过它
循环交易和当前结果
循环交易0[][]
该函数递归地处理所有组合,因此它不是特别有效(但可能没有更好的方法)。只有在第二次调用
循环时,它才是尾部递归的,但要使它完全尾部递归,需要继续,这会使示例更复杂。因为@Tomas已经给出了一个直接的解决方案,我想我会提出一个解决方案,强调高阶函数的组合是函数编程中常用的一种强大技术;该问题可分解为三个离散步骤:
生成一组元素的所有组合。这是问题中最困难和可重用的部分。因此,我们将问题的这一部分隔离到一个独立函数中,该函数返回给定通用元素列表的组合序列
给定(交易、价值)列表,筛选出价值总和不在给定公差范围内的所有组合
将每个组合从(交易、价值)列表映射到交易列表
我提升了@Tomas计算所有(除了空的)组合的底层算法
但使用递归序列表达式,而不是带有累加器的递归函数(我发现这更容易读写)
这很有趣。我发现有两种延续:构建器延续和处理延续
无论如何;这与子集和问题非常相似,它是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
使用生成器延续而不是累加器的原因是,您获得的结果与传入的结果的顺序相同,而无需反转。可能需要编辑您的示例-应该是(t1,-11),这样他们就可以算出每个交易的问题相当于子集和问题()这是NP完全。@BrokenGlass:I asume Brett将1定义为原始问题中提到的公差间隙(“…该网络为零+/-一些公差”)。没错,froeschli,这个例子是为了说明公差的使用……但公平地说,这可能是问题中最微不足道的部分,为了清楚起见,可以省略掉。+1@Tomas,你真的很快就完成了这个例子,然后回来并添加了精彩的评论。一个小建议:翻转nettedOutTrades
的trades
和tolerance
参数,这样您就可以在交易中通过公差和管道进行交易。+1@Tomas,感谢您提供的伟大解决方案。我接受了Stephen的回答,因为它分解了各个组成部分,我可以看到,在我的需求范围(不可避免地)逐渐扩大的将来,这将是多么有用。
[("a", 2); ("b", -1); ("c", -2); ("d", 1)] |> nettedOutTrades 1
/// 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)]