左右で頂点数が異なる完全2部グラフの最小重みマッチング問題をpythonで解いてみた


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

今回の記事は左右の頂点数が異なる完全2部グラフに対して頂点数が少ない方の頂点集合をすべてカバーする最小重みマッチングついて解説していきます!

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


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


左右で頂点数が違うのでこのグラフには完全マッチングが存在しません。ということで今回は

「頂点数が少ない方の頂点集合を全てカバーするようなマッチングのうち重みの合計が最小となるもの」

をpythonで求める方法を考えていきたいと思います。




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



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



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

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


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


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

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

引用元 : 経営工学 – Wikipedia

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



今回解く問題


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


この問題を整数計画問題として定式化すると以下のようになります。今回はこの整数計画問題をpythonで実行してみたいと思います。

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


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




コードの全貌と実行結果


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

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

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

## 整数計画問題の設定
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をインストール
from pulp import * # pulpをインポート

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


パラメータの設定

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

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

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


整数計画問題の設定

## 整数計画問題の設定
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変数)と設定しています。つまりこのコードだけで

\(x_{00},x_{01},x_{02},x_{10},x_{11},x_{12}\)

を作成することができます。

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^1_{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)\)

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

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


右側の頂点に関しては

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

を設定するので==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 \;j=0,1,2\)に対して\(x_{ij}\)の値を表示したいので、for i in v1: for j in v2:でiとjを回しています。


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


結果を1つずつ解釈する


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


1行目~5行目は無視して大丈夫です。(pulpがちゃんとインストールされたことを表しています。)

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


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

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


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


おわりに


いかがでしたか。

今回の記事では左右で頂点数が異なる2部グラフの最小重みマッチングについて解説していきました。

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

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

コメントを残す

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

CAPTCHA