シフトを自動で組むプログラムをPythonで書いた

アルバイトのシフトを組むのって大変らしいですね。

調べてみると「ナーススケジューリング問題はNP困難」などと出てきます。

ただ、頭のなかで考えていた問題は労働者ごとの職能に差がないものだったので、ナーススケジューリング問題よりは簡単かもしれない。……どうせ人間が目視で確認するし、何パターンか出力すれば良くね? 表にする部分の作業をやるだけでも楽になるのでは?と思い、最適化とか考えずに可能なシフトをいくつか出力してみました。

問題設定

入力

  • 労働者の希望勤務枠が与えられる。労働者が希望していない労働枠での勤務は不可。
  • 労働枠に対して必要人数が与えられる。必要人数以上の勤務は不可。不足の場合はダミー労働者を配置する。
  • 労働枠の重なりが与えられる。重なりのある労働枠を同一労働者が勤務することは不可。

出力

  • シフト案100個
  • 各シフト案に対して
    • 勤務枠ごとの勤務労働者
    • (シフト案を評価する目安として)勤務労働者内における勤務回数の標準偏差
import copy
import random
import collections
import string


def is_legal(plan, required, possible, group):
    for cell, workers in plan.items():
        if len(workers) != required[cell]:
            return False
        for worker in workers:
            if cell not in possible[worker]:
                return False
    for g in group:
        worked = []
        for cell in g:
            for worker in plan[cell]:
                if worker in worked:
                    return False
                worked.append(worker)
    return True


def evaluate(plan):
    total_work = 0
    worked = {}
    for cell, workers in plan.items():
        total_work += len(workers)
        for worker in workers:
            if 'dummy' in worker:
                pass
            elif worker in worked:
                worked[worker] += 1
            else:
                worked[worker] = 1
    total_work /= len(worked)
    score = sum(v * v for k, v in worked.items()) / len(worked)
    if score - total_work**2 < 0:
        return total_work + (total_work**2 - score)**0.5
    return (score - total_work * total_work)**0.5


def solver(required, possible, group):
    cells = [k for k, v in required.items()]
    workers = [k for k, v in possible.items()]
    timetable = {}
    for cell in cells:
        timetable[cell] = []
        for worker in workers:
            if cell in possible[worker]:
                timetable[cell].append(worker)
    generation = 0
    plan = {cell: [] for cell in cells}
    plans = list()
    for i in range(100):
        if is_legal(plan, required, possible, group):
            for cell in cells:
                plan[cell].sort()
            score = evaluate(plan)
            plans.append(
                (score, str(list(sorted(plan.items(), key=lambda t: t[0])))))
        for cell in cells:
            while len(timetable[cell]) < required[cell]:
                suffix = random.sample(string.ascii_uppercase, 4)
                dummy = 'dummy' + ''.join(suffix)
                timetable[cell].append(dummy)
                if dummy not in possible:
                    possible[dummy] = []
                possible[dummy].append(cell)
            plan[cell] = random.sample(timetable[cell], required[cell])

    return list(plans)


def generate_sampledate():
    eachday = {'A': 2, 'B': 1, 'C': 2, 'D': 1, 'E': 3, 'F': 1}
    not_same_member = [('A', 'B'), ('C', 'D'), ('E', 'F')]
    required = {}
    group = []
    for day in range(1, 31):
        d = ('0' + str(day))[-2:]
        for k, v in eachday.items():
            required[d + k] = v
        for g in not_same_member:
            group.append((d + e for e in g))

    workers = ['Amy', 'Basil', 'Clara', 'Desmond', 'Ernest', 'Fanny', 'George',
               'Hector', 'Ida', 'James', 'Kate', 'Leo', 'Maud', 'Neville',
               'Olive', 'Prue', 'Quentin', 'Rhoda', 'Susan', 'Titus', 'Una',
               'Victor', 'Winnie', 'Xerxes', 'Yorick', 'Zillah']
    possible = {}
    for w in workers:
        possible[w] = []
        for k in required:
            if random.random() > 0.78:
                possible[w].append(k)
    return required, possible, group


if __name__ == '__main__':
    plans = solver(*generate_sampledate())
    plans.sort()
    print(*plans, sep='\n')

感想

paizaのプログラミングで彼女を作るやつを全クリアした

paiza.jp 彼女欲しいって言ってたら友達にオヌヌメされたので解いた。全体的に制約が超ゆるいので何でも通っちゃう。 一応水着も手に入れたけど、僕がほしいのは生身の彼女なんだ!

「めがね」ゲットチャレンジ!

本当にやるだけ。制約が小さいので全部見れる。

