時間割編成問題: モデルの定式化と求解¶

1. はじめに¶

本資料は時間割編成問題の本編であり、データ整形課題で作成した定数を使って、270科目、教員100名、教室60室の実規模の時間割を実際に編成する。導入編(timetable_introduction)でpandas とPuLP の基礎を学び、データ整形課題で a.csv、h.csv、d.csv、m.csv と対応表3種を作成してから取り組むこと。

本資料の定式化は次の論文に基づく。

  • 柿本陽平、高橋弘毅、島川陽一、制約充足問題を線形計画法で解く場合の制約条件の緩和とその評価 — 時間割編成を例に、日本経営工学会論文誌、vol.66、no.4、pp.348-354、2016。

この論文の主題は「制約条件を課しすぎると問題が実行不能(解なし)になる。ではどの制約が悪さをしているのか、どう緩和すべきか」であり、本資料でもその過程を追体験する。

2. 問題設定¶

1週間は5日(曜日 $w = 1, \dots, 5$)、1日は4コマ(時限 $p = 1, \dots, 4$)とする。クラスは学年と学科の組(g, d)で定まる。各科目には担当できる教員、使える教室、週に必要なコマ数(単位数)が決まっており、教員や教室が同じ時間帯に重複しないように、全科目を曜日と時限に割り当てる。

科目は3種類に分かれる。

  • 固定科目(集合 $\mathrm{Sub}_2$): 開講する曜日と時限があらかじめ決まっている科目。
  • 連続科目(集合 $\mathrm{Sub}_1$): 2時限連続で行う科目(実験や実習)。
  • 一般科目: 上記以外。

3. 定式化¶

3.1 定数¶

データ整形課題で作成したものと、あらかじめ与えられるものがある。

定数 意味 ファイル
$a_{st}$ 教員 $t$ が科目 $s$ を担当できるなら1 a.csv(課題で作成)
$h_{sr}$ 教室 $r$ で科目 $s$ を行えるなら1 h.csv(課題で作成)
$d_s$ 科目 $s$ に必要な教員数 d.csv(課題で作成)
$m_s$ 科目 $s$ に必要な教室数 m.csv(課題で作成)
$b_{sgd}$ 科目 $s$(学年 $g$ 学科 $d$)の週に必要な単位数 b.csv
$e_{sgdwp}$ 固定科目 $s$ を曜日 $w$ 時限 $p$ に行うなら1 e.csv
$l_{gdwp}$ 学年 $g$ 学科 $d$ が曜日 $w$ 時限 $p$ に授業をしてよいなら1 g.csv
$\mathrm{Sub}_1, \mathrm{Sub}_2$ 連続科目、固定科目の集合 sub1_sub2.txt

3.2 決定変数¶

すべて0-1変数である。

  • $x_{stwp}$: 教員 $t$ が曜日 $w$ 時限 $p$ に科目 $s$ を行うなら1
  • $y_{sgdwp}$: 学年 $g$ 学科 $d$ で曜日 $w$ 時限 $p$ に科目 $s$ を行うなら1(クラスの時間割)
  • $z_{srwp}$: 教室 $r$ で曜日 $w$ 時限 $p$ に科目 $s$ を行うなら1
  • $k_{gdwp}$: 学年 $g$ 学科 $d$ が曜日 $w$ 時限 $p$ に授業を行うなら1

本資料のデータでは科目は「科目名+学年+学科」で1科目と定義されているため、科目 $s$ が属する $(g, d)$ は一意に定まる。以下では $y_{sgdwp}$ を $y_{swp}$ と略記する。

3.3 制約条件¶

