Pythonプログラミング(ステップ9・クラスとオブジェクト・グラフのマッチング)
このページでは、オブジェクトを使った例として、テキストの「4.4 学生を配属する」の実装について考える。
1.学生配属問題
参考書の「4.4 学生を配属する」(p.134)では、 人数制限のある複数の授業に、最大限に本人の希望を汲み入れながら、学生を振り分ける方法について述べられている。 ここでは、その内容を繰り返すことはしないが、解くべき問題を要約すると、
- $N$人の新入生の集合がある:$X=\{ x_1, x_2, \cdots, x_N\}$
- $M$個の授業(クラス)があって($C = \{ c_1, c_2, \cdots, c_M\}$)、 各クラスには定員が設けられている。ここで、クラス$c$の定員を $m(c)$ とする。
- 学生毎に最大$L$まで、配属の希望を出すことができる。
- これらの条件のもとで、希望先に配属される学生の数が最大となるように、学生の配属先をひとつずつ決める。
となる。
もちろん、学生の希望が100%叶うとは限らない。極端な場合、全員がひとつのクラスを希望し、 クラス定員が学生数より少なければ、多くの者は、希望しないクラスに配属せざるを得ない。
2.データの表現
まず、新入生の集合をPythonのリストで表すことにしよう。汎用性を持たせ、氏名以外の属性(配属先希望など)も扱えるよう、Studentというクラスを定義してみる:
L=2 class Student: def __init__(self,name,choices): self.name=name self.choices=choices # 配属先希望リスト if len(choices)>L: print("希望数が多すぎます")
次に、授業のほうは(Classというクラスを使いたいところではあるが、名前が衝突するので)Lessonというクラスを使って
class Lesson: def __init__(self,title,capacity): self.title=title self.capacity=capacity # 定員
としてみる。すると、参考書 図4.8の状況は
c1 = Lesson("数学",1) c2 = Lesson("天文学",2) c3 = Lesson("心理学",2) c4 = Lesson("医学",1) C = [c1,c2,c3,c4] x1 = Student("城島",[c1,c2]) x2 = Student("山口",[c2,c3]) x3 = Student("国分",[c1,c4]) x4 = Student("松岡",[c2,c4]) x5 = Student("長瀬",[c3,c4]) X = [x1,x2,x3,x4,x5]
のようなコードで表現できる。
3.配属数が1のダミーの授業に分解する
続いて、定員が $m$ のクラスを、定員1で $m$ 個の(ダミーの)クラスに「分解」する。
以下のコードでは、ダミーのクラスがリスト CP
に生成される:
リスト.extend(別のリスト)
で、「リスト」に「別のリスト」が連結される
import copy CP=[ ] for c in C: newlist=[ ] for i in range(c.capacity): cp = copy.copy(c) cp.capacity=1 newlist.append(cp) CP.extend(newlist) for x in X: for z in x.choices: if z is c: x.choices.remove(z) x.choices.extend(newlist) break
ここで、オブジェクトをコピーする際の重要な注意点がある。変数c
にオブジェクトがセットされているとき、
代入 cp = c
を行っても、そのオブジェクトの複製が生成されるわけではない。
変数にセットされているのは、オブジェクトの参照先(オブジェクトがどこにあるか)なので、「住所」だけコピーしても、
オブジェクト(家)が複製されるわけではない。
ここでは「浅いコピー」を行っている
上のコードでは、オブジェクトそのものの複製を生成したいので、代わりにcp = copy.copy(c)
と記述している。
copyモジュール を使うことで、オブジェクトの実体を複製し、その参照先を cp
にセットできる。
そうして、新たなリスト CP
の各オブジェクトは、定員1名ずつの新しいLessonクラスのオブジェクトが格納されてる。
4.データから「変形配属希望グラフ」を生成する
「配属先希望グラフ」を表現するために、二部グラフを表現するクラスを定義する。
ここで、u
は「左側」、v
は「右側」の頂点に対応づけることを想定している。
class BGraph: """a bipartile graph representation by Python """ class Vertex: def __init__(self,name): self.name=name self.link=[ ] # list of [Vertex] self.label=None def __init__(self): self.us=[ ] self.vs=[ ] def create_vertex_u(self,name): u = BGraph.Vertex(name) self.us.append(u) def create_vertex_v(self,name): v = BGraph.Vertex(name) self.vs.append(v) def append_link(self,v,a): if a in v.link: print("already connected") else: v.link.append(a) def connect_vertex(self,u,v): self.append_link(u,v) self.append_link(v,u)
このようにクラスBGraphを定義すると、学生の希望リスト$X$と授業のリスト$CP$(定員1名)を使って、 以下のコードで二部グラフが生成できる。
dict={} for c in CP: v = g.create_vertex_v(c.title) dict[c]=v for x in X: u = g.create_vertex_u(x.name) for c in x.choices: v = dict[c] g.connect_vertex(u,v)
ここで、希望リスト中のLessonクラスのオブジェクトと、BGraph中のVertexクラスのオブジェクトとの 対応関係を記憶しておくため、「辞書」dictを用いている。
5.グラフの最大マッチングを求める
ここでは、教科書で説明されている$M-$交互道のアイデアに沿って、最大マッチングを求めるコードをさらに追加した例を示す。 これまでに登場したコードも中に含まれる。
# coding: utf-8 import copy L=2 class Student: def __init__(self,name,choices): self.name=name self.choices=choices # 配属先希望リスト if len(choices)>L: print("希望数が多すぎます") class Lesson: def __init__(self,title,capacity): self.title=title self.capacity=capacity # 定員 class BGraph: """a bipartile graph representation by Python """ class Vertex: def __init__(self,name): self.name=name self.link=[ ] # list of [Vertex] self.label=None def __init__(self): self.us=[ ] # list of U self.vs=[ ] # list of V def create_vertex_u(self,name): u = BGraph.Vertex(name) self.us.append(u) return u def create_vertex_v(self,name): v = BGraph.Vertex(name) self.vs.append(v) return v def append_link(self,v,a): if a in v.link: print("already connected") else: v.link.append(a) def connect_vertex(self,u,v): self.append_link(u,v) self.append_link(v,u) def is_in_u(self,x): if x in self.us: return True else: return False def is_in_v(self,x): if x in self.vs: return True else: return False def remove_all_labels(self): for x in self.us: x.label=None for x in self.vs: x.label=None # a Python version of the algorithm described in # http://www.csl.mtu.edu/cs4321/www/Lectures/ # Lecture%2022%20-%20Maximum%20Matching%20in%20Bipartite%20Graph.htm def perform_matching(self): matching=[ ] queue=[x for x in self.vs] while len(queue)>0: w = queue.pop() if self.is_in_v(w): for u in w.link: u_matched=set() for x,y in matching: u_matched.add(y) if u not in u_matched: matching.append([w,u]) v=w while v.label is not None: u = v.label matching.remove([v,u]) v = u.label matching.append([v,u]) self.remove_all_labels() v_matched=set() for x,y in matching: v_matched.add(x) v_free = set(self.vs) - v_matched queue=list(v_free) break else: if ([w,u] not in matching) and u.label is None: u.label = w queue.append(u) else: for v,u in matching: if u is w: v.label = w queue.append(v) break return matching # end of BGraph # メイン部 c1 = Lesson("数学",1) c2 = Lesson("天文学",2) c3 = Lesson("心理学",2) c4 = Lesson("医学",1) C = [c1,c2,c3,c4] x1 = Student("城島",[c1,c2]) x2 = Student("山口",[c2,c3]) x3 = Student("国分",[c1,c4]) x4 = Student("松岡",[c2,c4]) x5 = Student("長瀬",[c3,c4]) X = [x1,x2,x3,x4,x5] # 各クラスを定員1名に分解し、CPにセットする CP=[ ] for c in C: newlist=[ ] for i in range(c.capacity): cp = copy.copy(c) cp.capacity=1 newlist.append(cp) CP.extend(newlist) for x in X: for z in x.choices: if z is c: x.choices.remove(z) x.choices.extend(newlist) break # XとCPから二部グラフを生成する g = BGraph() dict={} for c in CP: v = g.create_vertex_v(c.title) dict[c]=v for x in X: u = g.create_vertex_u(x.name) for c in x.choices: v = dict[c] g.connect_vertex(u,v) # 最大マッチングを求める matching=g.perform_matching() # 結果を出力する for v,u in matching: print(u.name,"--",v.name)
練習:コードの動作確認
仮の学生とクラスを想定し、上記のコードをそれに合わせて変更した上で、動作確認を行いなさい。