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)
 練習:コードの動作確認
仮の学生とクラスを想定し、上記のコードをそれに合わせて変更した上で、動作確認を行いなさい。