Algorithm for grouping friends at the cinema

Go To StackoverFlow.com

7

I got a brain teaser for you - it's not as simple as it sounds so please read and try to solve the issue. Before you ask if it's homework - it's not! I just wish to see if there's an elegant way of solving this. Here's the issue:

X-number of friends want's to go to the cinema and wish to be seated in the best available groups. Best case is that everyone sits together and worst case is that everyone sits alone. Fewer groups are preferred over more groups. Ballanced groups are preferred (3+3 is more preferred than 4+2). Sitting alone is least preferred.

Input is the number of people going to the cinema and output should be an array of integer arrays that contains:

  • Ordered combinations (most preferred are first)
  • Number of people in each group

Below are some examples of number of people going to the cinema and a list of preferred combinations these people can be seated:

  • 1 person: 1
  • 2 persons: 2, 1+1
  • 3 persons: 3, 2+1, 1+1+1
  • 4 persons: 4, 2+2, 3+1, 2+1+1, 1+1+1+1
  • 5 persons: 5, 3+2, 4+1, 2+2+1, 3+1+1, 2+1+1+1, 1+1+1+1+1
  • 6 persons: 6, 3+3, 4+2, 2+2+2, 5+1, 3+2+1, 2+2+1+1, 2+1+1+1+1, 1+1+1+1+1+1

Example with more than 7 persons explodes in combinations but I think you get the point by now.

Question is: What does an algorithm look like that solves this problem? My language by choice is C# so if you could give an answer in C# it would be fantastic!

2012-04-04 18:58
by Tim Skauge
Belongs on Programmers.SE or CodeGolf.SE - Yuck 2012-04-04 19:00
Definitively not CodeGolf, but I support the move to Programmer - Chris Laplante 2012-04-04 19:00
If Fewer groups are preferred over more groups, then why is 2+2+2 preferred over 5+1 - Tung 2012-04-04 19:07
this is about apriori algorithm - AliRıza Adıyahşi 2012-04-04 19:09
@Tung because sitting alone is the least preferred of them al - Tim Skauge 2012-04-04 19:09
You are simply enumerating the possible partitionings and applying some (as yet indeterminate) ranking function. Here's a slightly more applicable (imho) wiki-link: http://en.wikipedia.org/wiki/Integer_partitio - RBarryYoung 2012-04-04 19:10
It looks like you are sorting desirablity of combinations by the size of the smallest group (and then by the next smallest). Is that correct - Kendall Frey 2012-04-04 19:22
@KendallFrey, not entirely true. As Tim pointed out, having someone sit by himself is least desirable, which is why 2+2+2 is preferred over 5+1, as seen in the 6 persons exampl - Tung 2012-04-04 19:25
@Tung: That's what I mean. 1 < 2, therefore 5,1 < 2,2,2 - Kendall Frey 2012-04-04 19:26
I have a q, why is there no 3+1 in 4 and 4+1 in 5? Any problem with that combination - nawfal 2012-04-04 19:33
@nawfal I overlooked the 3+1 in 4 and 4+1 in 5 - there's nothing wrong with those combinations. Human error - I'll add them to the exampl - Tim Skauge 2012-04-04 19:36
possible duplicate of Can brute force algorithms scale?templatetypedef 2012-04-04 19:40
Voting for reopen: if this question was tagged "interview-question" it wouldn't have been close - BlackBear 2012-04-04 19:44
3+3 or 4+2 which is preferred? (for 6) and why - nawfal 2012-04-04 19:46
The question is clearly nothing more than an integer partition (as noticed by @RBarryYoung) plus an unspecified sort order. What to discuss? Integer partition algorithms are well known. The only enhancement one might consider is to ensure that partitions generated by the algorithm are already sorted as desired, thus enabling a generator approach (no memory required to store all exponentially many partitions). But without knowing the sort function, there's nothing we can do besides linking to the wiki - max 2012-04-04 19:49
@nawfal good question! Personally I would prefer to sit 3+3 rather than 4+2 because the groups are equal in size - but I guess it's a personal preference - Tim Skauge 2012-04-04 19:50
@max thanks. Please point me to a C# version of these well known integer partitions - it would be much appreciate - Tim Skauge 2012-04-04 19:51
.NET doesnt have a personal preference, hence lets give it: what's more balanced is more preferable : - nawfal 2012-04-04 19:53
Sorry, didn't find any in C#. But take a look at http://math.stackexchange.com/questions/18659/algorithm-for-generating-integer-partitions, http://www.programminglogic.com/integer-partition-algorithm/. And btw, it does seem like recursion would work after all, but is terribly inefficient without caching - max 2012-04-04 19:57
Tim what's stopping you from implementing it, is C# the language the big deal or the algorithm. Once you have the algorithm, the language part is fairly trivial to implement isnt it - nawfal 2012-04-04 20:02
I wrote a recursive implementation in PHP. Since the question was closed, I posted it here: http://engineering.jasonfunk.net/movie-alg.tx - jasonlfunk 2012-04-04 20:07
@nawfal problem being that the math symbols of the algorithm is confusing me. Biggest problem (currently) are the integer partitions. Sorting them I can manage - Tim Skauge 2012-04-04 20:08
Still need one more vote to re-Open .. - RBarryYoung 2012-04-05 03:24
I have written a partial solution in C#, but I cannot post it until the question is re-opened.. - RBarryYoung 2012-04-05 03:25
The specifications for preferability of constellations are incredibly bad, ambigous and incomplete. My interpretation of the problem in the grey box would be that the first priority would be to minimize the number of groups and if two constellations have equal groups, their preferability would be judged by balancedness (and balancedness has the consequence of making people sitting alone less preferable) - Alderath 2012-04-05 15:13
Also, I feel that it is not clearly defined what "balanced groups" means.