N=int(input())
Q=[list(map(int, input().split())) for i in range(N)]
M=int(input())
P=[list(map(int, input().split())) for i in range(M)]
for y in range(N-M+1):
    for x in range(N-M+1):
        is_miss = False
        for i in range(M):
            for j in range(M):
                if Q[y+i][x+j] != P[i][j]:
                    is_miss = True
                    break
            if is_miss:
                break
        if not is_miss:
            print(y,x)
            exit()

「サンタ服」ゲットチャレンジ!

個人的にサンタ服は好きです。何も考えずにXとYの最小幅とZをかける。

X, Y, Z, N = map(int, input().split())
cut = [[0, X], [0, Y]]
for i in range(N):
    d, a = map(int, input().split())
    cut[d].append(a)
min_v = [X, Y]
for i in range(2):
    cut[i].sort()
    for j in range(1, len(cut[i])):
        min_v[i] = min(min_v[i], cut[i][j] - cut[i][j - 1])
print(min_v[0] * min_v[1] * Z)

「水着」ゲットチャレンジ!

1<=N<=106が与えられるのでN! の最下位桁から続く0 をすべて除いた値の下位9桁を求める問題。
毎回109乗で余りを取っていけばいいかと思うのだが、5の2乗以上をかけた時にゼロが下に複数個ついて必要な桁の情報を持っていないようだったので59-106なので8桁余分に持って計算を進める。

N = int(input())
t = N
for i in range(2, N):
    t *= i
    while t % 10 == 0:
        t //= 10
    t %= 10**(9+8)
print(t % (10**9))
続きを読む

SymPyで辛い計算をちょっと辛い計算にする

この記事はPython Advent Calendar 2015 - Adventarの7日目の記事です。


SymPyというシンボル計算ライブラリを使ったことがありますか? 最近、卒論のお手伝いをしてもらっているライブラリです。 日本語の情報が少なくて困ったので自分用にという意味も込めて使い方を書きます。

インストール

Anacondaを使います。大人しくAnacondaを使います。

シンボル計算ライブラリ SymPy

シンボル計算ライブラリというのは「X」とか「Y」とか多項式の計算をしてくれるライブラリです。 例えば{Z = X \left(2 X + 4\right) + 5 Y - 7}なんて式が与えられたとして {Z^{2}}を計算する必要があるとしましょう。 {Z^{2} = \left(X \left(2 X + 4\right) + 5 Y - 7\right)^{2} = 4 X^{4} + 16 X^{3} + 20 X^{2} Y - 12 X^{2} + 40 X Y - 56 X + 25 Y^{2} - 70 Y + 49}となるのですが、こんな計算は高校の宿題までにしたいところで、SymPyの出番になるわけです。

使う記号を宣言して、計算式を入力するだけで計算結果を表示してくれるなんて、なんて素敵なんだ!

import sympy

sympy.init_printing()
# いい感じに出力するよう設定してくれるらしい

X = sympy.Symbol('X')
Y = sympy.Symbol('Y')
# 使用する記号を宣言して、変数に代入
# 左辺の変数Xと右辺の引数の文字Xは異なる文字列でも可
# 混乱を防ぐために同じ文字を使っている。

Z = 2 * (X + 2) * X + 5 * Y - 7

print(Z)
# => X*(2*X + 4) + 5*Y - 7

ZZ = Z * Z
print(ZZ)
# => (X*(2*X + 4) + 5*Y - 7)**2
print(sympy.simplify(ZZ))
# => (X*(2*X + 4) + 5*Y - 7)**2
print(sympy.expand(ZZ))
# => 4 X^{4} + 16 X^{3} + 20 X^{2} Y - 12 X^{2} + 40 X Y - 56 X + 25 Y^{2} - 70 Y + 49

代入 subs

多項式を計算したら、数値を代入したくなるのが人情というもの。 substituteのsubsだと思われるsubs関数を使って値を知ることが出来ます。 引数はタプルのリストで与え、各タプルの第一要素は何「に」代入するか、第二要素は何「を」代入するかを与えます。

print(Z.subs([(X, 1), (Y, 2)]))
# => 9
print(ZZ.subs([(X, 1), (Y, 2)]))
# => 81
print(ZZ.subs([(Z, X)]))
# => X**2

latex

さらに便利なのがsympy.latexという関数。この子を使えば添字も分数もバシッと出力してくれます。

print(sympy.latex(Z))
# => X \left(2 X + 4\right) + 5 Y - 7

print(sympy.latex(sympy.expand(ZZ)))
# => 4 X^{4} + 16 X^{3} + 20 X^{2} Y - 12 X^{2} + 40 X Y - 56 X + 25 Y^{2} - 70 Y + 49

print(sympy.latex(ZZ/(Z+1)))
# => \frac{\left(X \left(2 X + 4\right) + 5 Y - 7\right)^{2}}{X \left(2 X + 4\right) + 5 Y - 6}

