【问题标题】:SQL efficient schedule generation algorithmSQL高效调度生成算法
【发布时间】:2015-12-03 12:09:12
【问题描述】:

想法

想象一下拥有分支机构的教育中心。该教育中心的课程适用于所有分支机构。

分支机构

CREATE TABLE `Branch` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;


CREATE TABLE `Course` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `active` tinyint(1) DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

房间在每个分支中由管理员生成的每门课程。例如,管理员输入数学课程的房间数。系统生成 3 个房间。换句话说,它们受到数量的限制。

CREATE TABLE `Room` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `branch_id` int(10) unsigned DEFAULT NULL,
  `course_id` int(10) unsigned DEFAULT NULL,
  `occupied_hours` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

每个教室每天都有 5 个可用的教学时间。换句话说,Math-1 在每个教学小时(共 5 个)中将有 1 个不同的学生组。

学生 - 也按分支分组。每个学生都有每周计划 (week_day_mode) 来上中学。

  • 一周的第 1 天、第 3 天、第 5 天
  • 一周的第 2、4、6 天

class字段是学校(主要学校)的年级,

CREATE TABLE `Student` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `fullname` varchar(255) NOT NULL,
  `class` tinyint(2) DEFAULT NULL,
  `branchID` int(10) unsigned DEFAULT NULL,
  `week_day_mode` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `branchID` (`branchID`)
) ENGINE=InnoDB AUTO_INCREMENT=246 DEFAULT CHARSET=utf8;

当管理员第一次注册学生时,他选择了学生想参加的所有课程。例如,如果选择了 5 门课程 StudentCourseAssoc 将为该学生填充 5 行。在测试学生每门课程的基本知识水平后,管理员在特定课程上将学生评估为“聪明”(+1)或“愚蠢”(-1)。所以knowledge_level 是学生-课程连接的价值。

CREATE TABLE `StudentCourseAssoc` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `studentID` int(10) unsigned DEFAULT NULL,
  `courseID` int(10) unsigned DEFAULT NULL,
  `knowledge_level` tinyint(1) DEFAULT NULL,
  `group_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1144 DEFAULT CHARSET=utf8;

应用程序必须:

自动分组(可以创建新组或将学生添加到现有组)每个分支的学生具有以下条件

  • 聪明和愚蠢的学生必须分在不同的小组中
  • 组可能包含一些等级组合。所以,可以把 9 年级和 10 年级混在一起。 11th 毕业(12th class 表示 sql 毕业)。但不是10-11日。 (将有两种模式:9-10、11-12)
  • 小组最多可包含 8 名学生。
  • 课程空间有限。因此,每个房间白天只能容纳 5 组
  • 每个学生都必须在 1 天内参加所有(自己)选择的课程

搜索满足上述条件的group后,如果没有找到,应用必须创建并分配学生到group。然后:

CREATE TABLE `StudentGroupAssoc` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `group_id` int(10) unsigned DEFAULT NULL,
  `student_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

CREATE TABLE `Schedule` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `group_id` int(10) unsigned DEFAULT NULL,
  `week_day_mode` tinyint(1) DEFAULT NULL,
  `hour` tinyint(1) DEFAULT NULL,
  `room_id` int(4) unsigned DEFAULT NULL,
  `teacher_id` int(10) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `Unique Room for exact time` (`week_day_mode`,`hour`,`room_id`) USING BTREE,
  UNIQUE KEY `Unique Group for exact time` (`group_id`,`week_day_mode`) USING BTREE,
  KEY `Unique Teacher for exact time` (`week_day_mode`,`hour`,`teacher_id`),
  KEY `room_id` (`room_id`),
  KEY `teacher_id` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

And here is fiddle to play with.

我做了什么

