-
Notifications
You must be signed in to change notification settings - Fork 1
/
combination.ex
100 lines (82 loc) · 3.04 KB
/
combination.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
defmodule Combination do
@moduledoc """
Provide a set of algorithms to generate combinations and permutations.
For non-distinct elements in the source collection, simply pipe the result through `Enum.uniq/1` to remove duplicates.
"""
@doc """
Generate combinations of based on given collection.
## Examples
iex> 1..3 |> Combination.combine(2)
[[3, 2], [3, 1], [2, 1]]
"""
@spec combine(Enum.t, non_neg_integer) :: [list]
def combine(collection, k) when is_integer(k) and k >= 0 do
list = Enum.to_list(collection)
list_length = Enum.count(list)
if k > list_length do
raise Enum.OutOfBoundsError
else
do_combine(list, list_length, k, [], [])
end
end
@doc """
Generate k1..k2 combinations of based on given collection, where k2 > k1.
## Examples
iex> 1..3 |> Combination.combine(1..2)
[[1], [2], [3], [3, 2], [3, 1], [2, 1]]
iex> 1..3 |> Combination.combine(2..3)
[[3, 2], [3, 1], [2, 1], [3, 2, 1]]
"""
@spec combine(Enum.t, Range.t) :: [list]
def combine(collection, k1..k2) when k2 >= k1 do
Enum.flat_map(k1..k2, &combine(collection, &1))
end
defp do_combine(_list, _list_length, 0, _pick_acc, _acc), do: [[]]
defp do_combine(list, _list_length, 1, _pick_acc, _acc), do: list |> Enum.map(&([&1])) # optimization
defp do_combine(list, list_length, k, pick_acc, acc) do
list
|> Stream.unfold(fn [h | t] -> {{h, t}, t} end)
|> Enum.take(list_length)
|> Enum.reduce(acc, fn {x, sublist}, acc ->
sublist_length = Enum.count(sublist)
pick_acc_length = Enum.count(pick_acc)
if k > pick_acc_length + 1 + sublist_length do
acc # insufficient elements in sublist to generate new valid combinations
else
new_pick_acc = [x | pick_acc]
new_pick_acc_length = pick_acc_length + 1
case new_pick_acc_length do
^k -> [new_pick_acc | acc]
_ -> do_combine(sublist, sublist_length, k, new_pick_acc, acc)
end
end
end)
end
@doc """
Generate all permutation of the collection, filtered by `filter` function.
The `filter` function filters the generated permutation before it is added
to the result list. It returns true by default, thus allowing all permutations.
## Example
iex> 1..3 |> Combination.permutate
[[1, 2, 3], [2, 1, 3], [3, 1, 2], [1, 3, 2], [2, 3, 1], [3, 2, 1]]
iex> 1..3 |> Combination.permutate(fn p -> Enum.at(p, 0) == 1 end)
[[1, 2, 3], [1, 3, 2]]
"""
@spec permutate(Enum.t, (list -> as_boolean(term))) :: [list]
def permutate(collection, filter \\ fn _p -> true end) do
collection
|> Enum.to_list
|> do_permutate(filter, [], [])
end
defp do_permutate([], filter, pick_acc, acc) do
if filter.(pick_acc), do: [pick_acc | acc], else: acc
end
defp do_permutate(list, filter, pick_acc, acc) do
list
|> Stream.unfold(fn [h | t] -> {{h, t}, t ++ [h]} end)
|> Enum.take(Enum.count(list))
|> Enum.reduce(acc, fn {h, t}, acc ->
do_permutate(t, filter, [h | pick_acc], acc)
end)
end
end