完全2部グラフの最小重み完全マッチングを求める問題をpythonで解いてみた


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

今回の記事は2部グラフの最小重み完全マッチングついて解説していきます!

マッチングとはグラフ理論という数学の分野で登場する用語で、現実世界の様々な分野で応用できます。


マッチングには色々な種類がありますが、その中でも今回は完全2部グラフの最小重み完全マッチングを求める問題をpythonで解いてみたいと思います!


前回の記事で完全2部グラフの最小重み完全マッチングを求める問題を整数計画問題として定式化したので、それをpythonで表現する方法を解説したいと思います。

2部グラフの最小重み完全マッチングについてはこちらの記事で解説しています!



完全2部グラフの最小重み完全マッチングを求める問題を整数計画問題として定式化する方法は以下の記事で解説しています!



それではやっていきましょう!



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



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

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


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


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

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

引用元 : 経営工学 – Wikipedia

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



今回解く問題


今回は以下の完全2部グラフの最小重み完全マッチングを求めたいと思います。


この問題を整数計画問題として定式化すると以下のようになります。

\(\min \;\;\; \sum\limits_{i=0}^2 \sum\limits_{j=0}^2 w_{ij}x_{ij}\)
\(\;\text{s.t.}\;\;\;\sum\limits_{j=0}^2 x_{ij}=1 \;\;\;\; (i = 0,1,2)\)
\(\;\;\;\;\;\;\;\;\;\sum\limits_{i=0}^2 x_{ij}=1 \;\;\;\; (j = 0,1,2)\)
\(\;\;\;\;\;\;\;\;\; x_{ij} \in \{0,1\} \;\;(i=0,1,2 \; j=0,1,2)\)


\(w_{ij}\) : 辺\((i,j)\)の重み




コードの全貌と実行結果


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

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

## パラメータの設定
v1 = [0,1,2] # 左側の頂点を設定
v2 = [0,1,2] # 右側の頂点を設定
w = [[3,1,4],[3,2,5],[4,2,3]] # 各辺の重みを設定

## 整数計画問題の設定
prob = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定
x = LpVariable.dict("x",(v1,v2), cat = "Binary") # 0-1変数の設定
prob += lpSum(x[i,j] * w[i][j] for i in v1 for j in v2) # 目的関数の設定
for i in v1: 
  prob += lpSum(x[i,j] for j in v2) == 1 # 左側の頂点に関する制約条件を設定
for j in v2:
  prob += lpSum(x[i,j] for i in v1) == 1 # 右側の頂点に関する制約条件を設定
result = prob.solve() # 問題を解く

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

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


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

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


事前準備

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

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


パラメータの設定

## パラメータの設定
v1 = [0,1,2] # 左側の頂点を設定
v2 = [0,1,2] # 右側の頂点を設定
w = [[3,1,4],[3,2,5],[4,2,3]] # 各辺の重みを設定

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

v1が左側の頂点v2が右側の頂点を表しています。どちらも同じなのでぶっちゃけまとめて実行できるんですが、今回は分かりやすさ優先でコードを書いています。

wは各辺の重さを2次元リスト形式で表しています。例えばw[0][1]はwの1行2列目の数字1を表しますが、これは辺\((0,1)\)の重みが1であることを表しています。


整数計画問題の設定

## 整数計画問題の設定
prob = pulp.LpProblem(sense = LpMinimize) # 問題をMinimizeに設定
x = LpVariable.dict("x",(v1,v2), cat = "Binary") # 0-1変数の設定
prob += lpSum(x[i,j] * w[i][j] for i in v1 for j in v2) # 目的関数の設定
for i in v1: 
  prob += lpSum(x[i,j] for j in v2) == 1 # 左側の頂点に関する制約条件を設定
for j in v2:
  prob += lpSum(x[i,j] for i in v1) == 1 # 右側の頂点に関する制約条件を設定
result = prob.solve() # 問題を解く


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


問題をMinimizeに設定

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

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

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



0-1変数の設定

x = LpVariable.dict("x",(v1,v2), cat = "Binary") # 0-1変数の設定


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


上の例で言うと名前はx、添字の集合は左側の頂点集合v1と右側の頂点集合v2、変数はBinary(0-1変数)と設定しています。

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



目的関数の設定

prob += lpSum(x[i,j] * w[i][j] for i in v1 for j in v2) # 目的関数の設定

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

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

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


肝心の目的関数の設定についてですが、\(\sum\limits w_{ij}x_{ij}\)を表すにはlpSumが便利です。イメージ的にはlpSumが\(\sum\)の役割をしてくれると思ってください。


for i in v1 for j in v2と書くことで\(\sum\limits^2_{i=0}\sum\limits^2_{j=0}\)を表現することができます。


制約条件の設定

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


次に制約条件を設定していきます。まず左側の頂点に関する制約

\(\sum\limits^2_{j=0}x_{ij}=1 \;\; (i = 0,1,2)\)

を設定します。先ほどと同じように\(\sum\)はlpSumで表現します。かっこの中を(x[i,j] for j in v2)とすることで
\(\sum\limits^2_{j=0}x_{ij}\)
を表現することができます。(v2=[0,1,2])
これでが1になれば良いので==1としています。

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


右側の頂点に対しても同じように制約条件を設定します。


問題を解く

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


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


結果の表示

## 結果の表示
print(LpStatus[result]) # 得られた結果の状態を表示(Optimalなら最適値が求まっている)
for i in v1:
  for j in v2:
    print(f"x{i}{j} =",x[i,j].value()) # 最適解(各変数の値)を表示
print("最適値 :", prob.objective.value()) # 最適値を表示


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


次に最適解、すなわち各変数\(x_{ij}\)の値を表示するように設定します。解の値は.value()で得ることができます。\(i=0,1,2 \;j=0,1,2\)に対して\(x_{ij}\)の値を表示したいので、for i in v1: for j in v2:でiとjを回しています。


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


結果を1つずつ解釈する


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


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

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


3行目~11行目では最適解(各変数の値)を表しています。これを見るとx01、x10、x22の3つだけが1でそれ以外は0になっていますね。

これはつまり辺\((0,1)\)、辺\((1,0)\)、辺\((2,2)\)を選ぶのが最小重み完全マッチングだと言うことを表しています。


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


おわりに


いかがでしたか。

今回の記事では2部グラフの最小重み完全マッチングについて解説していきました。

今後もこのようなグラフ理論に関する記事を書いていきます!

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

コメントを残す

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

CAPTCHA