論文のCR1〜CR12 をそのまま用いる。目的関数は設けず、すべての制約を満たす解(実行可能解)を探す制約充足問題として扱う。

  • CR1: 教員 $t$ が同じ時間帯に受け持つ科目は1つまで: $\sum_{s: a_{st}=1} x_{stwp} \leq 1$
  • CR2: 教室 $r$ で同じ時間帯に行う科目は1つまで: $\sum_{s: h_{sr}=1} z_{srwp} \leq 1$
  • CR3: 科目 $s$ は週にちょうど $b_{sgd}$ 回行う: $\sum_w \sum_p y_{swp} = b_{sgd}$
  • CR4: クラスの各時間帯の科目数は $k_{gdwp}$ に一致(授業をするなら1科目): $\sum_{s \in (g,d)} y_{swp} = k_{gdwp}$
  • CR5: 科目を行う時間帯には必要教員数ちょうどの教員を割り当てる: $\sum_t x_{stwp} = d_s y_{swp}$
  • CR6: 同じく必要教室数ちょうどの教室を割り当てる: $\sum_r z_{srwp} = m_s y_{swp}$
  • CR7: 固定科目は決められた時間に行う: $y_{swp} = e_{sgdwp}$($s \in \mathrm{Sub}_2$)
  • CR8: 授業をしてはいけない時間帯には授業を入れない: $k_{gdwp} = 0$($l_{gdwp} = 0$ の時間帯)
  • CR9: 連続科目は2時限連続で行う: $y_{sw1} = y_{sw2}$、$y_{sw3} = y_{sw4}$($s \in \mathrm{Sub}_1$)
  • CR10: 授業間に空きができないよう、2、3時限目には授業を割り当てる: $k_{gdwp} = 1$($p \in \{2, 3\}$、固定科目や授業不可の時間帯を除く)
  • CR11: 連続科目と固定科目を除く科目は1日2回以上行わない: $\sum_p y_{swp} \leq 1$
  • CR12: 連続科目は午後(3、4時限目)に割り当てる: $y_{sw1} = y_{sw2} = 0$($s \in \mathrm{Sub}_1$)

4. 実装¶

4.1 データの読み込み¶

データ整形課題で作成した a.csv、h.csv、d.csv、m.csv、対応表3種をこのフォルダに置いてから実行すること。

In [1]:
import csv
import re
import time
import pulp
import pandas as pd

rd = lambda p: [r for r in csv.reader(open(p, encoding='utf-8-sig'))][1:]

# データ整形課題で作成した定数
a = [(int(s), int(t)) for s, t, _ in rd('a.csv')]
h = [(int(s), int(r)) for s, r, _ in rd('h.csv')]
d = {int(s): int(v) for s, v in rd('d.csv')}
m = {int(s): int(v) for s, v in rd('m.csv')}

# あらかじめ与えられる定数
b = {int(s): (int(g), int(dp), int(v)) for s, g, dp, v in rd('b.csv')}
e = {(int(s), int(g), int(dp), int(p), int(w)) for s, g, dp, p, w, v in rd('e.csv') if int(v) == 1}
l0 = {(int(g), int(dp), int(p), int(w)) for g, dp, p, w, v in rd('g.csv') if int(v) == 0}
txt = open('sub1_sub2.txt').read()
S1 = set(map(int, re.search(r'Subject1 = ([^;]+)', txt).group(1).split(',')))
S2 = set(map(int, re.search(r'Subject2 = ([^;]+)', txt).group(1).split(',')))

W = range(1, 6)   # 曜日
Pr = range(1, 5)  # 時限
subs = list(b)
GD = sorted({(g, dp) for g, dp, _ in b.values()})

# 科目ごとの担当可能教員と利用可能教室
Ts = {}; Rs = {}
for s, t in a: Ts.setdefault(s, []).append(t)
for s, r in h: Rs.setdefault(s, []).append(r)
sub_by_t = {}; sub_by_r = {}
for s, t in a: sub_by_t.setdefault(t, []).append(s)
for s, r in h: sub_by_r.setdefault(r, []).append(s)

print(f'科目 {len(subs)}, 教員 {len(sub_by_t)}, 教室 {len(sub_by_r)}, クラス {len(GD)}')
print(f'連続科目 {len(S1)}, 固定科目 {len(S2)}')
科目 270, 教員 100, 教室 60, クラス 20
連続科目 20, 固定科目 26

