割当問題の制約条件を色々変えてみてpythonで解いてみた 5


こんにちは!しゅんです!

割当問題を簡単に説明すると、例えば10人の従業員を10個の仕事に割り当てるときにどのように割り当てたら効率的になるかを考える問題です。


今回の記事では割当問題の中でも

「最も負担がかかる従業員の負担を最小にする割当問題」

をpythonで解いてみたいと思います。それではやっていきましょう!

「従業員に余りが出る場合の割当問題」



「従業員が複数の仕事を担当できる場合の割当問題」



「時間帯によって異なる仕事を行う場合の割当問題」



「1日でやる仕事数に上限がある場合の割当問題」



「最も負担がかかる従業員の負担を最小にする割当問題」



普段は統計検定2級の記事を書いてたりします。
ぜひ他の記事も読んでみてください!



このブログの簡単な紹介はこちらに書いてあります。
興味があったら見てみてください。

このブログでは経営工学を勉強している現役理系大学生が、経営工学に関することを色々話していきます!


ぼくが経営工学を勉強している中で感じたことや、興味深かったことを皆さんと共有出来たら良いなと思っています。


そもそも経営工学とは何なのでしょうか。Wikipediaによると

経営工学(けいえいこうがく、英: engineering management)は、人・材料・装置・情報・エネルギーを総合したシステムの設計・改善・確立に関する活動である。そのシステムから得られる結果を明示し、予測し、評価するために、工学的な分析・設計の原理・方法とともに、数学、物理および社会科学の専門知識と経験を利用する。

引用元 : 経営工学 – Wikipedia

長々と書いてありますが、要は経営、経済の課題を理系的な観点から解決する学問です。



今回解く問題



3人の従業員に4個の仕事を割り当てることを考えます。各辺にはコストが設定されていて、このコストが大きい程従業員はその仕事をしたくないと仮定します。


また今回の記事では従業員は何個でも仕事をできると仮定します。そのため以下のような割当もOKとします。


従業員1が4つ全ての仕事を担当しています。非常に不公平感がありますがこれもOKとします。とはいえさすがにこの割当は現実味がないので、今回は目的関数を

「最も負担がかかる従業員の負担を最小にする」

とします。

ここで言う従業員の負担は「その従業員が担当する仕事に対するコストの合計」とします。例えば上の例で言うと各従業員の負担は、従業員1が\(2+3+4+6=15\)で従業員2,3,4が\(0\)です。


このように目的関数を設定することで、不公平感をなるべく無くすことができますね。

仕事は必ず誰かによって担当されるとします。



整数計画問題として定式化したものをpythonで実装する


この割当問題を混合整数計画問題として定式化したものを、pythonで実装したいと思います。前回の記事で上の問題を混合整数計画問題として定式化する方法は解説しているので、ここでは結果だけ載せておきます。

・パラメータ
\(E = \{1,2,3\}\) : 従業員の集合
\(J = \{1,2,3,4\}\) : 仕事の集合
\(w_{ij}\) : 辺\((i,j)\)の重み


・変数
\(x_{ij} \in \{0,1\} \;\; (\forall i \in E, \forall j \in J)\)
\(t\)

整数計画問題として定式化:

\(\min \;\;\; t\)
\(\;\text{s.t.}\;\;\;\sum\limits_{j \in J} w_{ij}x_{ij} \leq t \;\;\;\; (\forall i \in E)\)
\(\;\;\;\;\;\;\;\;\;\;\sum\limits_{i \in E} x_{ij} = 1 \;\;\;\; (\forall j \in J)\)
\(\;\;\;\;\;\;\;\;\; x_{ij} \in \{0,1\} \;\;( \forall i \in E, \forall j \in J)\)


「時間帯によって異なる仕事を行う場合の割当問題」を整数計画問題として定式化する記事はこちらから!



コードの全貌と実行結果


まず最初にコードの全貌実行結果を示してから、コードと結果について1つずつ解説していきたいと思います。コードの全貌は以下のようになります。