Is 5 + 5 + 2 or 6 + 4 + 2 more balanced?

5 + 5 + 2 is more balanced in the sense that the size difference between the largest and smallest numbers is 3 (as opposed to 4). 6 + 4 + 2 is more balanced in the sense that for each number, there exists another number which differs by at most 2 (as opposed to 3)

How do you distinguish between 5 + 5 + 3 + 2 or 5 + 4 + 4 + 2?

A mathematical definition of balancedness is needed - Alderath 2012-04-05 15:13



4

I think you need to do it recursively but you need to make sure that you don't keep partitioning the same group over and over again. This will give you exponential execution time. In my solution it looks I have O(n*n) (you can verify it for me ;) , see the results below. Another thing is the desirablity function you mention. I don't know how such a function could look like, but you can instead compare 2 partitions. e.g. partition 1 + 1 + 2 + 4 is less desirable then 1 + 2 + 2 + 3 because it has two 'ones'. A general rule could be 'a partition is less desirable if it has more of the same number of people grouped than another partition'. Makes sense, the more people sit together, the better. My solution takes this approach for comparing 2 possible groupings and I get the result that you wanted to achieve. Let me show you some results first, then the code.

        var sut = new BrainTeaser();

        for (int n = 1; n <= 6; n++) {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("{0} person{1}: ", n, n > 1 ? "s" : "");

            var array = sut.Solve(n).Select(x => x.ToString()).ToArray();
            sb.AppendLine(string.Join(", ", array));

            Console.WriteLine(sb.ToString());
        }

1 person: 1

2 persons: 2, 1+1

3 persons: 3, 1+2, 1+1+1

4 persons: 4, 2+2, 1+3, 1+1+2, 1+1+1+1

5 persons: 5, 2+3, 1+4, 1+2+2, 1+1+3, 1+1+1+2, 1+1+1+1+1

6 persons: 6, 3+3, 2+4, 2+2+2, 1+5, 1+2+3, 1+1+4, 1+1+2+2, 1+1+1+3, 1+1+1+1+2, 1+1+1+1+1+1