4.2 モデル構築¶

制約CR9〜CR12 は引数で有効と無効を切り替えられるようにしておく。あとで「どの制約を課すと解けなくなるか」を実験するためである。

In [2]:
def build_model(cr9=False, cr10=False, cr11=False, cr12=False):
    prob = pulp.LpProblem('timetable', pulp.LpMinimize)
    y = {(s, w, p): pulp.LpVariable(f'y_{s}_{w}_{p}', cat='Binary') for s in subs for w in W for p in Pr}
    x = {(s, t, w, p): pulp.LpVariable(f'x_{s}_{t}_{w}_{p}', cat='Binary') for s in subs for t in Ts[s] for w in W for p in Pr}
    z = {(s, r, w, p): pulp.LpVariable(f'z_{s}_{r}_{w}_{p}', cat='Binary') for s in subs for r in Rs[s] for w in W for p in Pr}
    k = {(g, dp, w, p): pulp.LpVariable(f'k_{g}_{dp}_{w}_{p}', cat='Binary') for g, dp in GD for w in W for p in Pr}
    prob += 0  # 目的関数なし(制約充足問題)

    # CR1: 教員の重複禁止
    for t in sub_by_t:
        for w in W:
            for p in Pr:
                prob += pulp.lpSum(x[(s, t, w, p)] for s in sub_by_t[t]) <= 1
    # CR2: 教室の重複禁止
    for r in sub_by_r:
        for w in W:
            for p in Pr:
                prob += pulp.lpSum(z[(s, r, w, p)] for s in sub_by_r[r]) <= 1
    # CR3, CR5, CR6
    for s in subs:
        g, dp, bs = b[s]
        prob += pulp.lpSum(y[(s, w, p)] for w in W for p in Pr) == bs
        for w in W:
            for p in Pr:
                prob += pulp.lpSum(x[(s, t, w, p)] for t in Ts[s]) == d[s] * y[(s, w, p)]
                prob += pulp.lpSum(z[(s, r, w, p)] for r in Rs[s]) == m[s] * y[(s, w, p)]
    # CR4: クラスの時間帯には高々1科目(授業をするならちょうど1科目)
    for (g, dp) in GD:
        ss = [s for s in subs if b[s][0] == g and b[s][1] == dp]
        for w in W:
            for p in Pr:
                prob += pulp.lpSum(y[(s, w, p)] for s in ss) == k[(g, dp, w, p)]
    # CR7: 固定科目
    for s in S2:
        g, dp, _ = b[s]
        for w in W:
            for p in Pr:
                prob += y[(s, w, p)] == (1 if (s, g, dp, p, w) in e else 0)
    # CR8: 授業不可の時間帯
    for (g, dp, p, w) in l0:
        if (g, dp) in GD:
            prob += k[(g, dp, w, p)] == 0
    # CR9: 連続科目は2時限連続
    if cr9:
        for s in S1:
            for w in W:
                prob += y[(s, w, 1)] == y[(s, w, 2)]
                prob += y[(s, w, 3)] == y[(s, w, 4)]
    # CR10: 2、3時限目に空きを作らない(固定科目や授業不可の時間帯を除く)
    if cr10:
        R3gd = {(g, dp, p, w) for s, g, dp, p, w in e}
        for (g, dp) in GD:
            for w in W:
                for p in (2, 3):
                    if (g, dp, p, w) not in R3gd and (g, dp, p, w) not in l0:
                        prob += k[(g, dp, w, p)] == 1
    # CR11: 一般科目は1日1回まで
    if cr11:
        for s in subs:
            if s not in S1 and s not in S2:
                for w in W:
                    prob += pulp.lpSum(y[(s, w, p)] for p in Pr) <= 1
    # CR12: 連続科目は午後のみ
    if cr12:
        for s in S1:
            for w in W:
                prob += y[(s, w, 1)] == 0
                prob += y[(s, w, 2)] == 0
    return prob, y

