勤務シフト表の作成 基礎(3)【GLPK】

[**<--目次へ**](https://qiita.com/ki073/items/508b3c20eb03078703dd)


[勤務シフト表の作成 基礎(2)]
(https://qiita.com/ki073/items/1abbfa66ff7d44841b3c)からの続きです。そちらも合わせてご覧ください。
GLPKの使い方などは上の「目次へ」を辿ってみてください。

## 最適化の条件5
* 複数の業務に分けて割り当てをし、同じ日に業務の割り当てが重ならないように
* データセクションで書いた設定項目を整理して表示
* 設定項目の簡単なチェック
* 結果の出力の充実

などです。

ここに設定したデータでは、結構スカスカのシフトになりますが、通常の出勤日数の多いシフト作れるようになっています。基本的にはdataから後のデータセクションだけの変更で使えるます。
業務の割り当て回数は設定通りにできますが、日程的に偏りがでる可能性があります。その対策は、
[勤務シフト表の作成 基礎(4)【GLPK】](https://qiita.com/ki073/items/035e38d72f9014a48fe0)を見てください。

(2021.10.15 以下のプログラムを更新　checkを増やしています)

```GLPK:shift11.mod
# version 0.0.11.1
# 複数の業務に分けたシフトを作成
# 休日希望、出勤希望の設定あり 連続勤務日の制限あり

set Staff; # スタッフ名を読み込む
set Duty; # 業務名を読み込む
param max_nWorkingDays{Staff, Duty}; # 各スタッフの業務別の最大割り当て勤務日数を読み込む
param min_nWorkingDays{Staff, Duty}; # 各スタッフの業務別の最少割り当て勤務日数を読み込む
param firstDate; # 期間の最初の日付を読み込む
param lastDate;   # 最後の日付を読み込む
set Date := firstDate..lastDate; # 期間内の日付
param necessary_nStaff{Date, Duty}; # 日別の必要出勤人数を読み込む
set FixOffDuty dimen 2;   # 休日希望を読み込む
set FixOnDuty  dimen 3;   # 出勤希望を読み込む、業務別
param maxContinuousWorkDay{Staff}; # 最大連続勤務日数を読み込む、スタッフごとに設定可能
param daysFromOffDutyBeforeFirstDay{Staff}; # スタッフの期間前の連続勤務日数

# 設定項目出力
printf "\n休日希望\n";
for{d in Date}{
    printf "%2d ", d;
    printf{s in Staff : (s,d) in FixOffDuty} "\t%s", s;
    printf "\n";
}
printf "\n出勤希望\n";
for{d in Date}{
    printf "%2d ", d;
    printf{s in Staff, b in Duty : (s,d,b) in FixOnDuty} "\t%s:%s", s, b; printf "\n";
}
# printf "\n出勤可能日数\n";
# printf{s in Staff} "%s\t%d\n", s, card({d in Date : not (s,d) in FixOffDuty});

printf "\n必要勤務人数\n";
printf{b in Duty} "\t%s", b; printf "\t合計\出勤可能人数\n";
for{d in Date}{
    printf "%2d ", d;
    printf{b in Duty} "\t%d", necessary_nStaff[d,b];
    printf "\t%d\t%d\n", sum{b in Duty}necessary_nStaff[d,b], card({s in Staff : not (s,d) in FixOffDuty});
}
printf "合計";
printf{b in Duty} "\t%d", sum{d in Date}necessary_nStaff[d,b]; printf "\n";

printf "\nスタッフ別勤務割り当て可能日数\n";
printf{b in Duty} "\t%s", b; printf "\n";
for{s in Staff}{
    printf "%s", s;
    printf{b in Duty} (if min_nWorkingDays[s,b]<max_nWorkingDays[s,b] then "\t%d..%d" else (if min_nWorkingDays[s,b]==max_nWorkingDays[s,b] then "\t%d" else "\t***")), min_nWorkingDays[s,b], max_nWorkingDays[s,b];
    printf "\n";
}
printf "合計";
printf{b in Duty} (if sum{s in Staff}min_nWorkingDays[s,b]<sum{s in Staff}max_nWorkingDays[s,b] then "\t%d..%d" 
    else (if sum{s in Staff}min_nWorkingDays[s,b]==sum{s in Staff}max_nWorkingDays[s,b] then "\t%d" else "\t***")),
    sum{s in Staff}min_nWorkingDays[s,b], sum{s in Staff}max_nWorkingDays[s,b];
printf "\n\n";

# 以下のcheckで停止した時には１行上のコメントを参考にしてください
# スタッフ、業務別の最大勤務日数<最少勤務日数
check {s in Staff, b in Duty}: max_nWorkingDays[s, b]>=min_nWorkingDays[s, b];
# 最大勤務日数の合計が必要出勤人数の合計に足りない
check {b in Duty}: sum{s in Staff}max_nWorkingDays[s, b]>=sum{d in Date}necessary_nStaff[d,b];
# 必要出勤人数の合計が最少勤務日数の合計より少ない
check {b in Duty}: sum{s in Staff}min_nWorkingDays[s, b]<=sum{d in Date}necessary_nStaff[d,b];
# 休日希望の日に、出勤希望が出ている
check card({s in Staff, d in Date,  b in Duty : (s,d) in FixOffDuty and (s,d, b) in FixOnDuty})==0;
# 同じスタッフが同じ日に複数の業務の出勤を希望している
check {d in Date,  s in Staff} : sum{b in Duty : (s,d,b) in FixOnDuty}1<=1;
# 出勤希望が必要人数を上回っている
check {d in Date,  b in Duty} : sum{s in Staff : (s,d,b) in FixOnDuty}1<=necessary_nStaff[d,b];
# スタッフの出勤希望が多くて、スタッフ自身の最大割り当て勤務日数を超えている
check {s in Staff, b in Duty} : sum{d in Date : (s,d,b) in FixOnDuty}1<=max_nWorkingDays[s,b];
# 休日希望が多すぎてスタッフの最少割り当て勤務日数を満たせない
check {s in Staff} : card(Date)-sum{d in Date : (s,d) in FixOffDuty}1>=sum{b in Duty}min_nWorkingDays[s,b];

# 日付,スタッフ名、業務名が添字の配列を確保、出勤を割り当てる場合は1、休みは0が入る
# 出勤希望の場合は1以上、休日希望の場合は0以下に、それぞれ1,0に制限
var assignShiftSchedule{d in Date, s in Staff, b in Duty} binary, >= if (s,d, b) in FixOnDuty then 1 else 0, <= if (s,d) in FixOffDuty then 0 else 1;

# スタッフごとの出勤日数合計を合わす
# 最大割り当て勤務日数と最少割り当て勤務日数が一致してる場合
s.t. constrainWorkDays1{s in Staff, b in Duty: max_nWorkingDays[s,b]==min_nWorkingDays[s,b]}:
    sum{d in Date}assignShiftSchedule[d, s, b]==max_nWorkingDays[s,b];
# 最大割り当て勤務日数と最少割り当て勤務日数が異なる場合
s.t. constrainWorkDays2{s in Staff, b in Duty : max_nWorkingDays[s,b]!=min_nWorkingDays[s,b]}: sum{d in Date}assignShiftSchedule[d, s, b]<=max_nWorkingDays[s,b];
s.t. constrainWorkDays3{s in Staff, b in Duty : max_nWorkingDays[s,b]!=min_nWorkingDays[s,b]}: sum{d in Date}assignShiftSchedule[d, s, b]>=min_nWorkingDays[s,b];
# 日ごとの出勤人数を合わす
s.t. keepStaffInWorkDay{d in Date, b in Duty}: sum{s in Staff}assignShiftSchedule[d,s,b]==necessary_nStaff[d,b];
# 同じ日に複数の業務を割り当てない
s.t. constrainDutyInDay{d in Date, s in Staff}: sum{b in Duty}assignShiftSchedule[d,s,b]<=1;
# 連続勤務日数を制限
s.t. limitContinuousWorkDay1{s in Staff, d in firstDate+maxContinuousWorkDay[s]-daysFromOffDutyBeforeFirstDay[s]..firstDate+maxContinuousWorkDay[s]-1 : not exists{d1 in firstDate..d} (s,d1) in FixOffDuty}:
    sum{d2 in firstDate..d, b in Duty}assignShiftSchedule[d2, s, b]<=d-firstDate;
s.t. limitContinuousWorkDay2{s in Staff, d in firstDate+maxContinuousWorkDay[s]..lastDate : not exists{d1 in d-maxContinuousWorkDay[s]..d} (s,d1) in FixOffDuty}:
    sum{d2 in d-maxContinuousWorkDay[s]..d, b in Duty}assignShiftSchedule[d2, s, b]<=maxContinuousWorkDay[s];

solve;

# 出力
printf "\n結果\n";
printf "スタッフ別業務割り当て\n";
printf "日付";
printf{s in Staff} "\t%s", s;
printf "\n";
for{d in Date}{
    printf "%2d ", d;
    for{s in Staff}{
        printf{b in Duty : assignShiftSchedule[d,s,b]==1} "\t%s", b;
        printf (if sum{b in Duty}assignShiftSchedule[d,s,b]==0 then "\t---" else "");
    }
    printf "\n";
}

printf "\n業務別従事日数\n";
printf "氏名";
printf{b in Duty} "\t%s", b;
printf "\n";
for{s in Staff}{
    printf "%s", s;
    printf{b in Duty} "\t%d", sum{d in Date}assignShiftSchedule[d,s,b];
    printf "\n";
}
printf "合計";
printf{b in Duty} "\t%d", sum{s in Staff, d in Date}assignShiftSchedule[d,s,b]; printf "\n";

printf "\n業務別割り当て\n";
for{b in Duty}{
    printf "%s\n", b;
    for{d in Date}{
        printf "%2d ", d;
        printf{s in Staff : assignShiftSchedule[d,s,b]==1} "\t%s", s;
        printf "\n";
  }
}

data;
# 期間の最初の日付と最後の日付
param firstDate :=  1;
param lastDate := 7;

# 業務名
set Duty := "業務A" "業務B";

# スタッフ名
set Staff := "Aさん" "Bさん" "Cさん" "Dさん"
    "Eさん" "Fさん" "Gさん"
;

# 日付、業務別の必要出勤人数 defaultを1とし、それ以外の場合は後で指定
param necessary_nStaff default 1 :=
    1 "業務A" 2
    1 "業務B" 0
;

# スタッフ、業務別の最大割り当て勤務日数
param max_nWorkingDays default 1 :=
"Eさん"  "業務A" 2
;
# スタッフ、業務別の最少割り当て勤務日数
param min_nWorkingDays default 1 :=
"Fさん"  "業務B" 0
;

# 休日希望
set FixOffDuty :=
("Aさん", *)
("Bさん", *) 
("Cさん", *) 4 6
("Dさん", *) 5
("Eさん", *) 1 6
("Fさん", *) 1 5
("Gさん", *) 4
;

# 出勤希望
set FixOnDuty :=
("Aさん", *, *) 2 "業務A" 4 "業務B"
("Bさん", *, "業務A")  6
("Bさん", *, "業務B")  2
;

# 最大連続勤務日数
param maxContinuousWorkDay default 5:=
"Aさん" 6
"Bさん" 6
;

# 期間前の連続勤務日数
param daysFromOffDutyBeforeFirstDay default 0:=
;
```