performance looks to be O(n*n):

var sut = new BrainTeaser();

 for (int n = 1; n <= 40; n++) {
   Stopwatch watch = new Stopwatch();
   watch.Start();
   var count = sut.Solve(n).Count();
   watch.Stop();
   Console.WriteLine("Problem solved for {0} friends in {1} ms. Number of solutions {2}", n, watch.ElapsedMilliseconds, count);
}

Problem solved for 1 friends in 17 ms. Number of solutions 1
Problem solved for 2 friends in 49 ms. Number of solutions 2
Problem solved for 3 friends in 2 ms. Number of solutions 3
Problem solved for 4 friends in 1 ms. Number of solutions 5
Problem solved for 5 friends in 0 ms. Number of solutions 7
Problem solved for 6 friends in 2 ms. Number of solutions 11
Problem solved for 7 friends in 0 ms. Number of solutions 15
Problem solved for 8 friends in 0 ms. Number of solutions 22
Problem solved for 9 friends in 1 ms. Number of solutions 30
Problem solved for 10 friends in 1 ms. Number of solutions 42
Problem solved for 11 friends in 4 ms. Number of solutions 56
Problem solved for 12 friends in 4 ms. Number of solutions 77
Problem solved for 13 friends in 7 ms. Number of solutions 101
Problem solved for 14 friends in 9 ms. Number of solutions 135
Problem solved for 15 friends in 15 ms. Number of solutions 176
Problem solved for 16 friends in 21 ms. Number of solutions 231
Problem solved for 17 friends in 30 ms. Number of solutions 297
Problem solved for 18 friends in 43 ms. Number of solutions 385
Problem solved for 19 friends in 61 ms. Number of solutions 490
Problem solved for 20 friends in 85 ms. Number of solutions 627
Problem solved for 21 friends in 117 ms. Number of solutions 792
Problem solved for 22 friends in 164 ms. Number of solutions 1002
Problem solved for 23 friends in 219 ms. Number of solutions 1255
Problem solved for 24 friends in 300 ms. Number of solutions 1575
Problem solved for 25 friends in 386 ms. Number of solutions 1958
Problem solved for 26 friends in 519 ms. Number of solutions 2436
Problem solved for 27 friends in 677 ms. Number of solutions 3010
Problem solved for 28 friends in 895 ms. Number of solutions 3718
Problem solved for 29 friends in 1168 ms. Number of solutions 4565
Problem solved for 30 friends in 1545 ms. Number of solutions 5604
Problem solved for 31 friends in 2025 ms. Number of solutions 6842
Problem solved for 32 friends in 2577 ms. Number of solutions 8349
Problem solved for 33 friends in 3227 ms. Number of solutions 10143
Problem solved for 34 friends in 4137 ms. Number of solutions 12310
Problem solved for 35 friends in 5300 ms. Number of solutions 14883
Problem solved for 36 friends in 6429 ms. Number of solutions 17977
Problem solved for 37 friends in 8190 ms. Number of solutions 21637
Problem solved for 38 friends in 10162 ms. Number of solutions 26015
Problem solved for 39 friends in 12643 ms. Number of solutions 31185

Let me post now the 3 classes involved in the solution:

public class BrainTeaser {
    /// <summary>
    /// The possible groupings are returned in order of the 'most' desirable first. Equivalent groupings are not returned (e.g. 2 + 1 vs. 1 + 2). Only one representant
    /// of each grouping is returned (ordered ascending. e.g. 1 + 1 + 2 + 4 + 5)
    /// </summary>
    /// <param name="numberOfFriends"></param>
    /// <returns></returns>
    public IEnumerable<PossibleGrouping> Solve(int numberOfFriends) {
        if (numberOfFriends == 1) {
            yield return new PossibleGrouping(1);
            yield break;
        }
        HashSet<PossibleGrouping> possibleGroupings = new HashSet<PossibleGrouping>(new PossibleGroupingComparer());
        foreach (var grouping in Solve(numberOfFriends - 1)) {
            // for each group we create 'n+1' new groups 
            // 1 + 1 + 2 + 3 + 4 
            // Becomes
            //      (1+1) + 1 + 2 + 3 + 4  we can add a friend to the first group
            //      1 + (1+1) + 2 + 3 + 4  we can add a friend to the second group
            //      1 + 1 + (2+1) + 3 + 4  we can add a friend to the third group
            //      1 + 1 + 2 + (3+1) + 4  we can add a friend to the forth group
            //      1 + 1 + 2 + 3 + (4+1) we can add a friend to the fifth group
            //      (1 + 1 + 2 + 3 + 4) + 1  friend has to sit alone

            AddAllPartitions(grouping, possibleGroupings);
        }
        foreach (var possibleGrouping in possibleGroupings.OrderByDescending(x => x)) {
            yield return possibleGrouping;
        }
    }

    private void AddAllPartitions(PossibleGrouping grouping, HashSet<PossibleGrouping> possibleGroupings) {
        for (int i = 0; i < grouping.FriendsInGroup.Length; i++) {
            int[] newFriendsInGroup = (int[]) grouping.FriendsInGroup.Clone();
            newFriendsInGroup[i] = newFriendsInGroup[i] + 1;
            possibleGroupings.Add(new PossibleGrouping(newFriendsInGroup));
        }
        var friendsInGroupWithOneAtTheEnd = grouping.FriendsInGroup.Concat(new[] {1}).ToArray();
        possibleGroupings.Add(new PossibleGrouping(friendsInGroupWithOneAtTheEnd));
    }
}

/// <summary>
/// A possible grouping of friends. E.g.
/// 1 + 1 + 2 + 2 + 4 (10 friends). The array is sorted by the least friends in an group.
/// </summary>
public class PossibleGrouping : IComparable<PossibleGrouping> {
    private readonly int[] friendsInGroup;

    public int[] FriendsInGroup {
        get { return friendsInGroup; }
    }

    private readonly int sum;

    public PossibleGrouping(params int[] friendsInGroup) {
        this.friendsInGroup = friendsInGroup.OrderBy(x => x).ToArray();
        sum = friendsInGroup.Sum();
    }

    public int Sum {
        get { return sum; }
    }

    /// <summary>
    /// determine which group is more desirable. Example:
    /// Consider g1: 1 + 2 + 3 + 4 vs g2: 1 + 1 + 2 + 2 + 4  
    /// Group each sequence by the number of occurrences:
    /// 
    /// group   | g1   | g2
    /// --------|-------------
    /// 1       | 1    | 2
    /// ----------------------
    /// 2       | 1    | 2
    /// ----------------------
    /// 3       | 1    | 0
    /// ----------------------
    /// 4       | 1    | 1
    /// ----------------------
    /// 
    /// Sequence 'g1' should score 'higher' because it has 'less' 'ones' (least desirable) elements. 
    /// 
    /// If both sequence would have same number of 'ones', we'd compare the 'twos'.
    /// 
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public int CompareTo(PossibleGrouping other) {
        var thisGroup = (from n in friendsInGroup group n by n).ToDictionary(x => x.Key,
                                                                             x => x.Count());

        var otherGroup = (from n in other.friendsInGroup group n by n).ToDictionary(x => x.Key,
                                                                                    x => x.Count());

        return WhichGroupIsBetter(thisGroup, otherGroup);
    }