我正在尝试在知识评估期间将学生分配到group(现有的或创建新的)。例如,如果学生选择数学作为其中一门课程,当管理员评估他的数学知识并标记为正面时,程序开始为该学生选择正确的组:

  • 功能标志学生知识水平
  • 检查学生的可用时间(例如,第 1 个小时已经用完,然后他有 4 个可用时间)
  • 为搜索添加班级覆盖条件(如 9-10 年级或 11-12 年级)
  • 检查日程表,如果学生的每周计划中有任何可用时间的小组

如果没有,则尝试创建。

所以 PHP 表示看起来像这样

        //sets knowledge level of student
        $studentCourse->knowledge_level = intval($_POST["mark"]);

        //check hours of student, and keep only available hours
        $availableHours = array_combine(range(1, 5), range(1, 5));

        //Unsets students unavailable hours from possible hours
        if ($student->GroupRels)
            foreach ($student->GroupRels as $groupRel)
                unset($availableHours[$groupRel->hour]);

        //Checks available groups based on class coverage
        if (in_array($student->class, ['11', 'G']))
            $classCoverage = "11-m";
        else if (in_array($student->class, ['9', '10']))
            $classCoverage = "9-10";

        $availableGroups = Group::find()
            ->with("schedule")
            ->where([
                    "Group.class_coverage" => $classCoverage,
                    "Group.knowledge_level" => $studentCourse->knowledge_level,
                    "Group.participiant_count<8",
                    "Schedule.hour" => $availableHours,
                    'Schedule.week_day_mode' => $student->week_day_mode
                ]
            )->all();


        if (count($availableGroups) > 0) {
             //Selecting one of groups
             //adding row to StudentGroupAssoc
            //adding row to Schedule
        } else {
            $group = new Group();
            $group->branch_id = $student->branchID;
            $group->class_coverage = $classCoverage;
            $group->course_id=$studentCourse->courseID;
            $group->knowledge_level=$studentCourse->knowledge_level;
            $group->save();
            ...
            //adding row to StudentGroupAssoc
            //adding row to Schedule


        }

问题是

理论上,我这样做的方式就像买飞机票。是无错误的,并且必须工作,但它不是有效的和最优的。必须以最有效的方式满足所有分组条件:最少组数和满足有限房间数政策。这种方法很快就会产生大量不适合可用房间时间的团体。

由于我一个一个地花费数小时的时间,(在评估过程中)获得真正有效的结果变得越来越难。由于房间限制,找不到小组并且无法创建新小组的机会随着学生数小时的增加而增加。

你建议用什么来利用每个房间的每一个小时?

更新

根据@norbert_van_nobelen 的回答,我创建了“虚拟”小时表和以下视图,以获取每个学生所有可能的小时-房间-课程组合列表。

hours真正要计划的时间 hours_available 是二进制开关。 因此,在实际代码中,我们添加了 where 子句:WHERE hours_available=0 以仅获取我们想要计划的小时数:

SELECT
    `s`.`id` AS `student_id`,

IF ((ifnull(`sch`.`hour`, 0) > 0), 1, 0) AS `hour_available`,
 `d`.`hours` AS `hours`,
 `sca`.`courseID` AS `courseID`,
 `sch`.`room_id` AS `room_id`,
 `sca`.`knowledge_level` AS `knowledge_level`,
 (
    CASE
    WHEN (
        (`s`.`class` = 9)
        OR (`s`.`class` = 10)
    ) THEN
        '9-10'
    WHEN (
        (`s`.`class` = 11)
        OR (`s`.`class` = 12)
    ) THEN
        '11-12'
    ELSE
        '??'
    END
) AS `class_variant`
FROM
    (
        (
            (
                (
                    `dummy_hours` `d`
                    JOIN `Student` `s`
                )
                LEFT JOIN `StudentCourseAssoc` `sca` ON ((`s`.`id` = `sca`.`studentID`))
            )
            LEFT JOIN `StudentGroupAssoc` `b` ON ((`s`.`id` = `b`.`student_id`))
        )
        LEFT JOIN `Schedule` `sch` ON (
            (
                (
                    `sch`.`group_id` = `b`.`group_id`
                )
                AND (`d`.`hours` = `sch`.`hour`)
            )
        )
    )

