原因考察

通常、そんな事は起こりえないと思いますが、確かに解を見ると、そのような例が見受けられます。
そこで、制約を見ると、次のように階の被りがないように制約されています。



その結果、例えば、2階のスタッフのペアは、3階か4階のスタッフしかありえない訳です。
そうすると、3階か4階の夜勤可能なスタッフは、8人とかに限定されてしまいます。夜勤を6回やるとすると、確率的にありえない話ではなくなる、ということが分かりました。



そもそも3回夜勤一緒になると何が問題なのか?

という疑問が沸いてきます。夜勤経験のある方なら分かりますが、気の合うペアならば、何も問題ないです。しかし、気の合わないスタッフ、組みたくない相手である場合、3回の一緒、実質6日間も夜勤ペアならば、最悪の憂鬱となります。場合によっては、仕事を辞めたくなる要因になりえます。

従い、この問題は無視することが出来ない切実な問題、と考えられます。一方、一律で、1回に限定するとした場合、解空間を狭める、事が考えられます。この制約を入れたことによって、他の制約の成否に影響を与えたくありません。そこで、スタッフ毎に一緒になる回数を設定することにします。

一方「私は、誰とでもOKよ」というスタッフもいるでしょうし、上司からみて、皆から組むのを嫌がられているスタッフもいるでしょう。そこで、組むスタッフの一緒回数の最大をスタッフプロパティで設定するようにします。

選択は、1または2です。「私は、誰とでもOKよ」の人は、ブランクにします。どちらもブランクであれば、制約されません。どちらかが、ブランクでない場合は、ブランクでないスタッフの一緒回数で制約されます。どちらもブランクでないならば、小さいほうの数に制約されます。



実装は、次のPythonソースです。

import sc3

def assign_フロア長会議(会議開催数):
    listday=[]
    for day in 今月:
        list=[]
        for person in 全スタッフ:
            if person not in フロア長:
                continue
            v1=sc3.GetShiftVar(person,day,'日勤')
            v2=sc3.GetShiftVar(person,day,'入り')
            list.append(v1|v2)#日勤または、入り
        listday.append(sc3.And(list))#がフロア長全員、日勤または入りの日が、
    s='フロア長会議'
    sc3.AddSoft(sc3.SeqError(会議開催数,0,3,listday),s,5)#今月、2回以上あること

def 夜勤一緒回数制約sub(p1,p2):
    
    min_num=最大夜勤一緒回数属性[p1]#p1のDICは、存在する p2のDICは、存在しないかもしれない
    for person in 最大夜勤一緒回数属性:
        if p2 not in 最大夜勤一緒回数属性:#MAR072024
            continue
        if min_num> 最大夜勤一緒回数属性[p2]:
            min_num=最大夜勤一緒回数属性[p2]
    list=[]
    for day in 今月:
        v1=sc3.GetShiftVar(p1,day,'入り')
        v2=sc3.GetShiftVar(p2,day,'入り')
        list.append(v1&v2)#両方入りに入る
    s='夜勤一緒回数 '+staffdef[p1]+staffdef[p2]
    sc3.AddSoft(sc3.SeqError(0,min_num,2,list),s,5)#今月最大一緒回数


def 夜勤一緒回数制約():
    for person in 最大夜勤一緒回数属性:

        if person in a_2F:#2階は、3階と4階を見る
            if person not in 入り:
                continue
            for ペア3F in b_3F:
                if ペア3F not in 入り:
                    continue
                夜勤一緒回数制約sub(person,ペア3F)
            for ペア4F in c_4F:
                if ペア4F not in 入り:
                    continue
                夜勤一緒回数制約sub(person,ペア4F)

        if person in b_3F:#3階は、4階だけ見れば十分
            if person not in 入り:
                continue
            for ペア4F in c_4F:
                if ペア4F not in 入り:
                    continue
                夜勤一緒回数制約sub(person,ペア4F)

        #print(staffdef[person])
def check_フロア長会議(希望会議開催数):
    success_cnt=0
    for day in 今月:
        list=[]
        success=True
        for person in 全スタッフ:
            if person not in フロア長:
                continue
            v=shift_solution[person][day]
            if v !='日勤' and v!='入り':#日勤か入りでなければNG
                success=False
        if success:
            print('フロア長会議',daydef[day],'に可能です')
            success_cnt+=1
    if success_cnt<希望会議開催数:
        print('フロア長会議は、指定回数開催できませんでした。')