    private int WhichGroupIsBetter(IDictionary<int, int> thisGroup, IDictionary<int, int> otherGroup) {
        int maxNumberOfFriendsInAGroups = Math.Max(thisGroup.Keys.Max(), otherGroup.Keys.Max());

        for (int numberOfFriendsInGroup = 1;
             numberOfFriendsInGroup <= maxNumberOfFriendsInAGroups;
             numberOfFriendsInGroup++) {
            // zero means that the current grouping does not contain a such group with 'numberOfFriendsInGroup'
            // in the example above, e.g. group '3'
            int thisNumberOfGroups = thisGroup.ContainsKey(numberOfFriendsInGroup)
                                         ? thisGroup[numberOfFriendsInGroup]
                                         : 0;
            int otherNumberOfGroups = otherGroup.ContainsKey(numberOfFriendsInGroup)
                                          ? otherGroup[numberOfFriendsInGroup]
                                          : 0;

            int compare = thisNumberOfGroups.CompareTo(otherNumberOfGroups);

            if (compare != 0) {
                // positive score means that the other group has more occurrences. e.g. 'this' group might have 2 groups with each 2 friends,
                // but the other solution might have 3 groups with each 2 friends. It's obvious that (because both solutions must sum up to the same value)
                // this 'solution' must contain a grouping with more than 3 friends which is more desirable.
                return -compare;
            }
        }
        // they must be 'equal' in this case.
        return 0;
    }

    public override string ToString() {
        return string.Join("+", friendsInGroup.Select(x => x.ToString()).ToArray());
    }
}

public class PossibleGroupingComparer : EqualityComparer<PossibleGrouping> {
    public override bool Equals(PossibleGrouping x, PossibleGrouping y) {
        return x.FriendsInGroup.SequenceEqual(y.FriendsInGroup);
    }

    /// <summary>
    /// may not be the best hashcode function. for alternatives look here: http://burtleburtle.net/bob/hash/doobs.html
    /// I got this code from here: http://stackoverflow.com/questions/3404715/c-sharp-hashcode-for-array-of-ints
    /// </summary>
    /// <param name="obj"></param>
    /// <returns></returns>
    public override int GetHashCode(PossibleGrouping obj) {
        var array = obj.FriendsInGroup;

        int hc = obj.FriendsInGroup.Length;
        for (int i = 0; i < array.Length; ++i) {
            hc = unchecked(hc*314159 + array[i]);
        }
        return hc;
    }
}

Now to the solution:

The brainteaser class does the recursion. One trick in this class is to use a custom comparer (PossibleGroupingComparer) in the hashset. This will make sure that when we calculate new groupings (e.g. 1+1+2 vs 2+1+1), those will be treated as the same (our set will contain only one representant for each equivalent grouping). This should reduce exponential runtime to O(n^2).

The next trick is that ordering the result is possible because our PossibleGroupings class implements IComparable. The implementation of the Compare() method uses the idea mentioned above. This method essentially contains the salt in this solution and if you want to have it grouped differently you should only modify this method.

I hope you can understand the code otherwise let me know. I tried to make it readable and didn't care much about performance. You could for instance order the groupings only before you return them to the caller, the ordering within the recursions doesn't bring much.

One comment though: A typical scenario might be that a cinema has 'already' booked many seats and won't allow for 'any' partition. Here you need to get all partitions and then check one-by-one if it's possible to use for the current cinema. That works, but costs unnecessary CPU. Instead, we could use the input to reduce the number of recursions and improve the overall execution time. Maybe someone wants to post a solution for this ;)