手打ちしたくない数式も出力してくれます。 {\frac{Z^{2}}{Z+1} = \frac{\left(X \left(2 X + 4\right) + 5 Y - 7\right)^{2}}{X \left(2 X + 4\right) + 5 Y - 6}}

latexのバグ探しに時間が溶かされなくていいですね。 以上SymPyの話でした。

スペース区切りの文字列をリストに格納する方法の比較

競技プログラミングの入力では、一行にいくつかの数字がスペース区切りで並んでいるケースをよくみる。

例えば、No.16 累乗の加算 - yukicodera_1,a_2,...,a_N

Python3でこれらを一つのリストに格納したい時、一番早い方法はなんだろうか。

  1. map関数を使ってからlistにする
  2. リスト内包表記でmap関数
  3. ジェネレータ式からリストにする
  4. リスト内包表記でfor

それぞれに必要な時間を計測した。

import time
import random

A = [str(random.randint(0, 10000)) for i in range(5000)]
S = ' '.join(A)
times = 10000

print('method|elapsed[s]|type(A[0])')
print('---|---|---')
time_start_map = time.time()
for i in range(times):
    A = list(map(int, S.split()))
time_elapsed_map = time.time() - time_start_map
print('list(map)|', time_elapsed_map,'|',type(A[0]))

time_start_map = time.time()
for i in range(times):
    A = [map(int, S.split())]
time_elapsed_map = time.time() - time_start_map
print('[map]|', time_elapsed_map,'|',type(A[0]))


time_start_for = time.time()
for i in range(times):
    A = list(int(x) for x in S.split())
time_elapsed_for = time.time() - time_start_for
print('list(generator)|', time_elapsed_for,'|',type(A[0]))

time_start_for = time.time()
for i in range(times):
    A = [int(x) for x in S.split()]
time_elapsed_for = time.time() - time_start_for
print('[]|', time_elapsed_for,'|',type(A[0]))

結果

method elapsed[s] type(A[0])
list(map) 11.86867880821228 class 'int'
[map] 2.891165256500244 class 'map'
list(generater) 18.646066665649414 class 'int'
[] 17.813018798828125 class 'int'

結論

リスト内包表記でmap関数を使うのが爆速だけどmap形式だと意味が無いよ…… mapからlistに渡すのが早いと思っていたら4倍ほど遅かったのでびっくりしています。 びっくりはするもののこれがlist(map)が最速ですかね。

川渡りパズルを解く

川渡りパズルの解答を探すプログラムを書いてみました。
※この記事ではパズルの解答を1つしか求めていませんが、複数の解答が考えられる問題(3.危険な家族)も存在します。

川渡りパズルとは

  1. 狼とヤギとキャベツの問題 http://r27.jp/quiz/across-cabbage/

    1匹のオオカミと1匹のヤギを連れ、キャベツを携えた旅人が、舟で川を渡ろうとしている。 舟には、旅人の他に1つのものしか乗せることができない。 ヤギは旅人が近くにいないとキャベツを食べてしまう。 オオカミは旅人が近くにいないとヤギを食べてしまうのである。 全てのものを無事に向こう岸に渡すには、どうすればよいだろうか?

  2. 宣教師と先住民の問題 http://r27.jp/quiz/across-or-die/

    3人の宣教師と3人の人喰い人種が、舟で川を渡ろうとしている。 舟は1艘しかないが、1度に2人まで乗ることができ、誰でも漕ぐことが可能である。 ただし、それぞれの岸において、人喰い人種の数が宣教師の数を上回ると、宣教師は食べられてしまう。 さて、全員が無事に川を渡り切るには、どうすればよいだろうか?

  3. 危険な家族の問題 http://r27.jp/quiz/across-family/

    ある家族が、舟で川を渡ろうとしている。 家族は父、母、長男、次男、長女、次女、メイド、犬の8人であり、 舟は1艘しかないが、1度に2人まで乗ることができる。 また、舟を漕げるのは父、母、メイドの3人だけである。 ただしこの家族、実はとても危険な家族なのである。 まず父は、母が近くにいないと娘たちをいじめてしまう。 また母は、父が近くにいないと息子たちをいじめてしまう。 そして犬は、メイドが近くにいないと家族全員に襲い掛かってしまうのである。 さて、全員が無事に川を渡り切るには、どうすればよいだろうか?

解き方、考え方

  1. ルールで禁止されていない選択肢を幅優先探索
  2. ゴール(全員が川を渡り切る)すると解答
  3. 同じ局面に戻ってくるのは手数の無駄なので、各局面の左岸の状態とその直前の左岸の状態を記録しておき、過去に現れた局面を拒否する
  4. queueを使っているので一番初めに現れた解答が最短であることは確か

実装

banはそれぞれのタプルの第一項のメンバー全てが第二項のメンバー全てと異なる岸に居てはいけないという制約。

import queue
import itertools