## 事前準備
!pip install pulp # pulpをインストール
from pulp import * # pulpをインポート

## パラメータの設定
E = [i for i in range(3)] # 従業員側の頂点を設定
J = [j for j in range(4)] # 仕事側の頂点を設定
w = [[2,3,4,6],[8,3,2,4],[5,1,7,4]] # 各辺の重みを設定

## 整数計画問題の設定
prob = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定
x = LpVariable.dict("x",(E,J), cat = "Binary") # 0-1変数の設定
t = LpVariable("t", cat = "Continuous")
prob += t # 目的関数の設定

for i in E:
  prob += lpSum(x[i,j] * w[i][j] for j in J) <= t

for j in J:
  prob += lpSum(x[i,j] for i in E) == 1 # 仕事側の頂点に関する制約条件を設定

result = prob.solve() # 問題を解く

## 結果の表示
print(LpStatus[result]) # 得られた結果の状態を表示(Optimalなら最適解が得られている)
for i in E:
  for j in J:
    if x[i,j].value() == 1:
      print(f"x{i}{j} =",x[i,j].value()) # 最適解(変数の値が1のものだけ)を表示
print("最適値 :", prob.objective.value()) # 最適値を表示

このコードを実行すると以下のような結果が得られました。


それでは1つずつ解説していきます。


コードを1つずつ解説する


事前準備

## 事前準備
!pip install pulp # pulpをインストール
from pulp import * # pulpをインポート

問題を解くための事前準備としてpulpをインポートします。pulpはpythonで数理最適化の問題を解くために使うライブラリです。pulpをインストールしてからインポートしています。


パラメータの設定

## パラメータの設定
E = [i for i in range(3)] # 従業員側の頂点を設定
J = [j for j in range(4)] # 仕事側の頂点を設定
w = [[2,3,4,6],[8,3,2,4],[5,1,7,4]] # 各辺の重みを設定

次に各パラメータを設定していきます。今回は完全2部グラフを考えているので頂点と各辺の重みを設定するだけでOKです。


Eが従業員側の頂点Jが仕事側の頂点を表しています。


wは各辺の重さを辞書形式で表しています。例えばw[0][2]=4ですが、これは辺\((1,3)\)の重みが4であることを表しています。つまり従業員1の仕事3に対する仕事やりたくない度は4であることを表しています。

pythonではリストのインデックスが0から始まるので1つずつ数字がずれます。




整数計画問題の設定

## 整数計画問題の設定
prob = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定
x = LpVariable.dict("x",(E,J), cat = "Binary") # 0-1変数の設定
t = LpVariable("t", cat = "Continuous")
prob += t # 目的関数の設定

for i in E:
  prob += lpSum(x[i,j] * w[i][j] for j in J) <= t

for j in J:
  prob += lpSum(x[i,j] for i in E) == 1 # 仕事側の頂点に関する制約条件を設定

result = prob.solve() # 問題を解く


それではいよいよ整数計画問題をpythonで記述していきたいと思います。少し長いのでさらにいくつかのパートに分けて説明したいと思います。


問題をMinimizeに設定

prob = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定

この行では問題の種類を設定しています。pulp.LpProblemで数理最適化の問題を作ることができます。かっこの中のsense=の所で問題の種類を設定できます。今回は最小値を求めたいのでLpMinimizeと設定します。

最大値を求めたい場合はLpMaximizeとします。



0-1変数の設定

x = LpVariable.dict("x",(E,J), cat = "Binary") # 0-1変数の設定
t = LpVariable("t", cat = "Continuous")


次に0-1変数\(x_{ij}\in\{0,1\}\)と連続変数\(t\)を設定しています。変数はLpVariableで設定できます。変数をたくさん作りたい場合は.dictを使うと便利です。かっこの中は左から順番に(”変数の名前” 添字の集合, cat=”変数の種類”)を表しています。