def check_夜勤一緒回数():
    for p1 in 最大夜勤一緒回数属性:
        for p2 in 最大夜勤一緒回数属性:
            if p1==p2:
                continue
            count=0
            max=最大夜勤一緒回数属性[p1]
            if max> 最大夜勤一緒回数属性[p2]:
                max=最大夜勤一緒回数属性[p2]
            for day in 今月:
                v1=shift_solution[p1][day]
                v2=shift_solution[p2][day]
                if v1=='入り' and v2=='入り':
                    count+=1
            if count>max:
                print("************Error",staffdef[p1],"と",staffdef[p2],"で夜勤一緒回数が、",count,"になっています。")
            else:
                print(staffdef[p1],"と",staffdef[p2],"で夜勤一緒回数が、",count,"になっています。")

def post_main():#求解後、呼び出しルーチン 
    if ダイナミック割り当てフロア長会議制約をEnable:
        check_フロア長会議(2)
    if 夜勤一緒回数制約をEnable:
        check_夜勤一緒回数()

if ダイナミック割り当てフロア長会議制約をEnable:
    assign_フロア長会議(2) #main 開催数最小値で呼び出しする
if 夜勤一緒回数制約をEnable:
    夜勤一緒回数制約()

検証

ポストプロセスで出力しています。

ポスト処理を実行します。ソルバを呼び出し中です。
スタッフ1 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ7 で夜勤一緒回数が、 2 になっています。
スタッフ1 と スタッフ8 で夜勤一緒回数が、 1 になっています。
スタッフ1 と スタッフ9 で夜勤一緒回数が、 1 になっています。
スタッフ1 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ1 と スタッフ14 で夜勤一緒回数が、 1 になっています。
スタッフ1 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ7 で夜勤一緒回数が、 1 になっています。
スタッフ2 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ12 で夜勤一緒回数が、 2 になっています。
スタッフ2 と スタッフ13 で夜勤一緒回数が、 1 になっています。
スタッフ2 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ2 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ7 で夜勤一緒回数が、 1 になっています。
スタッフ3 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ9 で夜勤一緒回数が、 1 になっています。
スタッフ3 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ12 で夜勤一緒回数が、 1 になっています。
スタッフ3 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ3 と スタッフ14 で夜勤一緒回数が、 2 になっています。
スタッフ3 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ8 で夜勤一緒回数が、 1 になっています。
スタッフ4 と スタッフ9 で夜勤一緒回数が、 1 になっています。
スタッフ4 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ4 と スタッフ14 で夜勤一緒回数が、 1 になっています。
スタッフ4 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ5 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ1 で夜勤一緒回数が、 2 になっています。
スタッフ7 と スタッフ2 で夜勤一緒回数が、 1 になっています。
スタッフ7 と スタッフ3 で夜勤一緒回数が、 1 になっています。
スタッフ7 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ13 で夜勤一緒回数が、 1 になっています。
スタッフ7 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ7 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ1 で夜勤一緒回数が、 1 になっています。
スタッフ8 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ4 で夜勤一緒回数が、 1 になっています。
スタッフ8 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ8 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ1 で夜勤一緒回数が、 1 になっています。
スタッフ9 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ3 で夜勤一緒回数が、 1 になっています。
スタッフ9 と スタッフ4 で夜勤一緒回数が、 1 になっています。
スタッフ9 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ12 で夜勤一緒回数が、 1 になっています。
スタッフ9 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ9 と スタッフ14 で夜勤一緒回数が、 1 になっています。
スタッフ9 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ10 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ2 で夜勤一緒回数が、 2 になっています。
スタッフ12 と スタッフ3 で夜勤一緒回数が、 1 になっています。
スタッフ12 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ9 で夜勤一緒回数が、 1 になっています。
スタッフ12 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ12 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ2 で夜勤一緒回数が、 1 になっています。
スタッフ13 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ7 で夜勤一緒回数が、 1 になっています。
スタッフ13 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ14 で夜勤一緒回数が、 0 になっています。
スタッフ13 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ1 で夜勤一緒回数が、 1 になっています。
スタッフ14 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ3 で夜勤一緒回数が、 2 になっています。
スタッフ14 と スタッフ4 で夜勤一緒回数が、 1 になっています。
スタッフ14 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ9 で夜勤一緒回数が、 1 になっています。
スタッフ14 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ14 と スタッフ15 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ1 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ2 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ3 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ4 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ5 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ7 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ8 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ9 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ10 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ12 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ13 で夜勤一緒回数が、 0 になっています。
スタッフ15 と スタッフ14 で夜勤一緒回数が、 0 になっています。
ポスト処理を終了しました。 1 (秒)

プロジェクト

プロジェクトは、以下です。

ダウンロード して、実装の参考にしてください。