In my answer, I will ignore the last result you had: Adam, Bob: 1, 2, 3
, because it is a logical exception. Go to the end to see my conclusion.
Explanation:
The idea would be to iterate over the combinations "which group will belong to the element".
Say that you have elements "a, b, c", and you have groups "1, 2", let's have an array of size 3, as the number of elements that will contain all possible combinations of groups "1, 2", with repetition:
{1, 1, 1} {1, 1, 2} {1, 2, 1} {1, 2, 2} {2, 1, 1} {2, 1, 2} {2, 2, 1} {2, 2, 2}
Now we take each group and make it a key-value, with the following logic:
The group elements[i]
will be the value of comb[i]
.
Example with {1, 2, 1}: a: group 1 b: group 2 c: group 1 And in a different view, the way you wanted it: group 1: a, c group 2: b
After all this, you just need to filter out all the combinations that do not contain all the groups, because you wanted all the groups to have at least one value.
So, you should check if all groups are displayed in a certain combination and filter those that do not match, so you will be left with:
{1, 1, 2} {1, 2, 1} {1, 2, 2} {2, 1, 2} {2, 2, 1} {2, 1, 1}
This will lead to:
1: a, b 2: c 1: a, c 2: b 1: a 2: b, c 1: b 2: a, c 1: c 2: a, b 1: b, c 2: a
This group combination logic will work for more elements and groups. Here is my implementation, which could probably be done better, because even I got a little lost there while I encoded it (this is really not an intuitive problem), but it works well.
Implementation:
public static IEnumerable<ILookup<TValue, TKey>> Group<TKey, TValue> (List<TValue> keys, List<TKey> values, bool allowEmptyGroups = false) { var indices = new int[values.Count]; var maxIndex = values.Count - 1; var nextIndex = maxIndex; indices[maxIndex] = -1; while (nextIndex >= 0) { indices[nextIndex]++; if (indices[nextIndex] == keys.Count) { indices[nextIndex] = 0; nextIndex--; continue; } nextIndex = maxIndex; if (!allowEmptyGroups && indices.Distinct().Count() != keys.Count) { continue; } yield return indices.Select((keyIndex, valueIndex) => new { Key = keys[keyIndex], Value = values[valueIndex] }) .OrderBy(x => x.Key) .ToLookup(x => x.Key, x => x.Value); } }
Application:
var employees = new List<string>() { "Adam", "Bob"}; var jobs = new List<string>() { "1", "2", "3"}; var c = 0; foreach (var group in CombinationsEx.Group(employees, jobs)) { foreach (var sub in group) { Console.WriteLine(sub.Key + ": " + string.Join(", ", sub)); } c++; Console.WriteLine(); } Console.WriteLine(c + " combinations.");
Conclusion:
Adam: 1, 2 Bob: 3 Adam: 1, 3 Bob: 2 Adam: 1 Bob: 2, 3 Adam: 2, 3 Bob: 1 Adam: 2 Bob: 1, 3 Adam: 3 Bob: 1, 2 6 combinations.
UPDATE
Prototype Protocol of Combined Keys:
public static IEnumerable<ILookup<TKey[], TValue>> GroupCombined<TKey, TValue> (List<TKey> keys, List<TValue> values) { // foreach (int i in Enumerable.Range(1, keys.Count)) for (var i = 1; i <= keys.Count; i++) { foreach (var lookup in Group(Enumerable.Range(0, i).ToList(), keys)) { foreach (var lookup1 in Group(lookup.Select(keysComb => keysComb.ToArray()).ToList(), values)) { yield return lookup1; } } } /* Same functionality: return from i in Enumerable.Range(1, keys.Count) from lookup in Group(Enumerable.Range(0, i).ToList(), keys) from lookup1 in Group(lookup.Select(keysComb => keysComb.ToArray()).ToList(), values) select lookup1; */ }
There is still a slight duplicate problem, but it gives all the results.
Here is what I would use to remove duplicates as a workaround :
var c = 0; var d = 0; var employees = new List<string> { "Adam", "Bob", "James" }; var jobs = new List<string> {"1", "2"}; var prevStrs = new List<string>(); foreach (var group in CombinationsEx.GroupCombined(employees, jobs)) { var currStr = string.Join(Environment.NewLine, group.Select(sub => string.Format("{0}: {1}", string.Join(", ", sub.Key), string.Join(", ", sub)))); if (prevStrs.Contains(currStr)) { d++; continue; } prevStrs.Add(currStr); Console.WriteLine(currStr); Console.WriteLine(); c++; } Console.WriteLine(c + " combinations."); Console.WriteLine(d + " duplicates.");
Conclusion:
Adam, Bob, James: 1, 2 Adam, Bob: 1 James: 2 James: 1 Adam, Bob: 2 Adam, James: 1 Bob: 2 Bob: 1 Adam, James: 2 Adam: 1 Bob, James: 2 Bob, James: 1 Adam: 2 7 combinations. 6 duplicates.
Note that it will also create non-combined groups (if possible, due to the lack of empty groups). To create ONLY combination keys, you need to replace this:
for (var i = 1; i <= keys.Count; i++)
Wherein:
for (var i = 1; i < keys.Count; i++)
At the beginning of the GroupCombined method. Test the method with three people and three tasks to see how it works.
OTHER CHANGE:
The best way to handle duplicates is to handle duplicate key combinations at the GroupCombined level:
public static IEnumerable<ILookup<TKey[], TValue>> GroupCombined<TKey, TValue> (List<TKey> keys, List<TValue> values) { for (var i = 1; i <= keys.Count; i++) { var prevs = new List<TKey[][]>(); foreach (var lookup in Group(Enumerable.Range(0, i).ToList(), keys)) { var found = false; var curr = lookup.Select(sub => sub.OrderBy(k => k).ToArray()) .OrderBy(arr => arr.FirstOrDefault()).ToArray(); foreach (var prev in prevs.Where(prev => prev.Length == curr.Length)) { var isDuplicate = true; for (var x = 0; x < prev.Length; x++) { var currSub = curr[x]; var prevSub = prev[x]; if (currSub.Length != prevSub.Length || !currSub.SequenceEqual(prevSub)) { isDuplicate = false; break; } } if (isDuplicate) { found = true; break; } } if (found) { continue; } prevs.Add(curr); foreach (var group in Group(lookup.Select(keysComb => keysComb.ToArray()).ToList(), values)) { yield return group; } } } }
It seems to be wise to add restrictions to the method so that TKey
ICompareable<TKey>
and possibly also IEquatable<TKey>
.
The result is 0 duplicates at the end.