上の例で言うと名前はx、添字の集合は従業員の集合Eと仕事の集合J、変数はBinary(0-1変数)と設定しています。


\(t\)は連続変数にしたいのでcat = “Continuous”としています。

Binary : 0-1変数
Integer : 整数変数
Continuous : 連続変数



目的関数の設定

prob += t # 目的関数の設定

これは本筋ではありませんが、目的関数と制約条件はprob+=を最初に付けます。probは問題をMinimizeに設定したときにprob=としたのでそれに合わせます。もしMinimizeの設定の所で

problem = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定

と書いたら目的関数と制約条件の設定の所ではproblem+=と書く必要があります。


肝心の目的関数の設定についてですが、今回は単に\(t\)なのでそのまま書いています。


制約条件の設定

for i in E:
  prob += lpSum(x[i,j] * w[i][j] for j in J) <= t

for j in J:
  prob += lpSum(x[i,j] for i in E) == 1 # 仕事側の頂点に関する制約条件を設定


次に制約条件を設定していきます。まず1つ目の制約である

\(\sum\limits_{j \in J}x_{ij} \leq t \;\; (\forall i \in E)\)

を設定します。\(\sum\)はlpSumを使えば表現できます。かっこの中を(x[i,j] for j in J)とすることで\(\sum\limits_{j \in J}x_{ij}\)を表現することができます。

これが\(t\)以下になれば良いので<= tとしています。


続いて2つ目の制約である

\(\sum\limits_{i \in E}x_{ij} = 1 \;\; (\forall j \in J)\)

を設定します。1つ目の制約と同じようにlpSumで\(\sum\)を表現し、かっこの中を(x[i,j] for i in E)とすることで\(\sum\limits_{i \in E}x_{ij}\)を表現することができます。

これが1になれば良いので== 1とします。

= 1にするとエラーが出るので気を付けてください。



問題を解く

result = prob.solve() # 問題を解く


prob.solve()で問題を解くことができます。結果をresultに格納しておきます。


結果の表示

## 結果の表示
print(LpStatus[result]) # 得られた結果の状態を表示(Optimalなら最適解が得られている)
for i in E:
  for j in J:
    if x[i,j].value() == 1:
      print(f"x{i}{j} =",x[i,j].value()) # 最適解(変数の値が1のものだけ)を表示
print("最適値 :", prob.objective.value()) # 最適値を表示


最後に結果を表示するコードについて説明したいと思います。まずprint(LpStatus[result])で得られた結果の状態を表示します。もしOptimalと返されたらちゃんと最適値が求まっています。


次に最適解、すなわち\(x_{ij}=1\)を満たす変数\(x_{ij}\)を表示するように設定します。解の値は.value()で得ることができます。

for i in E:とfor j in J:で回すことによって全ての\(x_{ij}\)に関して値を確認をすることができます。


最後に最適値を表示するように設定します。最適値はobject.value()で得ることができます。


結果を1つずつ解釈する


上のコードを実行したら以下の結果が出力されたので、この結果を1つずつ解釈していきましょう。


1行目は無視して大丈夫です。(pulpが既にインストールされていることを表しています。)

2行目は結果の状態を表しています。Optimalと表示されているのでちゃんと最適解が得られていることが分かりますね。


3行目~6行目では最適解(変数の値が1のものだけ)を表しています。これを見るとx00、x12、x21、x23が1であることが分かります。

これはつまり

従業員1が仕事1を担当し、
従業員2が仕事3を担当し、
従業員3が仕事2と仕事4を担当する


のが最適な割当だということを表しています。

pythonではリストのインデックスが0から始まるので1つずつ数字がずれます。



従業員3に一番負担がかかっていることが分かりますね。

7行目は最適値を表しています。これを見ると今回の問題の最適値は5であることが分かりますね。


おわりに


いかがでしたか。

今回の記事では割当問題について解説していきました。

今後もこのような組合せ最適化に関する記事を書いていきます!

最後までこの記事を読んでくれてありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA