我经常教授大型入门编程课程(400-600名学生),当考试时间到来时,我们经常需要将课程分成不同的房间,以确保每个人都有考试的席位。
为了保持后勤方面的简单,我通常会将姓名分开。例如,我可能会将姓氏为A - H的学生发送到一个房间,将姓氏I - L发送到第二个房间,将M - S发送到第三个房间,将T - Z发送到第四个房间。
这样做的挑战是房间通常具有截然不同的容量,很难找到一种方法来分类,使每个人都适应。例如,假设姓氏的分布是(为简单起见)以下内容:
假设我有容量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放在另一个房间里。
谢谢!
答案 0 :(得分:6)
可以使用[m, 2^n]
空间上的某种DP解决方案来解决,其中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
。
<强> [编辑] 强>
经过数千次自动生成测试后,我的朋友发现代码中存在一个错误,后者构建了解决方案。它不影响主要算法,所以修复这个bug对读者来说是一种练习。
显示错误的测试用例是students = [13,75,21,49,3,12,27,7]
和rooms = [6,82,89,6,56]
。我的解决方案没有回答,但实际上有一个答案。请注意,解决方案的第一部分工作正常,但答案构造部分失败。
答案 1 :(得分: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
问题的一般实例到特定的问题实例。
希望这会有所帮助:)
答案 2 :(得分:0)
这是一种应该合理地运作的方法,给出了关于姓氏分布的常见假设。在限制范围内尽可能紧凑地将房间从最小容量填充到最大容量,没有回溯。
对于最后列出的最大房间,对于尚未列出的“其他人”来说,似乎是合理的(至少对我而言)。
答案 3 :(得分:0)
有没有理由让生活如此复杂?为什么不能为每个学生分配注册号码然后使用该号码按照你想要的方式分配它们:)你不需要编写代码,学生很开心,每个人都很开心。