2012-04-05 10:51
by shaft
brilliant work - nawfal 2012-04-05 11:01
I am not quite sure here, but I think the algorithm can be classifed as dynamic programming problem (http://en.wikipedia.org/wiki/Dynamic_programming). divide and conquer algorithms that grow exponentially but keep solving the same problem over and over again can use caching to improve performance. A prominent example is the lehvenstein distance - shaft 2012-04-05 11:02
I just noticed a risk with my solution. The comparable implementation must guarantee to be transitive: if a < b and b < c, then a < c, otherwise the ordering might run infinitely. I am not sure how to proove that (but it seems to work ;) - shaft 2012-04-05 11:15
@shaft: If the ordering is not transitive it won't lead to an infinitely loop. Instead it would lead to different ordered lists depending in which order the elements are in the first place - Oliver 2012-04-05 13:13
@shaft Amazing! Were hacking away on my own solution last night and came up with a crappy and slow solution - but at least it worked. This looks cool - Tim Skauge 2012-04-05 18:08


2

Assuming I've understood you correctly, you could do this recursively.

  • For one person, the only grouping is 1.
  • For n people, the groupings are all those for a grouping of 1 person and the remaining n-1 people, a grouping of 2 people and the remaining n-2 people, and so on.

Once you have your list of possible groupings, you could sort them by "desirability" based on whatever criteria you want.

2012-04-04 19:06
by Li-aung Yip
That were my initial thought too. That were until I started to write the code to would solve the problem. But sorting the arrays aren't as easy as it sounds. Furthermore 2+1 and 1+2 are the same combination - Tim Skauge 2012-04-04 19:14
@TimSkauge: That hardest part is creating a function that calculates a "desirability" score. Once you have that function, you can use it as criteria for sorting - Chris Laplante 2012-04-04 23:36


1

Here is a function that enumerates all partitions using the fastest know algorithim

    public static List<List<int>> EnumerateAll(int n)
    {
        /* Fastest known algorithim for enumerating partitons
         * (not counting the re-ordering that I added)
         * Based on the Python code from http://homepages.ed.ac.uk/jkellehe/partitions.php
         */
        List<List<int>> lst = new List<List<int>>();
        int[] aa = new int[n + 1];
        List<int> a = new List<int>(aa.ToList<int>());
        List<int> tmp;
        int k = 1;

        a[0] = 0;
        int y = n - 1;

        while (k != 0)
        {
            int x = a[k - 1] + 1;
            k -= 1;
            while (2 * x <= y)
            {
                a[k] = x;
                y -= x;
                k += 1;
            }
            int l = k + 1;
            while (x <= y)
            {
                a[k] = x;
                a[l] = y;

                // copy just the part that we want
                tmp = (new List<int>(a.GetRange(0, k + 2)));

                // insert at the beginning to return partions in the expected order
                lst.Insert(0, tmp);
                x += 1;
                y -= 1;
            }
            a[k] = x + y;
            y = x + y - 1;

            // copy just the part that we want
            tmp = (new List<int>(a.GetRange(0, k + 1)));

            // insert at the beginning to return partions in the expected order
            lst.Insert(0, tmp);
        }

        return lst;
    }

And here is a function that will reorder the list of partitions returned (above) according to your preference:

    /// <summary>
    /// ReOrders a list of partitons placing those with the smallest groups last
    ///   NOTE: this routine assumes that each partitoning lists the smallest 
    ///   integers *first*.
    /// </summary>
    public static IList<List<int>> ReOrderPartitions(IList<List<int>> source)
    {
        // the count is used in several places
        long totalCount= source.Count;
        long k = 0;     // counter to keep the keys unique

        SortedList<long, List<int>> srt = new SortedList<long, List<int>>(source.Count);

        foreach (List<int> prt in source)
        {
            srt.Add(-(prt[0] * totalCount) + k, prt);
            k++;
        }

        return srt.Values;
    }

Finally, here is a method that can be called from a control event to call these functions and display the results in a ListBox. (note: "Partitons" is the class containing the functions, above)

    private void ListPreferredPartitons(int n, ListBox listOut)
    {
        IList<List<int>> pts = Partitions.EnumerateAll(n);
        pts = Partitions.ReOrderPartitions(pts);

        listOut.Items.Clear();

        foreach (List<int> prt in pts)
        {
            // reverse the list, so that the largest integers will now be first.
            prt.Reverse();
            string lin = "";
            foreach (int k in prt)
            {
                if (lin.Length > 0) lin += ", ";
                lin += k.ToString();
            }
            listOut.Items.Add(lin);
        }
    }
2012-04-05 15:28
by RBarryYoung
Ads