【问题标题】:Algorithm for grouping friends at the cinema在电影院分组朋友的算法
【发布时间】:2012-04-04 18:58:52
【问题描述】:

我为您准备了一个脑筋急转弯 - 它并不像听起来那么简单,所以请阅读并尝试解决问题。在你问它是否是家庭作业之前 - 它不是!我只是想看看是否有解决这个问题的优雅方法。问题来了:

X 位朋友想去看电影并想坐下 在最佳可用组中。最好的情况是每个人都坐在一起 最坏的情况是每个人都独自坐着。较少的组优于较多的组。优选平衡基团(3+3比4+2更优选)。 最不喜欢独坐。

输入是去电影院的人数,输出应该是一个整数数组,包含:

  • 有序组合(最优先考虑)
  • 每组人数

以下是一些去电影院的人数示例以及这些人可以就座的首选组合列表:

  • 1人:1
  • 2人:2、1+1
  • 3人:3、2+1、1+1+1
  • 4人:4、2+2、3+1、2+1+1、1+1+1+1
  • 5人:5、3+2、4+1、2+2+1、3+1+1、2+1+1+1、1+1+1+1+1
  • 6人: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

7 人以上组合爆炸的例子,但我想你现在明白了。

问题是:解决这个问题的算法是什么样的? 我选择的语言是 C#,所以如果你能用 C# 给出答案,那就太好了!

【问题讨论】:

  • 属于 Programmers.SE 还是 CodeGolf.SE?
  • 如果Fewer groups are preferred over more groups,那么为什么2+2+25+1 更受欢迎?
  • 您只是在枚举可能的分区并应用一些(尚未确定的)排名函数。这是一个更适用(恕我直言)的维基链接:en.wikipedia.org/wiki/Integer_partition
  • @KendallFrey,不完全正确。正如 Tim 指出的那样,让某人独自坐着是最不理想的,这就是为什么 2+2+25+1 更受欢迎,如 6 人示例中所示
  • 投票重新开放:如果这个问题被标记为“interview-question”,它就不会被关闭

标签: c# algorithm


【解决方案1】:

我认为您需要递归地执行此操作,但您需要确保不要一遍又一遍地对同一个组进行分区。这将为您提供指数级的执行时间。在我的解决方案中,看起来我有 O(n*n) (您可以为我验证它;),请参见下面的结果。另一件事是您提到的愿望功能。我不知道这样的功能会是什么样子,但您可以改为比较 2 个分区。例如分区 1 + 1 + 2 + 4 不如 1 + 2 + 2 + 3 可取,因为它有两个“1”。一般规则可能是“如果一个分区比另一个分区拥有更多相同数量的人,那么它就不那么受欢迎了”。有道理,坐在一起的人越多越好。我的解决方案采用这种方法来比较 2 个可能的分组,我得到了你想要达到的结果。让我先给你看一些结果,然后是代码。

        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 人:1

2人:2、1+1

3人:3、1+2、1+1+1

4人:4、2+2、1+3、1+1+2、1+1+1+1

5人:5、2+3、1+4、1+2+2、1+1+3、1+1+1+2、1+1+1+1+1

6人: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

性能看起来是 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

现在让我发布解决方案中涉及的 3 个类:

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;
    }
}

现在解决方法:

脑筋急转弯类进行递归。此类中的一个技巧是在哈希集中使用自定义比较器 (PossibleGroupingComparer)。这将确保当我们计算新的分组(例如 1+1+2 与 2+1+1)时,它们将被视为相同(我们的集合将只包含每个等效分组的一个表示)。这应该将指数运行时间减少到 O(n^2)。

下一个技巧是可以对结果进行排序,因为我们的PossibleGroupings 类实现了 IComparable。 Compare() 方法的实现使用了上面提到的思想。此方法本质上包含此解决方案中的盐,如果您想对其进行不同的分组,您应该只修改此方法。

我希望你能理解代码,否则请告诉我。我试图让它可读并且不太关心性能。例如,您可以仅在将分组返回给调用者之前对它们进行排序,递归中的排序不会带来太多。

但有一条评论:一个典型的场景可能是电影院“已经”预订了很多座位,并且不允许“任何”分区。这里需要获取所有分区,然后一一检查是否可以用于当前影院。这行得通,但会花费不必要的 CPU。相反,我们可以使用输入来减少递归次数并提高整体执行时间。也许有人想为此发布解决方案;)

【讨论】:

  • 这里我不太确定,但我认为该算法可以归类为动态规划问题(en.wikipedia.org/wiki/Dynamic_programming)。呈指数增长但一遍又一遍地解决相同问题的分治算法可以使用缓存来提高性能。一个突出的例子是列文斯坦距离。
  • 我刚刚注意到我的解决方案存在风险。可比较的实现必须保证是可传递的:如果 a
  • @shaft:如果排序不是传递的,它不会导致无限循环。相反,它会导致不同的有序列表,具体取决于元素的排列顺序。
  • @shaft 太棒了!昨晚我在破解我自己的解决方案,并想出了一个蹩脚而缓慢的解决方案——但至少它奏效了。这看起来很酷!
【解决方案2】:

假设我对您的理解正确,您可以递归地执行此操作。

  • 对于一个人,唯一的分组是1
  • 对于n 人,分组是1 人和剩余n-1 人的分组,2 人和剩余n-2 人的分组,依此类推。

一旦您有了可能的分组列表,您就可以根据您想要的任何标准按“合意性”对它们进行排序。

【讨论】:

  • 这也是我最初的想法。直到我开始编写代码来解决问题。但是对数组进行排序并不像听起来那么容易。此外,2+1 和 1+2 是相同的组合。
  • @TimSkauge:最难的部分是创建一个计算“可取性”分数的函数。拥有该功能后,您可以将其用作排序标准。
【解决方案3】:

这是一个使用已知最快的算法枚举所有分区的函数

    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;
    }

这里有一个函数会根据您的偏好重新排序返回的分区列表(上图):

    /// <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;
    }

最后,这是一个可以从控件事件中调用的方法,以调用这些函数并将结果显示在 ListBox 中。 (注意:“Partitons”是包含上述函数的类)

    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);
        }
    }

【讨论】:

    猜你喜欢
    • 2017-08-12
    • 2012-02-06
    • 1970-01-01
    • 1970-01-01
    • 2015-10-21
    • 1970-01-01
    • 2011-12-12
    • 1970-01-01
    • 2011-08-10
    相关资源
    最近更新 更多