使用此视图可以显示当前情况的完整场景。但我还是想不出算法来

  • 将学生分组
  • 将组放置在房间中

以最有效、最优化的方式创建最少的组数。

有什么建议吗?

【问题讨论】:

  • 为什么房间仅限一门课程?
  • @DanBracuk 这是教育中心的规定
  • 这就是赏金系统的真正用途,提升一个人的声誉,然后让其他人为我们解决系统中最复杂的广泛方面,笑声。我需要参与进来
  • 您将软件开发的几个方面混为一谈:数据库设计、算法、域分析、PHP 等。我认为它太宽泛了,因为您要求的是“最有效、最佳的方式” ":你要什么证明?
  • 在知识评估期间,您为什么要尝试将学生加入小组(现有小组或创建新小组)?是否可以推迟该任务?做出该决定的最后时刻/事件是什么?

标签: php mysql sql algorithm search


【解决方案1】:

有趣的问题,对我来说,我会提出一种方法的建议,尽管我的大脑不会以数学方式构建逻辑问题,但我被认为展示了美发之外的智慧,所以我开始了。

我可以遵循建议的缺乏约束,这让我想到了线性问题/编程,它也需要精确的约束来计算最优值。但是我们也可以将矩阵计算减半,首先将其除以 2,然后在下半部分或上半部分搜索结果,因为它不能同时在两者中。但是没有什么可以减半的,所以我认为更合理的假设是这里必须有一个对现实生活有意义的东西,否则它不会起作用或很快加起来是我的术语: D

所以我现在建议这种方法存在逻辑:从入门课程到考试的课程是线性的。因此,没有参加过入门课程的学生可能会再次参加,因为这很愚蠢(爱因斯坦等人:)。因此,每个参加过 math1 的学生都可以被排除在该课程之外。因此,如果我们采用数学 1 到 5 的渐进式方法以及必须参加所有课程的规则,其中课程级别与学生级别的差异不得超过 -2,该学生级别等于给定学生以线性方式参加的课程,那么所有参加过课程 1 的学生都可以被排除在该课程之外。因此,对于数学 2,只有具有水平或参加过课程的学生 0 和 1 可以参加。对于数学 3 级的 1 级或 2 级学生可以参加。因此,如果我们开始创建最大的学生群体,他们可以参加任何课程,并立即削减聪明和愚蠢以节省时间,因为 4 级学生永远无法参加与 0 级相同的数学课,1 名学生?

或者类似的东西。正在为此绘制该图表,但此时它看起来更像是我邻居的车库,所以我猜不要期望太多..

