通过总计运行F#列表组?

时间:2018-09-19 17:40:17

标签: f#

我有以下由第一项排序的元组列表。我想通过以下方式聚类时间

  1. 如果元组的第二项大于50,则它将位于其自己的簇中。
  2. 否则,将总和小于50的项目聚类。
  3. 无法更改顺序。

代码:

let values =
  [("ACE", 78);
   ("AMR", 3);
   ("Aam", 6);
   ("Acc", 1);
   ("Adj", 23);
   ("Aga", 12);
   ("All", 2);
   ("Ame", 4); 
   ("Amo", 60);
   //.... 
  ]
values |> Seq.groupBy(fun (k,v) -> ???)

期望值为

[["ACE"] // 78
 ["AMR"; "Aam"; "Acc"; "Adj"; "Aga"; "All"] // 47
 ["Ame"] // 4
 ["Amo"] // 60
....]

理想情况下,我想平均分配第二组(["AMR"; "Aam"; "Acc"; "Adj"; "Aga"; "All"],总数为47)和第三组(["Ame"],总数只有4)。

如何在F#中实现它?


我有以下解决方案。它使用一个可变变量。这不是F#惯用的吗? for ... do是否在F#中势在必行,或者它是某种函数构造的语法糖?

seq {
    let mutable c = []
    for v in values |> Seq.sortBy(fun (k, _) -> k) do
        let sum = c |> Seq.map(fun (_, v) -> v) |> Seq.sum
        if not(c = []) && sum + (snd v) > 50 
        then 
            yield c
            c <- [v]
        else
            c <- List.append c [v]
 }

4 个答案:

答案 0 :(得分:3)

我想我明白了。这不是有史以来最好的代码,但是可以工作并且是不变的。

let foldFn (acc:(string list * int) list) (name, value) =
    let addToLast last = 
        let withoutLast = acc |> List.filter ((<>) last)
        let newLast = [((fst last) @ [name]), (snd last) + value]
        newLast |> List.append withoutLast

    match acc |> List.tryLast with
    | None -> [[name],value]
    | Some l ->
        if (snd l) + value <= 50 then addToLast l
        else [[name], value] |> List.append acc

values |> List.fold foldFn [] |> List.map fst

更新:由于添加操作可能会非常昂贵,因此我添加了仅添加版本(仍然满足保留订单的原始要求)。

let foldFn (acc:(string list * int) list) (name, value) =
    let addToLast last = 
        let withoutLast = acc |> List.filter ((<>) last) |> List.rev
        let newLast = ((fst last) @ [name]), (snd last) + value
        (newLast :: withoutLast) |> List.rev

    match acc |> List.tryLast with
    | None -> [[name],value]
    | Some l ->
        if (snd l) + value <= 50 then addToLast l
        else ([name], value) :: (List.rev acc) |> List.rev

注意:第4行上仍然有@运算符(在群集中创建新名称列表时),但是由于群集中理论上最大名称数量为50(如果所有这些名称均为等于1),则此处的效果可以忽略不计。

如果在最后一行删除List.map fst,则将获得列表中每个群集的总和。

答案 1 :(得分:2)

追加操作非常昂贵。即使在处理后需要将列表反转,带有中间结果的直接折叠也会更便宜。

["ACE", 78; "AMR", 3; "Aam", 6; "Acc", 1; "Adj", 23; "Aga", 12; "All", 2; "Ame", 4; "Amd", 6; "Amo", 60]
|> List.fold (fun (r, s1, s2) (t1, t2) ->
    if t2 > 50 then [t1]::s1::r, [], 0
    elif s2 + t2 > 50 then s1::r, [t1], t2
    else r, t1::s1, s2 + t2 ) ([], [], 0)
|> fun (r, s1, _) -> s1::r
|> List.filter (not << List.isEmpty)
|> List.map List.rev
|> List.rev
// val it : string list list =
//   [["ACE"]; ["AMR"; "Aam"; "Acc"; "Adj"; "Aga"; "All"]; ["Ame"; "Amd"];
//    ["Amo"]]

答案 2 :(得分:1)

这是一个递归版本-与fold-versions的工作方式大致相同:

let groupBySums data =
    let rec group cur sum acc lst =
        match lst with
        | [] -> acc |> List.where (not << List.isEmpty) |> List.rev
        | (name, value)::tail when value > 50 -> group [] 0 ([(name, value)]::(cur |> List.rev)::acc) tail
        | (name, value)::tail -> 
            match sum + value with
            | x when x > 50 -> group [(name, value)] 0 ((cur |> List.rev)::acc) tail
            | _ -> group ((name, value)::cur) (sum + value) acc tail
    (data |> List.sortBy (fun (name, _) -> name)) |> group [] 0 []

values |> groupBySums |> List.iter (printfn "%A")

答案 3 :(得分:0)

还使用Seq.mapFoldSeq.groupBy的另一种解决方案:

let group values =
    values
    |> Seq.mapFold (fun (group, total) (name, count) -> 
        let newTotal = count + total
        let newGroup = group + if newTotal > 50 then 1 else 0
        (newGroup, name), (newGroup, if newGroup = group then newTotal else count) 
        ) (0, 0)
    |> fst
    |> Seq.groupBy fst
    |> Seq.map    (snd >> Seq.map snd >> Seq.toList)

像这样调用它:

[   "ACE", 78
    "AMR", 3
    "Aam", 6
    "Acc", 1
    "Adj", 23
    "Aga", 12
    "All", 2
    "Ame", 4
    "Amo", 60
] 
|> group        
|> Seq.iter    (printfn "%A")

// ["ACE"]
// ["AMR"; "Aam"; "Acc"; "Adj"; "Aga"; "All"]
// ["Ame"]
// ["Amo"]