prob, y = build_model()
n_vars = len(prob.variables())
print(f'変数 {n_vars}, 制約 {len(prob.constraints)} (CR1-CR8 のみの場合)')
変数 22220, 制約 15228 (CR1-CR8 のみの場合)

4.3 制約を段階的に課して解く¶

まず基本の制約CR1〜CR8 だけで解き、CR9〜CR12 を順に追加して、どこまで実行可能かを調べる。これは上記論文の数値実験(方法1)の追体験である。

In [3]:
cases = [
    ('CR1-CR8', {}),
    ('+CR9(連続)', {'cr9': True}),
    ('+CR9+CR11(1日1回)', {'cr9': True, 'cr11': True}),
    ('+CR9+CR10(空き禁止)+CR11', {'cr9': True, 'cr10': True, 'cr11': True}),
    ('+CR9+CR10+CR11+CR12(午後限定)', {'cr9': True, 'cr10': True, 'cr11': True, 'cr12': True}),
]
results = {}
for name, kw in cases:
    prob, y = build_model(**kw)
    t0 = time.time()
    prob.solve(pulp.PULP_CBC_CMD(msg=0, timeLimit=600))
    status = pulp.LpStatus[prob.status]
    print(f'{name}: {status} ({time.time() - t0:.1f}秒)')
    results[name] = (status, prob, y)
CR1-CR8: Optimal (2.2秒)
+CR9(連続): Optimal (3.0秒)
+CR9+CR11(1日1回): Optimal (3.1秒)
+CR9+CR10(空き禁止)+CR11: Optimal (3.8秒)
+CR9+CR10+CR11+CR12(午後限定): Infeasible (5.0秒)

CR11 まで(CR10 を含む)は実行可能だが、CR12(連続科目を午後に限定)を加えると実行不能になる。つまり、このデータではすべての要望を同時に満たす時間割は存在しない。論文ではこのような場合に、制約の違反を許してペナルティ項を目的関数に加え、違反量を最小化する緩和法を提案している。どの制約を諦めるか(緩和するか)は運用者の判断であり、モデルはその判断材料を与えてくれる。

4.4 時間割表への復元¶

実行可能だった CR1〜CR11 の解から、クラスごとの時間割表を作る。科目ID のままでは読めないので、データ整形課題で作った対応表(subject_map.csv)で科目名に戻す。

In [4]:
status, prob, y = results['+CR9+CR10(空き禁止)+CR11']
smap = {int(r[0]): r[1] for r in rd('subject_map.csv')}

def class_table(g, dp):
    ss = [s for s in subs if b[s][0] == g and b[s][1] == dp]
    days = ['月', '火', '水', '木', '金']
    tbl = pd.DataFrame('', index=[f'{p}限' for p in Pr], columns=days)
    for s in ss:
        for w in W:
            for p in Pr:
                if y[(s, w, p)].value() == 1:
                    tbl.iloc[p - 1, w - 1] = smap[s]
    return tbl