def leftover(all_items, remove_items):
    leftover_items = []
    for i in all_items:
        if i not in remove_items:
            leftover_items.append(i)
    return leftover_items

LEFT = 0
RIGHT = 1

characters = sorted(['man', 'wolf', 'goat', 'cabbage', 'boat'])
left_side = sorted(['man', 'wolf', 'goat', 'cabbage', 'boat'])
# everybody in the left side of rever
right_side = sorted([])
# nobady in the right side of rever
rowers = ['man']
# the man only can row the boat
boat_size = 2
ban = [(['man'], ['wolf', 'goat']), (['man'], ['goat', 'cabbage'])]
# wolf eat goat if no man with them, goat eat cabbage if no man with them.
goal = [[], sorted(['man', 'wolf',  'goat', 'cabbage', 'boat'])]
Q = queue.Queue()
past = dict()
Q.put((left_side[:], right_side[:], ['start']))
while not Q.empty():
    q = Q.get()
    if tuple(q[LEFT]) in past:
        continue
    else:
        past.update({tuple(q[LEFT]): q[2]})
    if q[LEFT] == goal[LEFT] and q[RIGHT] == goal[RIGHT]:
        path = []
        t = q[LEFT]
        while t != ['start']:
            path.append(t)
            t = past[tuple(t)]
        path.reverse()
        for p in path:
            print(p, leftover(characters, p))
        exit()

    boat = RIGHT
    if 'boat' in q[LEFT]:
        boat = LEFT
    for rower in rowers:
        if rower in q[boat]:
            for passengers in itertools.combinations([''] * (boat_size - 1) + q[boat], boat_size - 1):
                if rower in passengers or 'boat' in passengers:
                    continue
                left = q[LEFT][:]
                right = q[RIGHT][:]
                if boat == LEFT:
                    left.remove('boat')
                    right.append('boat')
                    if rower != '':
                        left.remove(rower)
                        right.append(rower)
                    for passenger in passengers:
                        if passenger != '':
                            left.remove(passenger)
                            right.append(passenger)
                else:
                    right.remove('boat')
                    left.append('boat')
                    if rower != '':
                        right.remove(rower)
                        left.append(rower)
                    for passenger in passengers:
                        if passenger != '':
                            right.remove(passenger)
                            left.append(passenger)
                is_banned = False
                for b in ban:
                    if (all(c in left for c in b[0]) and all(c in right for c in b[1])) or
                       (all(c in left for c in b[1]) and all(c in right for c in b[0])):
                        is_banned = True
                if not is_banned:
                    Q.put((sorted(left[:]),
                           sorted(right[:]), sorted(q[LEFT][:])))
print('no answer')

dictはlistをキーにできない、タプルは必ず複数個の要素が必要という点にハマりました。 他の問題を解いたコードはgithubにあげてあります。(所要時間3時間) github.com

ABC#027の感想

すっかり存在を忘れていて残り時間30分からの挑戦。 Cの解法は解説読んでも釈然としません。
http://abc027.contest.atcoder.jp/

A: 長方形 - AtCoder Beginner Contest 027 | AtCoder

a,b,c = [int(x) for x in input().split()]
if b==c:
    print(a)
elif c==a:
    print(b)
else:
    print(c)

B: 島と橋 - AtCoder Beginner Contest 027 | AtCoder

左の島から見ていって、橋でつないだ島人口の合計が目指すべき島人口×島数になるようにつなぐ。

N = int(input())
A = [int(x) for x in input().split()]
sum_A = sum(A)
if sum_A%N != 0:
    print(-1)
else:
    target = sum_A // N
    cnt = 0
    p = 0
    n = 0
    for i in range(N):
        p += A[i]
        n += 1
        if p == n*target:
            cnt += n-1
            p = 0
            n = 0
    print(cnt)

C: 倍々ゲーム - AtCoder Beginner Contest 027 | AtCoder

N = int(input())
depth = 0
n = N
while n>0:
    depth += 1
    n //= 2
depth %= 2
x = 1
turn = 0
player = ['Takahashi', 'Aoki']
while x<=N:
    if turn+depth == 1:
        x = x*2+1
    else:
        x = x*2
    turn = (turn+1)%2
print(player[turn])

D: ロボット - AtCoder Beginner Contest 027 | AtCoder

それぞれのMに対して右側にある'+'と'-'の個数の差を記録し、その差が少ない半分は'<'、多い半分は'>'とする。

S = input()
A = []
m_cnt = 0
p_cnt = 0
for i in range(len(S)):
    s = S[len(S)-1-i]
    if s == 'M':
        A.append(p_cnt)
        m_cnt += 1
    elif s == '+':
        p_cnt += 1
    elif s == '-':
        p_cnt -= 1
A.sort()
print(sum(A[m_cnt//2:]) - sum(A[:m_cnt//2]))