【讨论】:

    【解决方案2】:

    我相信您所描述的是constraint satisfaction problem 的一个版本,它经常用于解决资源分配问题。很有可能解决方案是NP-complete,或者换句话说,解决问题所需的时间会随着问题规模(在这种情况下是学生/班级/房间的数量)的增长而呈指数增长.这是计算机科学中经典的突出问题之一。没有已知的完美解决方案,但这并不意味着没有对您的情况有用的东西。在提出解决方案之前,我将尝试以更详细的方式描述您的问题。

    两个问题

    您至少有两个要解决的问题:

    1. 是否可以找到适合可用时间房间的学生-小组-班级的任意组合?
    2. 从可能的组合中,其中一种是否比另一种更优化?是否有可能在合理的时间内确定哪种组合是最佳的?

    首先,很可能没有可能的组合可以满足您的限制条件。为了证明这一点,假设你只有两个学生,只有一个教室只有一小时可用。如果两个学生可以被分到同一个小组,那么就可以将他们同时安排到一个教室。但是,如果无法将两个学生分组,例如一个是“愚蠢”,一个是“聪明”,那么没有任何资源组合可以满足您的限制。

    虽然很容易确定解决方案是否存在于我所描述的非常简单的案例中,但很难确定解决方案是否存在于任意大的学生/班级/房间集合中。

    设置合理的限制

    首先,很容易为可以注册的学生人数设置一个绝对上限。理论上的最大注册人数等于

    rooms * hours * students/room / hours/student

    例如,如果您有 100 个房间,每个房间可使用 5 小时,每个房间可容纳 8 名学生,每个学生需要学习 5 小时:

    100 * 5 * 8 / 5 = 800 students

    但是,鉴于随机收集的不同年级和能力水平的学生,您几乎不可能达到这个理论上的最大值。

    如果我们来自另一端,假设您有 500 个课时(100 个房间 * 5 小时),那么您知道您始终可以容纳至少 100 名学生(每个房间 1 名学生 * 5 小时)。诀窍是找出一个合理的上限,在 100 到 800 之间,使这个问题可以在合理的时间内解决。

    为了合理猜测这个上限应该是多少,查看组形成的限制似乎是谨慎的。

    分组约束

    学生分为两个维度:

    1. 能力等级:愚蠢、正常、聪明(D、N、C)
    2. 年级:9、10、11、12

    这意味着您有 12 类学生:9D、9N、9C、10D、10N、10C、...

    只有其中一些类别相互兼容以进行分组,这为您提供了有限数量的潜在组类型。假设您只有 12 名学生,12 种类型中的每一种中的 1 种,那么理论上的最大组类型数(假设任何学生类型都可以与任何其他类型配对)将是 12!/4! = 19,958,400。但是考虑到限制,实际可能的组类型数量会更少。事实证明,我认为我们可以安全地将小组类型减少到四种,每一种都由不同类型的学生组合而成:

    1. 9D、9N、10D、10N
    2. 9N、9C、10N、10C
    3. 11D、11N、12D、12N
    4. 11N、11C、12N、12C

    这里有一些明显的重叠,因为“普通”学生可以属于多个组类型。但我们终于开始获得一些对组队有用的信息:

    首先将最严格类别的学生分配到小组中。然后将学生添加到限制较少的组中。

    也就是说,“笨”和“聪明”类别的学生只能属于四种组类型中的一种,所以应该先分配。所以算法可能看起来像:

    1. 每门课程
    2. 选择 9/10 或 11/12 年级的所有聪明/愚蠢的学生
    3. 与该类别的学生一起创建尽可能多的 8 人小组
    4. 用“普通”学生用空位填满剩余的组
    5. 将剩余的“正常”学生分成 8 人一组

    这应该会产生尽可能少的组数的分组。这样做的问题是,它只是可能的数千个(可能是数百万个)其他分组中的一个。这个特定的分组不太可能是正确的。我们仍然可以在不同的小组中交换学生,但我们需要一种聪明的方法来做到这一点。

    调度约束

    现在您已将学生分配到小组,您可以开始将小组放在教室/时间段中。这里的主要限制是您不能将两个小组安排在一个时间段中,这将要求学生同时在多个地方。

    让我们再次从一个我们可以在脑海中想象的更简单的例子开始。假设只有四门课程,艺术、音乐、数学和科学,将在 4 个教室的 2 个时间段中教授。我们将有 8 组,每组 2 名学生,注意每个学生将分在 2 个组中,因为每个学生都参加了两个可用的课程。为简单起见,我们假设所有学生都属于同一类别,例如9N,因此可以毫无问题地在组之间交换。学生由字母 A-H 代表,一个小组由两个字母代表,例如AB组包含学生A和B。假设系统生成的第一个时间表如下所示:

             Art  Music  Math  Science
    Time_1    AB   CD     EF    AH
    Time_2    CD   EF     GH    GB
    

    每门课程都教了两次,我们看到所有小组都由一组有效的学生组成,但我们看到学生 A 和 G 都是双订的:A 在 Time_1 有两个班级,G 有两个Time_2 上课。简单的做法是在他们的科学时代交换 A 和 G:

             Art  Music  Math  Science
    Time_1    AB   CD     EF    GH
    Time_2    CD   EF     GH    AB
    

    但也有更复杂的解决方案,涉及移动大量人员并更改所有组,例如:

             Art  Music  Math  Science
    Time_1    AC   ED     GF   BH
    Time_2    BD   FC     HE   AG
    

    显然,其中一个比另一个更有效,但是计算机没有简单的方法来区分它们。作为人类,我们可以在此示例中相对较快地看到解决方案,但想象一下数十门课程,每门有 8 名学生,您会发现这很快就会变得一团糟。显然,我们不希望通过蛮力检查所有可能的排列来找到解决方案。

    当然,另一种解决方案就是增加更多的时隙,例如:

             Art  Music  Math  Science
    Time_1    AB   CD     EF    GH
    Time_2    CD   EF     H     B
    Time_3                G     A
    

    从计算上讲,这更简单、更快捷,但显然不会优化课堂空间和教师时间,当然,如果所有可能的时间段都已经有课,这是不可能的。

    对此保持聪明

    让我们退后一步,想想我们对整个系统的了解。以下是我们知道的一些事情:

    1. 相似的学生可能会选择相似的课程集
    2. 如果您有一组学生都在上同一组课程,那么安排他们很简单

    例如,如果我们有 4 名学生(2 组,每组 2 人)都想参加同一组课程,则很容易将这些组放入矩阵中:

             Class_1 Class_2
    Time_1     AB      CD
    Time_2     CD      AB
    

    这样做,我们可以提前确信不会发生冲突。这很简单,可以很好地扩展,并为我们带来第二个见解。

    首先创建一组都参加相同课程的学生。

    考虑到这一点,我们可能会将上面的算法更改为:

    1. 针对限制类别中的每个学生(即愚蠢/聪明)
    2. 遍历该类别中的所有其他学生,并从所有选择相同课程的学生中创建组
    3. 如果剩下的小组有
    4. 将学生添加到小组后,将他们从总池中删除
    5. 对所有限制类别的学生重复此操作
    6. 以网格矩阵方式安排所有这些学生
    7. 对剩余的普通学生重复此操作

    如果运气好的话,到此完成时,您将拥有更少的学生,他们的日程安排要求更具挑战性。

    下一步是什么?

    从这里开始,最明智的做法似乎取决于计划外的学生池中还剩下多少学生。可能性包括:

    • 重复上述策略,但将学生分组为 5 个共有 4 个班级
    • 根据剩余库中的请求课程创建需要创建的课程列表,然后循环遍历这些课程中的每一个,并从更严格的类别中的学生开始依次添加尽可能多的学生并填写和普通学生一起
    • 如果数量足够少,只需手动创建剩余的课程

    在某些时候,我认为您会发现手动为任何“怪人”分配时间表会更容易。

    您可能会考虑想办法给同年级学生分组的权重稍高。

    代码示例

    这里有一些可能有帮助的 sn-ps 代码。请注意,在重新阅读您的问题时,我刚刚意识到 knowledge_level 是按课程分配的,而不是分配给整个学生的。我会尝试对此进行调整。

    // function to determine whether two students have selected the same classes
    function studentsSelectedSameClasses($s1, $s2) {
        // returns true if students selected the same set of classes
        // returns false other wise
        // this takes into account knowledge_level and will consider
        // a class the same if the knowledge_levels are compatible
    }
    
    // create arrays of unscheduled students, i.e. not yet in groups, by grade
    // 9th/10th and 11th/12th together since they can be in the same classes
    $unscheduled["9_10"] = Student::find()->whereIn('class', [9,10])->all();
    $unscheduled["11_G"] = Student::find()->whereIn('class', [11,G])->all();
    
    // copy this array into another array from which we'll remove
    // students as they get put into groups
    $pool = $unscheduled;
    
    // loop through unscheduled; try to match on course selections
    foreach($unscheduled as $grade => $students) {
        foreach($students as $i => $student) {
            // make sure they are still in the pool, i.e. not already in a group
            if(!in_array($student, $pool[$grade]) continue;
    
            // now loop through other students
            foreach($pool[$grade] as $j => $potential_classmate) {
                if(studentsSelectedSameClasses($student,$potential_classmate)){
                    // create new groups for each class if necessary
                    // add both students to the groups if necessary
                    // remove them from the $pool
                    // if the group size reaches 8 go on to the next unscheduled
                }
            }
        }
    }
    
    // At this point $pool may not be empty, but you should have a bunch of 
    // easily scheduled groups and a much smaller pool to resolve
    

    感谢一个有趣的问题。我喜欢考虑它,希望这会有所帮助!

    【讨论】:

      【解决方案3】:

      这个答案只是作为时间表部分的解决方案方向,而不是 100% 好的解决方案:

      您创建的内容需要循环才能满足所有条件。

      为了更快地解决这种情况,可以在向量中工作,而不是在向量中所有位置都由 0(可用)和 1(已占用)表示。

      所以学生/数学一题:

      假设有 2 个房间和 3 个小时:那么每个房间的 math-1 向量是:

      Room 1: [0 0 0]
      Room 2: [0 0 0]
      

      基本上(至少我)不关心某个房间是否可用,只要 1 可用: 因此,在这种情况下,每个索引的 AND 可能是可用性的答案(请记住:0 可用):

      房间 1:[1 0 0] 房间 2:[0 0 0] 房间结果:[1 0 0] AND [0 0 0]=[0 0 0]

      所以 AND 可以判断第一个小时是否仍然可用。

      如果您现在将其与具有可用时间的学生(本示例中也只有 3 个)结合起来:

      学生 A:[0 0 1] 房间结果:[0 0 0] 学生使用 OR 进行此操作与房间匹配: [0 0 1] 或 [0 0 0]=[0 0 1]

      所以学生 A 会匹配到房间结果。

      在 SQL 中:数据模型(部分:缺少的是课程匹配): 表室:

      CREATE TABLE room(
      room_id INT,
      space TINYINT DEFAULT 0,
      hour INT DEFAULT 1
      );
      
      CREATE TABLE student(
      student_id INT,
      space TINYINT DEFAULT 0,
      hour INT DEFAULT 1
      )
      

      所有数据都已完整插入到表格中:在这种情况下,1 个房间,3 小时,3 个可用位置。

      INSERT INTO room VALUES (1,0,1);
      INSERT INTO room VALUES (1,0,1);
      INSERT INTO room VALUES (1,0,1);
      INSERT INTO room VALUES (1,0,2);
      INSERT INTO room VALUES (1,0,2);
      INSERT INTO room VALUES (1,0,2);
      INSERT INTO room VALUES (1,0,3);
      INSERT INTO room VALUES (1,0,3);
      INSERT INTO room VALUES (1,0,3);
      

      学生有:

      INSERT INTO student VALUES(1,0,1);   
      INSERT INTO student VALUES(1,0,2);   
      INSERT INTO student VALUES(1,1,3);   
      

      所以学生只在前两个小时有空。

      现在从查询中获取结果:

      SELECT room_id
      FROM room a
      INNER JOIN student b ON a.space=b.space AND a.hour=b.hour;
      

      这个结果只需要分成最多8个的组,其中它是SQL部分的结束和另一种编程语言的时间。

      此模型可以使用日期进行扩展,但仅使用小时和工作日时效果最佳(工作日可用性再次为 0 或 1)。

      正如我所说:这是一个概念/想法,而不是 100% 的解决方案,因此在您使用它之前需要工作.....

      【讨论】:

        猜你喜欢
        • 2010-10-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-17
        • 2018-11-13
        • 1970-01-01
        相关资源
        最近更新 更多