print('1年1学科の時間割:')
class_table(1, 1)
1年1学科の時間割:
Out[4]:
月 火 水 木 金
1限 LHR(1年1学科) 平面基礎デザイン演習Ⅲ(1年1学科) 保健/体育実技(1年1学科) 化学(1年1学科) 基礎数学Ⅰ(1年1学科)
2限 平面基礎デザイン演習Ⅰ(1年1学科) 情報倫理(1年1学科) 基礎数学Ⅰ(1年1学科) 基礎数学Ⅱ(1年1学科) 国語(1年1学科)
3限 現代社会(1年1学科) 英語(1年1学科) 英語(1年1学科) 英語(1年1学科)
4限 基礎数学Ⅱ(1年1学科) 平面基礎デザイン演習Ⅳ(1年1学科) 表現(1年1学科) 平面基礎デザイン演習Ⅱ(1年1学科)
In [5]:
print('4年1学科の時間割:')
class_table(4, 1)
4年1学科の時間割:
Out[5]:
月 火 水 木 金
1限 デザイン実習Ⅱ(4年1学科) 英語演習(4年1学科)
2限 デザイン概論(4年1学科) 選択英語(4年1学科) デザイン実習Ⅱ(4年1学科) 自然科学概論(4年1学科)
3限 造形学(4年1学科) 応用デザイン実習Ⅰ(4年1学科) デザイン方法論(4年1学科) コンピュータグラフィックスⅡ(4年1学科) デザイン心理(4年1学科)
4限 国語(4年1学科) 応用デザイン実習Ⅰ(4年1学科) プレゼンテーション(4年1学科) 体育実技(4年1学科) デザイン実習Ⅱ(4年1学科)

どのクラスにも空きコマが2、3時限目に存在せず(CR10)、同じ科目が1日に2回現れない(CR11)ことを確認してほしい。また4年生の時間帯には、授業をしない時間帯(CR8 で g.csv により禁止された時間帯)が反映されている。

5. 課題¶

  1. CR12 だけを外した場合と、CR10 だけを外した場合で、実行可能性と求解時間がどう変わるか実験せよ。
  2. 実行不能だった CR1〜CR12 について、CR12 の対象科目(連続科目)を半分に減らすと実行可能になるか調べよ。
  3. 自分のクラスに相当する時間割を表示し、現実の時間割として不自然な点がないか考察せよ。

6. 注意事項¶

  • 本資料の実行にはデータ整形課題で作成した a.csv、h.csv、d.csv、m.csv、subject_map.csv が必要である。課題を先に完成させること。
  • 本番規模(変数約2万)でもCBC ソルバーで数秒から数分で解けるが、制約の組合せによっては時間がかかる場合がある。timeLimit を設定しておくこと。
  • 理解できない場合には指導教員まで相談すること。

7. 卒業研究概要への手引き¶

  • このテーマを卒業研究のテーマにしたい場合は、ここの対応をすること(必ず事前に相談すること)。
  • 以下、すべて含めて、A4用紙2枚にまとめること。
  • 厳密に守る必要はないが、文章量の比率を目安にすること(15%とは、A4用紙2枚分の15%という意味)。

1章 はじめに(文章量の比率: 15%)¶

  • 時間割編成問題とは何か、スケジューリング問題の中でどのような位置づけかまとめること。
  • 時間割の自動編成に関する先行研究を紹介すること。
  • 時間割編成問題を解く際に使われる整数計画法とは何か、まとめること。

2章 時間割編成問題(文章量の比率: 30%)¶

  • 時間割編成問題の数理モデル(定数、決定変数、制約条件)について、説明すること。

3章 実験(文章量の比率: 40%)¶

3.1節 概要¶

  • 第5節の課題に取り組んだ過程を説明すること。
  • 制約を段階的に課したときに解がどう変わるかを確認すること。

3.2節 結果と考察¶

  • 得られた時間割が各制約を満たしているか示すこと。
  • 制約を追加すると何が変わるか(解けなくなる場合はなぜか)を説明すること。
  • 結果から、どのようなことが言えるか、考え説明すること。

4章 おわりに(文章量の比率: 15%)¶

  • 今回の実験に対する感想を記載すること。
  • 例えば、手作業で時間割を組むのと数理モデルにより組むのでは、どちらがよさそうか、また良い理由を記載すること。

参考文献¶

参考にした資料を、2〜3件記載すること。以下、書き方の例である。

  • [1] 柿本ほか, XXXに関する分析, XXX学会論文誌, 2020.
  • [2] XXXに関する情報, http://xxx.ddd.ttt.com, 2020年4月20日閲覧

本文中で引用する場合は「柿本らはXXXを実施している [1]。また、〜」のように、どこで引用したのか明白にすること。