16

我经常教大型入门编程课程(400 - 600 名学生),当考试时间到来时,我们经常不得不将班级分成不同的房间,以确保每个人都有座位参加考试。

为了使事情在逻辑上简单,我通常按姓氏将班级分开。例如,我可以将姓 A - H 的学生送到一个房间,将姓 I - L 送到第二个房间,将 M - S 送到第三个房间,将 T - Z 送到第四个房间。

这样做的挑战在于,房间的容量通常大相径庭,而且很难找到一种方法来划分班级,让每个人都适合。例如,假设姓氏的分布(为简单起见)如下:

  • 姓氏以 A 开头:25
  • 姓氏以 B 开头:150
  • 姓氏以 C 开头:200
  • 姓氏以 D 开头:50

假设我有容量为 350、50 和 50 的房间。寻找房间分配的贪心算法可能是将房间按容量降序排列,然后尝试按该顺序填充房间。不幸的是,这并不总是有效。例如,在这种情况下,正确的选择是将姓 A 放入一个大小为 50 的房间,姓 B - C 放入大小为 350 的房间,姓 D 放入另一个大小为 50 的房间。贪心算法将将姓氏 A 和 B 放入 350 人的房间,然后找不到其他人的座位。

通过尝试房间排序的所有可能排列,然后对每个排序运行贪心算法,很容易解决这个问题。这将找到有效的分配或报告不存在的分配。但是,我想知道是否有更有效的方法来做到这一点,因为房间的数量可能在 10 到 20 之间,并且检查所有排列可能不可行。

总而言之,正式的问题陈述如下:

您将获得班级中学生姓氏的频率直方图,以及房间列表及其容量。您的目标是按学生姓氏的第一个字母划分学生,以便为每个房间分配一个连续的字母块,并且不会超出其容量。

有没有一种有效的算法,或者至少有一种对合理的房间大小有效的算法?

编辑:很多人都询问过连续情况。规则是

  • 每个房间最多应该分配一个连续的字母块,并且
  • 不应将字母分配给两个或更多房间。

例如,您不能将 A - E、H - N 和 P - Z 放入同一个房间。你也不能把 A - C 放在一个房间里,把 B - D 放在另一个房间里。

谢谢!

4

4 回答 4

6

它可以在空间上使用某种 DP 解决方案来解决[m, 2^n],其中m是字母数(英文为 26)和n房间数。m == 26并且n == 20它将需要大约 100 MB 的空间和大约 1 秒的时间。下面是我刚刚在 C# 中实现的解决方案(它也可以在 C++ 和 Java 上成功编译,只需要进行一些小改动):

int[] GetAssignments(int[] studentsPerLetter, int[] rooms)
{
    int numberOfRooms = rooms.Length;
    int numberOfLetters = studentsPerLetter.Length;
    int roomSets = 1 << numberOfRooms; // 2 ^ (number of rooms)
    int[,] map = new int[numberOfLetters + 1, roomSets];

    for (int i = 0; i <= numberOfLetters; i++)
        for (int j = 0; j < roomSets; j++)
            map[i, j] = -2;

    map[0, 0] = -1; // starting condition

    for (int i = 0; i < numberOfLetters; i++)
        for (int j = 0; j < roomSets; j++)
            if (map[i, j] > -2)
            {
                for (int k = 0; k < numberOfRooms; k++)
                    if ((j & (1 << k)) == 0)
                    {
                        // this room is empty yet.
                        int roomCapacity = rooms[k];
                        int t = i;
                        for (; t < numberOfLetters && roomCapacity >= studentsPerLetter[t]; t++)
                            roomCapacity -= studentsPerLetter[t];

                        // marking next state as good, also specifying index of just occupied room
                        // - it will help to construct solution backwards.
                        map[t, j | (1 << k)] = k;
                    }
            }

    // Constructing solution.
    int[] res = new int[numberOfLetters];
    int lastIndex = numberOfLetters - 1;
    for (int j = 0; j < roomSets; j++)
    {
        int roomMask = j;
        while (map[lastIndex + 1, roomMask] > -1)
        {
            int lastRoom = map[lastIndex + 1, roomMask];
            int roomCapacity = rooms[lastRoom];
            for (; lastIndex >= 0 && roomCapacity >= studentsPerLetter[lastIndex]; lastIndex--)
            {
                res[lastIndex] = lastRoom;
                roomCapacity -= studentsPerLetter[lastIndex];
            }

            roomMask ^= 1 << lastRoom; // Remove last room from set.

            j = roomSets; // Over outer loop.
        }
    }

    return lastIndex > -1 ? null : res;
}

OP问题的示例:

int[] studentsPerLetter = { 25, 150, 200, 50 };
int[] rooms = { 350, 50, 50 };
int[] ans = GetAssignments(studentsPerLetter, rooms);

答案将是:

2
0
0
1

这表示每个学生的姓氏字母的房间索引。如果无法分配,我的解决方案将返回null


[编辑]

经过数千次自动生成的测试后,我的朋友在代码中发现了一个向后构建解决方案的错误。它不会影响主要算法,因此修复此错误将是读者的练习。

揭示错误的测试用例是students = [13,75,21,49,3,12,27,7]rooms = [6,82,89,6,56]。我的解决方案没有返回任何答案,但实际上有一个答案。请注意,解决方案的第一部分工作正常,但答案构造部分失败。

于 2013-05-04T19:03:19.363 回答
2

这个问题是NP-Complete,因此没有已知的polynomial时间(又名有效)解决方案(只要人们无法证明P = NP)。您可以将背包或装箱问题的实例简化为您的问题以证明它是NP-complete.

要解决这个问题,您可以使用 0-1 背包问题。方法如下:首先选择最大的教室,并尝试分配尽可能多的学生组(使用 0-1 背包),即等于房间的大小。您保证不会拆分一组学生,因为这是 0-1 背包。完成后,进入下一个最大的教室并继续。

(您使用任何已知的启发式方法来解决 0-1 背包问题。)

这是减少 - 您需要将 0-1 背包的一般实例减少为问题的特定实例。因此,让我们举一个 0-1 背包的一般实例。让我们拿一个重量为 的麻袋,W你有x_1, x_2, ... x_n组,它们对应的重量为w_1, w_2, ... w_n

现在减少---这个一般实例减少到您的问题如下:您有一个有座位的教室W。每个x_i (i \in (1,n))都是一组学生,他们的最后一个字母以 开头,i他们的人数(也就是小组的大小)是w_i

现在你可以证明0-1背包问题是否有解,你的问题有解……反之亦然……如果0-1背包没有解,那么你的问题也没有解,反之亦然。

请记住简化的重要事项——从已知NP-C问题的一般实例到问题的特定实例。

希望这可以帮助 :)

于 2013-05-04T17:59:21.383 回答
0

考虑到关于姓氏首字母分布的常见假设,这是一种应该可以很好地工作的方法。在约束范围内尽可能紧凑地填充房间,从最小容量到最大容量,没有回溯。

最后列出最大的房间似乎是合理的(至少对我来说),因为对于尚未列出的“其他所有人”。

于 2013-05-04T19:23:52.780 回答
0

有什么理由让生活变得如此复杂?为什么不能为每个学生分配注册号,然后使用该编号以任何您想要的方式分配它们:)您无需编写代码,学生快乐,每个人都快乐。

于 2013-05-04T20:21:30.977 回答