CPLEXのサンプルの Staffing(人材割り当て問題 )を読み解いていきます。
オリジナルのサンプルは<サンプル導入ディレクトリ>opl/examples/opl/models/Staffing
/staffing.mod
にあります。
-
実行環境
- CPLEX 22.1.1
- Windows 11 64bit
-
サンプル
□コード
この記事でのサンプルは、オリジナルのサンプルよりデータ量をすくなくし、一部決定変数を集約してわかりやすくしています
□サンプルデータ
問題
飲食店のシフト表を作ります。
各営業日は朝、昼、夜の3つのシフトに分割されており、料理人、会計係の2つの仕事を4人の人員に割り当てます。月曜日から水曜日までのシフトを作ります。
- 一日のシフトが2シフトである場合、連続である必要があります。朝と夜のシフトのような間を飛ばしたシフトは不可です
- 一日の間に2シフト以上連続では働けません
- 夜のシフトで労働する人は、翌日の朝のシフトで労働しません
- 一人の人が一つのシフト内でできる仕事は料理人、会計係のどちらかのみです
- 各シフトには、料理人、会計係が最低一人は必要です
- 人員によってできる仕事が異なります。料理人、会計係両方できる人もいますし、どちらかだけできる人もいます
- 一部の人員は特定の日に労働できないことがあります
目的は、①各シフトには各仕事の割当要求人数がある。それを満たさないシフト数を最小化する、②作業者のシフトが偏らないようにすることです。
利用データ
- 人員関連
人員 | スキル | 出勤不可 |
---|---|---|
Abe | 料理、会計 | 水 |
Beth | 会計 | |
Charles | 料理 | |
David | 会計 |
2.割当要求人数
決定変数
- 各曜日、各シフトに人員と割り当てた仕事
目的関数
最小化:以下の①と②の合計
①各シフトには各仕事の割当要求人数がある。それを満たさないシフト数を最小化する。満たさないシフト数*(総シフト数+1)(ペナルティ)
②作業者のシフトが偏らないようにする。各人員に割り当てられたシフト数の最大差を最小化する
制約
- 一日のシフトが2シフトである場合、連続である必要があります。朝と夜のシフトのような間を飛ばしたシフトは不可です
- 一日の間に2シフト以上連続では働けません
- 夜のシフトで労働する人は、翌日の朝のシフトで労働しません
- 一人の人が一つのシフト内でできる仕事は料理人、会計係のどちらかのみです
- 各シフトには、料理人、会計係が最低一人は必要です
- 人員によってできる仕事が異なります。料理人、会計係両方できる人もいますし、どちらかだけできる人もいます
- 一部の人員は特定の日に労働できないことがあります
問題の種類
整数計画
解
実はこの問題は解なしでした。
緩和解として「一日の間に2シフト以上連続では働けません」の制約を緩和して、「水曜日のCharlesが3シフトにでる」という解を返しています。
目的関数
①6(満たさないシフト数)*7(ペナルティ)
②4(各人員に割り当てられたシフト数の最大差)
=46(ただし緩和解)
決定変数
1.各曜日、各シフトに人員と割り当てた仕事
2.割当要求人数を満たさないシフト
OPLコード解説
OPLコードの解説を行っていきます。
利用データ
まず基本データの定義を行っています。
int Totalshifts = ...; // 1日あたりの総シフト数
int Nbshifts = ...; // 1日あたりに一人が働けるシフト数
range Shifts = 1..Totalshifts;
{string} Skills = ...; // スキル
{string} Weekdays = ...; // 労働曜日
{string} People = ...; // 人員
int Req[Weekdays][Shifts][Skills] = ...; // 各シフトの割当要求人数
定義はstaffing_custom.datファイルの中で行っています。
Totalshifts=3;
Nbshifts=2;
// N=2; // 労働者が連続して働けるシフト数
Weekdays={Mon, Tue, Wed};
Skills= {cashier, cooker};
Req = [
[ [1,1], [2,2], [1,1] ],
[ [1,1], [2,2], [2,2] ],
[ [1,1], [2,2], [1,1] ]
];
次に人員関連のデータ定義を行っています。
{string} People = ...; // 人員
// Data Structure
tuple shift {
key string p;
string w;
}
tuple PSkill {
key string p;
{string} s;
}
{PSkill} PeopleSkills = ...; // 各人が持つスキルのリスト
{shift} Unavailable = ...; // 各人の出勤不可曜日
People= {Abe, Beth, Charles, David};
PeopleSkills = {
<Abe, {cooker,cashier}>,
<Beth, {cashier}>,
<Charles,{cooker}>,
<David,{cashier}>
};
Unavailable = {<Abe, Wed>};
次に未割当シフトに対するペナルティの定義を行っています。
出勤可能なシフト数+1で定義されています。
int Penalty = card(Weekdays)*Nbshifts+1; // 未割当シフトに対するペナルティ
決定変数
次に決定変数の定義を行っています。
dvarが決定変数を意味します。
決定変数1:シフト割り当て
シフトの割当です。
dvar boolean Assign[Weekdays][Shifts][People][Skills];
これらの決定変数は、求められた解では以下のようになりました。
例えば、Monの朝(1)のBethのシフトはcashierです。
データは縦持ちですが、シフトと人のクロス表で表現すると以下のようになります。
決定変数2:割当要求人数を満たさないシフト
Reqで定義した割当要求人数を満たさないシフトです。
dvar int Unfilled[Weekdays][Shifts][Skills] in 0..maxint;
これらの決定変数は、求められた解では以下のようになりました。
例えば、Monの昼(2)の会計のシフトは割当要求を1名分満たしていません。
やはりデータは縦持ちですが、シフトとスキルのクロス表で表現すると以下のようになります。
決定変数3:各人各日の勤務の最初のシフト
各人各日の勤務の最初のシフトも定義しています。
これは業務上の問題としては定義されていなかった決定変数ですが、シフトの連続性を制約するために使われています。
dvar boolean Start[Weekdays][Shifts][People];
これらの決定変数は、求められた解では以下のようになりました。
例えば、BethのMonの最初のシフトは朝(1)です。
目的関数
次に目的関数の定義を行っています。
「①各シフトには各仕事の割当要求人数がある。それを満たさないシフト数を最小化する」ので、まず決定式で割当要求人数を満たさないシフトの総数を計算します。
Unfilled
の合計を計算しています。
dexpr int TotUnfilled =
sum(w in Weekdays, s in Shifts, j in Skills) Unfilled[w][s][j];
図で表すと以下のようなイメージです。合計で6枠分のシフトが未割当です。
次に「②作業者のシフトが偏らないようにする。各人員に割り当てられたシフト数の最大差を最小化する」を求めるために、決定式で各人のシフトの総数(PAssign)を計算し、その最大値と最小値の差(Pdiff)を算出します。
//作業者のシフトが偏らないようにする。各人員に割り当てられたシフト数の最大差
dexpr int PAssign[p in People] = sum(w in Weekdays, s in Shifts, j in Skills) Assign[w][s][p][j];
dexpr int Pdiff = max(p in People) PAssign[p] - min(p in People) PAssign[p];
①と②を合わせた値を最小化します。
「①各シフトには各仕事の割当要求人数がある。それを満たさないシフト数を最小化する」を優先するので、Penalty = card(Weekdays)*Nbshifts+1
をかけています。「②各人員に割り当てられたシフト数の最大差」は最大でもcard(Weekdays)*Nbshifts
なので、それ以上の数をPenalty
として①にかけることで①を優先しています。
minimize TotUnfilled*Penalty + Pdiff;
// Note: ペナルティは、各人員に割り当てられたシフト数の最大差よりも高いため、スケジュールは常に最初に需要を満たし、次に作業負荷のバランスをとります。
制約
次に制約の定義を行っています。
subject toの中カッコで囲みます。
subject to{
---略----
}
制約1: 一日のシフトが2シフトである場合、連続である必要があります。朝と夜のシフトのような間を飛ばしたシフトは不可です
この制約はCPLEX上のモデル上では三つの制約で実現しています。
制約1-1:シフトが連続すること。シフトk中の人員の数 = kとk-1シフトの開始人員の合計数。
forall(w in Weekdays, s in Shifts)
で曜日×シフト分との制約を作っています。
各シフトに割り当てられた人数と、そのシフトとひとつ前のシフトでシフトが始まった人数が同一であることを制約しています。
forall(w in Weekdays, s in Shifts)
ct01_1:
sum(p in People, j in Skills) Assign[w][s][p][j] == sum(i in Shifts: s-Nbshifts+1 <= i <= s, j in People) Start[w][i][j];
(i in Shifts: s-Nbshifts+1 <= i <= s)
で1シフト前からの合計を求めています。
図で表すと以下のようなイメージです(月曜日分のみ)。例えば月曜の昼(2)のシフトはStart
の朝(1)、昼(2)の合計とAssign
の昼(2)の合計が一致することを示しています。
制約1-2:ある人がシフト k で開始した場合、その人は次の nbshifts シフトで勤務可能になります。
forall(w in Weekdays, s in Shifts,p in People)
で曜日×シフト×人分、そしてforall(k in Shifts: s-Nbshifts+1 <= k <= s)
で一つ前のシフト分の制約を作っています。
各シフトに割り当てられた人は、そのシフトかひとつ前のシフトでシフトが始まっていることを制約しています。シフトが始まっていないのに割り当てがあることのないように制約されています。
forall(w in Weekdays, s in Shifts,p in People) {
// ある人がシフト k で開始した場合、その人は次の nbshifts シフトで勤務可能になります。
forall(k in Shifts: s-Nbshifts+1 <= k <= s)
ct01_2:
sum(j in Skills) Assign[w][s][p][j] >= Start[w][k][p];
図で表すと以下のようなイメージです(一部のみを表示しています)。例えば月曜の昼(2)のBethのシフトはStart
の朝(1)、昼(2)のいずれかで始まっていることを示しています。
制約1-3:開始は連続できない。人がすでに開始している場合は、次の nbshifts シフトのいずれでも再度「開始」することはできません
forall(w in Weekdays, s in Shifts,p in People)
で曜日×シフト×人分、そしてforall(k in Shifts: s+1 <= k <= s+Nbshifts-1)
で一つ後のシフト分の制約を作っています。
一つ前のStart
で開始したら、次のStart
では開始ができないことを制約しています。
forall(w in Weekdays, s in Shifts,p in People) {
// 開始は連続できない。人がすでに開始している場合は、次の nbshifts シフトのいずれでも再度「開始」することはできません。
forall(k in Shifts: s+1 <= k <= s+Nbshifts-1)
ct01_3:
1-Start[w][s][p] >= Start[w][k][p];
図で表すと以下のようなイメージです(一部のみを表示しています)。
ポイントは直前の開始に*(-1)+1
でビットを反転していることです。
例えば月曜の朝(1)のAbeの開始はなかったので、昼(2)の開始はあってもなくてもかまいません。月曜の朝(1)のBethの開始はあったので、昼(2)の開始はあってはいけません(もしあると0>=1
になってしまいます)。
なお、制約の以下の式は
1-Start[w][s][p] >= Start[w][k][p];
以下のように移項して書き換えた方が理解しやすいかもしれません。連続するシフトのStartの合計が1以下になるようにという意味になります。
Start[w][s][p] + Start[w][k][p] <=1;
制約2.一日の間に2シフト以上連続では働けません
forall(w in Weekdays, p in People)
で曜日×人分の制約を作っています。
各曜日の1日分のシフト数が2以下を示しています。
forall(w in Weekdays, p in People)
ct02Shifts:
sum(s in Shifts, j in Skills)
Assign[w][s][p][j] <= Nbshifts;
図で表すと以下のようなイメージです(月曜分のみ)。
MonのBethは朝と昼のシフトで2シフトになっています。
制約3:夜のシフトで労働する人は、翌日の朝のシフトで労働しません
forall(p in People, k in Skills)
で人員×スキル分の制約を作っています。
前日の夜(3)シフトがある場合は各人の朝(1)シフトがないようにしています。
Totalshifts
は3なのでここでは夜(3)になっています。
*(-1)+1
で夜のシフトをビット反転させています。これによって夜シフトがあった場合は0
になり、朝シフトがあると<=
が成り立たなくなり制約違反になります。夜シフトがなかった場合は1
になるので、朝シフトがあっても<=
が成り立ちます。
forall(p in People, k in Skills) {
ct03_1:
Assign["Tue"][1][p][k] <= 1 - sum(j in Skills) Assign["Mon"][Totalshifts][p][j];
ct03_2:
Assign["Wed"][1][p][k] <= 1 - sum(j in Skills) Assign["Tue"][Totalshifts][p][j];
}
図で表すと以下のようになります火―月分のみ)。
Abeは月曜夜(3)のシフトがあるので火曜朝(1)は0
でないといけません。
Bethは月曜夜(3)のシフトがないので火曜朝(1)は1
でも構いません。
制約4:一人の人が一つのシフト内でできる仕事は料理人、会計係のどちらかのみです
forall(w in Weekdays, s in Shifts, p in People)
で曜日×シフト×人員分の制約を作っています。
スキルについて合計値をだしてそれが1以下という制約になっています。つまり料理人、会計係のどちらかが1になったら、もう一つのスキルは担当できないということになります。
forall(w in Weekdays, s in Shifts, p in People)
ct04Tasks:
sum(j in Skills)
Assign[w][s][p][j] <= 1;
図で表すと以下のようになります(月分のみ)。
Abeは料理人、会計係のどちらも担当可能ですが、月曜夜(3)のシフトでは、料理人のみを担当しています。
制約5:各シフトには、料理人、会計係が最低一人は必要です
forall(w in Weekdays, s in Shifts, j in Skills)
で曜日×シフト×スキル分の制約を作っています。
各シフトの割当要求人数のReq
にかかわらず、全てのシフトで料理人と会計係に一人はAssignがあるようにします。
forall(w in Weekdays, s in Shifts, j in Skills)
ct05Skills:
sum(p in People) Assign[w][s][p][j] >= 1;
図で表すと以下のようになります(火のみ)。
火曜昼(2)の会計はBethとDavidが割り当てられているので、一人以上が割り当てられています。
制約6:人員によってできる仕事が異なります。料理人、会計係両方できる人もいますし、どちらかだけできる人もいます
forall(w in Weekdays, s in Shifts, t in PeopleSkills, k in Skills: k not in t.s)
で曜日×シフト×PeopleSkills
で定義された人員×not in
で持っていないスキル分の制約を作っています。
持っていないスキルのAssignが0
であるという制約です。
t.p
とt.s
は以下のPeopleSkills
から取得しています。
forall(w in Weekdays, s in Shifts, t in PeopleSkills, k in Skills: k not in t.s)
ct06:
Assign[w][s][t.p][k] == 0;
図で表すと以下のようになります(月のみ)。
Abeは料理と会計のスキルを両方持っているので制約はありません。
Bethは会計のスキルは持っていますが、料理のスキルがないので==0
の制約があります。
この制約は全てのシフトに対して繰り返して適用されます。
制約7:一部の人員は特定の日に労働できないことがあります
forall(<p,w> in Unavailable, s in Shifts, j in Skills)
で、Unavailable
で定義された曜日×人員×シフト×スキル分の制約を作っています。
Unavailable
で定義された曜日、人員のAssignが0
であるという制約です。
p
とw
は以下のUnavailable
から取得しています。つまりAbeは水曜に出勤できません。
forall(<p,w> in Unavailable, s in Shifts, j in Skills)
ct07:
Assign[w][s][p][j] == 0;
図で表すと以下のようになります。
Abeの水曜のすべてのシフトに==0
の制約があります。
制約8:各シフトにおいて、あるタイプのタスクに割り当てられた人数 + 空いているスロット >= そのタイプの必要な人数 (必要な人数を満たしてなくてもよい )
この制約は問題の定義にはありませんでした。未割当の人数の決定変数であるUnfilled
に値をいれるための制約です。
forall(w in Weekdays, s in Shifts, j in Skills)
で、曜日×シフト×スキル分の制約を作っています。
各シフトにアサインされた人数+未割当の人数>=割当要求人数
なので、割り当て人数に満たない場合にUnfilled
へ値が入ります。さらにUnfilled
は目的関数でMinimizeされているので、基本的には割当要求人数-各シフトにアサインされた人数=未割当の人数
が入ることになります。
ただし、Unfilled
は非負で、>=
の制約なので、割当要求人数よりもアサイン人数を多くすることもこの制約は許容しています。あまりシフトに入っていない人がいる場合には、シフトの平準化の目的関数のために割当要求人数よりも多くの人員をアサインをする可能性があります。
forall(w in Weekdays, s in Shifts, j in Skills)
ct08Unfilled:
sum(p in People)
Assign[w][s][p][j] + Unfilled[w][s][j] >= Req[w][s][j];
図で表すと以下のようになります。
月曜昼(2)の会計はReq
では2
人ですが、Beth1
人しか割り当てられていないのでUnfilled
が1
人になっています。
完成OPL
製品サンプルとほぼ同じ内容ですが、日本語コメントを入れたり順序を入れ替えたり、添え字を入れ替えたりして読みやすくしています。またオリジナルのサンプルには、PersonnelやAvailという決定変数がありましたが、結局はAssignで表現できていそうだったので、割愛しています。
//**************************** Data **************************************
int Totalshifts = ...; // 1日あたりの総シフト数
int Nbshifts = ...; // 1日あたりに一人が働けるシフト数
range Shifts = 1..Totalshifts;
{string} Skills = ...; // スキル
{string} Weekdays = ...; // 労働曜日
int Req[Weekdays][Shifts][Skills] = ...; // 各シフトの割当要求人数
{string} People = ...; // 人員
// Data Structure
tuple shift {
key string p;
string w;
}
tuple PSkill {
key string p;
{string} s;
}
{PSkill} PeopleSkills = ...; // 各人が持つスキルのリスト
{shift} Unavailable = ...; // 各人の出勤不可曜日
int Penalty = card(Weekdays)*Nbshifts+1; // 未割当シフトに対するペナルティ
//********************************* 決定変数 **********************************
dvar boolean Assign[Weekdays][Shifts][People][Skills]; // シフト割り当てを示します
dvar int Unfilled[Weekdays][Shifts][Skills] in 0..maxint; // 割当要求人数を満たさないシフト
dvar boolean Start[Weekdays][Shifts][People]; // 各人各日の勤務の最初のシフト
/************************************* Model *********************************/
// the total # of slots unfilled in a week
// 割当要求人数を満たさないシフトの総数
dexpr int TotUnfilled =
sum(w in Weekdays, s in Shifts, j in Skills) Unfilled[w][s][j];
//作業者のシフトが偏らないようにする。各人員に割り当てられたシフト数の最大差
dexpr int PAssign[p in People] = sum(w in Weekdays, s in Shifts, j in Skills) Assign[w][s][p][j];
dexpr int Pdiff = max(p in People) PAssign[p] - min(p in People) PAssign[p];
minimize TotUnfilled*Penalty + Pdiff;
// Note: ペナルティは、各人員に割り当てられたシフト数の最大差よりも高いため、スケジュールは常に最初に需要を満たし、次に作業負荷のバランスをとります。
subject to {
// 制約1: 一日のシフトが2シフトである場合、連続である必要があります。朝と夜のシフトのような間を飛ばしたシフトは不可です
// シフトが連続すること。シフトk中の人員の数 = kとk-1シフトの開始人員の合計数。
forall(w in Weekdays, s in Shifts)
ct01_1:
sum(p in People, j in Skills) Assign[w][s][p][j] == sum(i in Shifts: s-Nbshifts+1 <= i <= s, j in People) Start[w][i][j];
forall(w in Weekdays, s in Shifts,p in People) {
// ある人がシフト k で開始した場合、その人は次の nbshifts シフトで勤務可能になります。
forall(k in Shifts: s-Nbshifts+1 <= k <= s)
ct01_2:
sum(j in Skills) Assign[w][s][p][j] >= Start[w][k][p];
// 開始は連続できない。人がすでに開始している場合は、次の nbshifts シフトのいずれでも再度「開始」することはできません。
forall(k in Shifts: s+1 <= k <= s+Nbshifts-1)
ct01_3:
1-Start[w][s][p] >= Start[w][k][p];
}
// In each shift, # of people assigned to a type of task +
// unfilled slot >= # of people of that type required
// 制約8:各シフトにおいて、あるタイプのタスクに割り当てられた人数 + 空いているスロット >= そのタイプの必要な人数 (必要な人数を満たしてなくてもよい )
forall(w in Weekdays, s in Shifts, j in Skills)
ct08Unfilled:
sum(p in People)
Assign[w][s][p][j] + Unfilled[w][s][j] >= Req[w][s][j];
// 制約6:人員によってできる仕事が異なります。料理人、会計係両方できる人もいますし、どちらかだけできる人もいます
forall(w in Weekdays, s in Shifts, t in PeopleSkills, k in Skills: k not in t.s)
ct06:
Assign[w][s][t.p][k] == 0;
// 制約2:一日の間に2シフト以上連続では働けません
forall(w in Weekdays, p in People)
ct02Shifts:
sum(s in Shifts, j in Skills)
Assign[w][s][p][j] <= Nbshifts;
// 制約7:一部の人員は特定の曜日に労働できないことがあります
forall(<p,w> in Unavailable, s in Shifts, j in Skills)
ct07:
Assign[w][s][p][j] == 0;
// Each person can take only one task in each shift
// 制約4:一人の人が一つのシフト内でできる仕事は料理人、会計係のどちらかのみです
forall(w in Weekdays, s in Shifts, p in People)
ct04Tasks:
sum(j in Skills)
Assign[w][s][p][j] <= 1;
// If a person is on a night shift, he cannot be assigned to the morning
// shift the next day
// 制約3:夜のシフトで労働する人は、翌日の朝のシフトで労働しません
forall(p in People, k in Skills) {
ct03_1:
Assign["Tue"][1][p][k]+ sum(j in Skills) Assign["Mon"][Totalshifts][p][j] <= 1 ;
// Assign["Tue"][1][p][k] <= 1 - sum(j in Skills) Assign["Mon"][Totalshifts][p][j];
ct03_2:
Assign["Wed"][1][p][k] <= 1 - sum(j in Skills) Assign["Tue"][Totalshifts][p][j];
}
//Each shift has at least a cook, a cleaner and a cashier
//制約5:各シフトには、料理人、会計係が最低一人は必要です
forall(w in Weekdays, s in Shifts, j in Skills)
ct05Skills:
sum(p in People) Assign[w][s][p][j] >= 1;
}
データはわかりやすくするためにスタッフ数やシフト数をオリジナルのサンプルよりも減らして小さくしています。
// 1日あたりの総シフト数
Totalshifts=3;
// 1日あたりに一人が働けるシフト数
Nbshifts=2;
// N=2; // 労働者が連続して働けるシフト数
// 労働曜日
Weekdays={Mon, Tue, Wed};
// スキル
Skills= {cashier, cooker};
// 各シフトに必要なスキルタイプの人数
Req = [
[ [1,1], [2,2], [1,1] ],
[ [1,1], [2,2], [2,2] ],
[ [1,1], [2,2], [1,1] ]
];
//Shifts= { 8to12, 12to4, 4to8};
// 人員
People= {Abe, Beth, Charles, David};
// 各人が持つスキルのリスト
PeopleSkills = {
<Abe, {cooker,cashier}>,
<Beth, {cashier}>,
<Charles,{cooker}>,
<David,{cashier}>
};
// 各人の出勤不可曜日
Unavailable = {<Abe, Wed>};
//Unavailable = {};
実行
緩和解
実はこの問題は実行可能解がありません(もともとのサンプルは実行可能ですが、簡単にするためにStaffの数などを減らしたため実行可能解がなくなっています)。そのため出てきた結果は、制約が一部緩和された緩和解になっています。
緩和
タブを開くと制約2に抵触していて、水曜日のCharlesが2シフト以上アサインした緩和解になっていることがわかります。クリックすると対象となっている制約のOPLコードが開きます。
制約2を見てみるとスラックが-1
になっていて制約違反をしていることがわかります。
Assign
を確認すると確かにCharlesは朝昼夜の3シフトにアサインされています。
競合の解決
CPLEXが出してきた緩和解を受け入れられない場合は、別の制約を緩めるか、データを修正していきます。
競合
タブを開くと競合した制約が表示されます。クリックすると対象となっている制約のOPLコードが開きます。
複数の競合した制約がありますが、ここでは、制約7で競合が起きていて、Abeが水曜日に休まなければ、競合が起きないことに注目してみます。
では、ここではデータを修正して、Abeが水曜に休むというデータUnavailable = {<Abe, Wed>};
を削除しUnavailable = {};
にしてみます。
再度求解すると、今度は競合がなくなり、実行可能解、最適解が得られました。
Abeが水曜日に出勤していることがわかります。
このように競合した制約を確認し、制約を緩和したり、データを修正することで、実行可能解を探していくことができます。
おまけ. スタッフや曜日の数が増えた場合
この元々のサンプルのstaffing.datファイルでは、スタッフが8名、月ー金までの曜日が定義されています。
このdatファイルで実行してみた結果が以下です。8名のスタッフのシフトが作成されています。
参考
人材割り当て問題 - IBM Documentation
CPLEXサンプルを読み解く記事一覧