Pythonプログラミング(ステップ9・クラスとオブジェクト・グラフのマッチング)

このページでは、オブジェクトを使った例として、テキストの「4.4 学生を配属する」の実装について考える。

1.学生配属問題

参考書の「4.4 学生を配属する」(p.134)では、 人数制限のある複数の授業に、最大限に本人の希望を汲み入れながら、学生を振り分ける方法について述べられている。 ここでは、その内容を繰り返すことはしないが、解くべき問題を要約すると、

となる。

もちろん、学生の希望が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)

icon-pc 練習:コードの動作確認

仮の学生とクラスを想定し、上記のコードをそれに合わせて変更した上で、動作確認を行いなさい。