A different way to think about this is to think of each permutation as being a list of indexes into each of those inner lists. For example:
input = [[1, 2], [3, 4]]
Here, the indexes for each permutation/generation are:
permuation |
list 1 index |
list 2 index |
1 |
0 |
0 |
2 |
0 |
1 |
3 |
1 |
0 |
4 |
1 |
1 |
If we imagined those indexes as the list of lists:
steps = [
[0,0],
[0,1],
[1,0],
[1,1],
]
Here you can see for the first list [1,2]
we “stay” on index 0 for 2 permutations before ticking over to index 1 for two generations. For the second list [3,4]
we “stay” on a given index for 1 tick only.
We could apply these indexes to the inputs like so:
Enum.reduce(steps, [], fn indexes, acc ->
permutation = Enum.zip_with(input, indexes, &Enum.at(&1, &2))
[permutation | acc]
end)
|> Enum.reverse()
Yielding us:
[
[1, 3],
[1, 4],
[2, 3],
[2, 4]
]
So the question becomes, is there a way to generate the correct list of indexes we’d need for each step, for each list? Well, there is a pattern at play. The general rule is for each list we “stay” on a specific index for N generations, where N is the length of the list after it multiplied by the length of the list after that and so on (ie the cartesian product).
To see this, let’s look at your original example:
[[1, 2, 3], [4, 5], [6, 7], [1]]
in the first list [1,2,3]
we stay at index 0 for 4 steps / permutations. Why 4? Because length([4,5]) * length([6,7]) * length([1]) == 4
. If we were to write our the indexes for this list for every permutation it would look like this:
000011112222
to generate this we use the formula current_permutation mod total_permutations / how long we stay on an index
.
We know there are only 3 items in the first list so their are only 3 indexes, meaning total number of permutations is 3*4 = 12.
The formula for this then is:
index_for_permutation = div(
rem(permutation_number, repetitions),
steps_on_an_index
)
Since we know there are 12 permutations total for input, and that we stay on an index for 4, we get:
index_for_permutation = div(
rem(permutation_number, 12),
4
)
So we can:
for n <- 0..(12 - 1) do
index = div(rem(n, 12), 4)
end
Now to generalise to all lists it’s easier to start at the end and work back:
reversed = input |> Enum.reverse()
[rightmost_list | rest_lists] = reversed
index_info =
rest_lists
|> Enum.reduce([{length(rightmost_list), 1}], fn list, [{total_prior_steps, _} | _] = acc ->
total_steps = length(list) * total_prior_steps
[{total_steps, total_prior_steps} | acc]
end)
generates:
[{12, 4}, {4, 2}, {2, 1}, {1, 1}]
which are the two numbers for each list that we need to plug into our formula.
Now we can generalise the above:
defmodule Combinations do
def combinations(input) do
reversed = input |> Enum.reverse()
[rightmost_list | rest_lists] = reversed
index_info =
rest_lists
|> Enum.reduce([{length(rightmost_list), 1}], fn list, [{total_prior_steps, _} | _] = acc ->
total_steps = length(list) * total_prior_steps
[{total_steps, total_prior_steps} | acc]
end)
[{total_number_of_permutations, _} | _] = index_info
for n <- 0..(total_number_of_permutations - 1) do
Enum.zip_with(input, index_info, fn list, {repetitions, steps_on_an_index} ->
index = div(rem(n, repetitions), steps_on_an_index)
Enum.at(list, index)
end)
end
end
end
lists = [[1, 2, 3], [4, 5], [6, 7], [1]]
Combinations.combinations(lists)
I’m not sure if this is faster or anything given lists are linked lists in Elixir and such, but it was fun to come up with.