2026年3月23日月曜日

Reduced Cost Fixing

 数理最適化の文脈における Reduced Cost Fixing(縮小コストによる固定)は、混合整数計画問題(MIP)などの難しい問題を解く際に、「最適解においてその変数が取るべき値を事前に確定(固定)させる」ことで探索範囲を絞り込む手法です。

UBが既知のときに、

bitを反転させるコストがUBを上回るならば、bitを反転させることは、必ずUBを上回ることを意味するので、bitを反転させることはできない、→bit固定化できる、

LB+反転ReducedCost>UB →ビット固定

ということになります。GAP=UB-LBが小さくないと、UBを上回ることはないので、この手法が有効なのは、GAPが小さいときに限られます。逆にLB=UBとなれば、極小さいReducedCostでもビットを固定化できるので、大量のFixedBitsを得ることが出来ます。そうなれば、MIP/SAT ソルバ共簡単に解けるようになるので、出来る限りLBを上げること目指している訳です。

以下の説明では、Simplexを念頭に置いたBasic変数と非Basic変数に関する説明がされていますが、Simplexに限定されるものではなく、PDLP(FirstOrder)法、内点法等でも、成立します。


1. Reduced Cost(縮小コスト)とは
線形計画法(LP)において、ある変数が現在の最適解で 
 であるとき、その変数を 
 単位増やすために必要な「コストの増加分(または利益の減少分)」を指します。
  • 基本変数の場合: 縮小コストは常に 
     です。
  • 非基本変数の場合: その変数が最適解に含まれていない理由(=どれだけ損か)を数値化したものです。
2. Fixing(固定)の仕組み
MIPを解く際、まず整数制約を無視した「LP緩和問題」を解きます。このとき得られる縮小コスト 
 と、暫定解(現在見つかっている最も良い整数解)の目的関数値 
、およびLP緩和の目的関数値 
 を用いて以下の判断を行います。
  • 論理: 「もし変数 
     を 
     にした場合、目的関数値が 
     よりも悪化することが確実であれば、その変数は最適解で 
     になることはあり得ない」
  • 判定式: 
     (最小化問題の場合)
    • この条件が満たされる場合、変数 
       を 
       に固定(Fix)して以降の探索から除外できます。
3. この手法のメリット
  • 探索空間の削減: 枝分かれ限定法(Branch and Bound)において、調べるべきノードの数を劇的に減らすことができます。
  • 高速化: 特に変数の数が多い大規模な問題において、計算時間の短縮に直結します。
この手法は、多くの商用ソルバー(Gurobi, CPLEXなど)やオープンソースのソルバーにプリソルブ(前処理)やノード内での絞り込み戦略として標準的に組み込まれています。
C++疑似コードで。

2026年2月24日

C++ソルバー(Gurobi, CPLEX, SCIPなど)を想定した 
Reduced Cost Fixing の疑似コードを以下に示します。
この手法は、暫定解(現状の最高スコア)とLP緩和解(理想的なスコア)の差を利用して、逆転不可能な変数を切り捨てます。
C++ 疑似コード
cpp
#include <vector>
#include <iostream>
#include <cmath>

// 変数の状態を定義
enum class VariableStatus { FREE, FIXED_TO_ZERO, FIXED_TO_ONE };

struct Variable {
    int id;
    double reduced_cost; // LP緩和解から得られる縮小コスト
    double current_value; // LP緩和解における変数の値
    VariableStatus status;
};

/**
 * Reduced Cost Fixing を実行する関数
 * @param variables 変数リスト
 * @param z_lp LP緩和問題の目的関数値 (下界)
 * @param z_best 現在見つかっている最良の整数解の目的関数値 (上界)
 * @param is_minimization 最小化問題ならtrue
 */
void applyReducedCostFixing(std::vector<Variable>& variables, 
                             double z_lp, 
                             double z_best, 
                             bool is_minimization) {
    
    // ギャップ(あとどれだけ改善の余地があるか)
    double gap = std::abs(z_best - z_lp);

    for (auto& var : variables) {
        if (var.status != VariableStatus::FREE) continue;

        // 最小化問題におけるバイナリ変数の判定例
        if (is_minimization) {
            // 変数が現在 0 で、1 に変えた時のコスト増(reduced_cost)が 
            // ギャップを超えているなら、1 になることは絶対にない
            if (var.current_value < 1e-6 && var.reduced_cost > gap) {
                var.status = VariableStatus::FIXED_TO_ZERO;
                std::cout << "Variable " << var.id << " fixed to 0\n";
            }
            // 変数が現在 1 で、0 に変えた時のコスト増(-reduced_cost)が
            // ギャップを超えているなら、0 になることは絶対にない
            else if (var.current_value > 1.0 - 1e-6 && -var.reduced_cost > gap) {
                var.status = VariableStatus::FIXED_TO_ONE;
                std::cout << "Variable " << var.id << " fixed to 1\n";
            }
        }
    }

2026年3月22日日曜日

キャパシティカットの効果

 前回のキャパシティカットをScheduling Benchmarksのインスタンスに適用してみました。

LBとは、LowerBoundで、LPソルバが出力するObjValueになります。一般にはFractionalな値になります。適用前、適用後でどのようにこの値が変化するかをまとめたのが下表です。

例えば、インスタンス7では、キャパシティカット適用前32だったLBが926に急上昇していることが分かります。厳密解UBに近くなってきていることが分かります。



キャパシティカット適用前instance7、Highsは、1日廻しても解けませんでしたが、適用後は、447秒で解けています。


COPTに至っては、僅か18秒で解けました。



このように、絶大な効果を発揮するインスタンスがある一方で、殆どLBが上がらないインスタンスもあります。この違いは何でしょうか? 

恐らくは、エラー発生箇所の違いによるものでしょう。このキャパティカットは、スタッフの土日の休み数制限、言い換えるとスタッフ側勤務可能キャパシティをカバー制約に伝える働きがあると考えられます。土日勤務に関するカバー制約に対するスラックが0でなくなることの理由付けになります。土日勤務で発生するカバー制約エラーに対して作用します。つまり、土日カバー制約エラーが発生しているインスタンスでは効果がありますが、元々、発生していないインスタンスでは効果がない、と考えられます。

<LBを上げることは正義>

一般に厳密解を得る上で、重要なのは、LBを上げる操作です。極論を言えば、LBがUBに限りなく等しければ、解けたも同然です。LBがUBに近ければ近い程、打てる手は多くなります。

<数式だけでは推測しにくい、インスタンス構造に応じたカットを見つける>

MPSとかLPとかあるいは、CNF/WCNF とか数式モデル化してしまうと、もはや土日の勤務数という、問題構造由来視点での効果的なカットを見つけることは困難です。MIPソルバは、自動でカットを適用しますが、問題構造は分かっていないので往々にして、やみくもカットになることが多いです。もっと効果的なのは、問題自体を作成した作成者自身がインスタンス特性を知っているはずなので、作成者自身がカットを適用することです。









2026年3月21日土曜日

キャパシティーカット

 オリジナル問題で、LBが上がらない原因の一つは、カバー制約(スケジュールナース用語では、列制約)で、緩和解が満足してしまうことです。通常、簡単な資源であれば、LPソルバは、資源が足りない、人が居ない、(←スラックが0でない)を理解してくれますが、制約が複雑だと、理解するところまで行ってくれません。(←スラックが0になってしまう。人が足りていないはずなのに、足りているという結論になってしまう)

次の制約は、インスタンス記述のAutoRosterのrosファイルの記述になります。




    ここで、青部に、パターン記述があります。この記述は、スケジュールナース上では、Python制約に自動変換され、


となります。(rosファイル読み込み時)これらの形式は、SchedulingBenchmarks  instance1-instance24で、同じです。制約の意味は、

土日に一回でも勤務したなら、1カウントとして、総計MAX以内に収める(ハード制約)

というものです。

ここでパターンを見て頂きたいのですが、3つのパターン

~YY

Y~Y

~Y~Y

のORを取って、総計制約(SEQLE)しています。Yは、やすみ(休み)のYシフトです。~Yは、休みの否定で、勤務を表します。

つまり、

土日    |

------------------------------

~YY :土勤務 日曜休み

Y~Y :土休み 日曜勤務

~Y~Y:土日共勤務

のいずれかだったらを1カウントとして総計カウントして制約、が上になります。この演算では、3AND項と1OR 項が演算として使われています。Y~Yの2種類が2つあるので、全部で4パターンが存在します。4パターンのうち、3パターンを使いました。


しかし、それだったら、

土  | 日

~Y | ~Y :土曜日勤務もしくは、日曜日勤務

をカウントした方が、OR項が1項だけで済みます。これは、残りの1パターンYYを否定したもの(=既述3パターン)です。~(YY) → ~Y |~Y (ドモルガンの法則) 

これをPython制約にしたのが、次です。


プログラムでそのようにしたのではなく、エディタで置き換えたました。(プログラム化するのは難しい)

このように問題のモデル化(モデリング)を分かり易くすると、LPソルバも余計な演算をしなくてよいので、LBが上がる(スラックが0でなくなる)可能性があります。

上の制約は、各スタッフ毎に決まるMAX値になります。各Dayでは、各スタッフの各Shiftを累計して、[Max,Min]に制約しますが、上のような書き方をすることで、LPソルバに資源の制限をより分かり易く伝えることが出来ます。資源の供給側である各スタッフは、Max回までしか土日勤務はできないんだよ。とLPソルバに分かり易く伝えることができます。

一方カバー制約側では、各スタッフ累計[Max,Min]要るんだよ、と言う要求を満たすように各スタッフに勤務を割り当てます。ここで、LPソルバは、緩和という技を持っているので、Fractinalな値(0.5とか)を使って、表面上取り繕う([Max,Min]を満たす)動きをしてきます。

このような人的資源に関わる制約に絡んできます。今回の場合、追加ではありませんが、明示的な資源制約追加を一般に、キャパシティカットと呼んでいます。

出来る限りFractionalな値を持ち出されないようにする、というのがその真意です。

モデル作成時(問題作成時)にそこまでやってくれれば、苦労が減るのですが、一般の実務問題でも、上のような簡単化(ユーザヒアリング→モデル化)をすると、問題が簡単になるのは、良く遭遇します。







2026年3月18日水曜日

Q. 表示用シフト集合の作り方

 Q.

1.シフト集合にシフト集合名「宿直カウント用」、ラベル「宿直集合」、演算子「または」、

シフト名「宿直」、「30分遅・宿直(終了日が休日)」、「60分遅・宿直(終了日が休日)」

及び「30分遅・30分早上がり宿直(終了日が平日)」を追加する。


2.行制約グループ1の宿直回数の「宿直」を「宿直集合」に変更する。

とすれば、ひょっとしたらうまくいくのでは?と思いましたが、2の設定ができませんでした。


Ans.

シフト集合の要素は、「シフト」である必要があります。シフト集合の要素をシフト集合とすることは出来ません。エントリしたいシフト集合のシフトに分解してエントリしてください。


演算子は、または(非自動含む)を選択してください。(自動割り当てされないシフトも含むので。)

制約は、≧0とすることで、実質制約することはしません。表示Only目的での制約の書き方になります。


以上です。最後に動作を確認して終了です。



2026年3月17日火曜日

Q. カウント表示がおかしいのでは?

Q. 2内Aに宿直が1回割り当てられていますが、2内Aの宿直回数はゼロ回になってしまっています。

Ans.

当該部を拡大してみると、宿直60分遅となっており、同じ青色の常勤医師の宿直とは異なるラベルになっています。


実際、にシフト定義を見ると、シフト宿直とは異なり、派遣医師用の特殊シフト(自動割り当てされない、予定入力専用シフト)であることが分かります。

実際の制約は、宿直のみに作用しています。

結論として、宿直回数0になっているのは正しい、です。

<考察>
同じ青色でしたので、誤解を招く結果となりました。対策として、

1)宿直60分等の色を少し変える
2)例えば、総合宿直のような合成集合シフトを作って、≧0で制約する

等が考えられます。



2026年3月16日月曜日

Q. アルゴリズム1は、過去に比べてどのくらい良くなってますか?また、今後の見通しについては?

 Ans. 

最新のSAT Competitionを推移を見ると、確かに僅かずつではありますが、性能向上が認められます。が、必ずしも、ナーススケジューリング問題用に向上している訳ではありません。むしろ、1世代前の方が良い場合もありえます、というよりもその場合の方が多いです。

過去との比較、スケジュールナース1/2との明確な比較結果はありませんが、体感できるくらいの性能向上は出来ていると思います。

現在リリース中のソルバは、1世代前のCryptiminisatのあるバージョンを修正したものを使用しています。最近、最新ソルバに置き換えたものを比較してみましが、全く最新ソルバは、現行リリース版に勝てませんでした。従い、現在リリース中のものが、アルゴリズム1としては、過去現在において最高性能であることの裏付けが取れました。

推測になりますが、アカデミックな関心がSAT competitionで優勝することに向いていて、実務的な性能向上から離れてしまっていることに原因があるような気がしています。具体的には、incremental solvingに関する向上が殆どない、ことに起因します。

今後については、アルゴリズム1単独での性能アップはあまりなく、他の種類のソルバとの連携において力を発揮する、と考えています。そのための統合化作業を現在進行中です。

2026年3月15日日曜日

MDDサイズは、Kに比例する

幅0の係数制約について考えてみます。幅0とは、

ΣCoeff[i]*X[i]≦K、ΣCoeff[i]*X[i]≧K

と、最大・と最小がKに一致した問題です。

K=0のMDDグラフです。幅1であることが分かります。選択肢がありません。

X[i]=0 ∀i

であることを要求しているので当然です。



K=1のMDDグラフです。0または1の2種選択肢があります。




K=4のMDDグラフです。定常状態では、5つの選択肢(幅)となります。




K=10のMDDグラフです



一般にイメージされがちなのは、最大・最小が同じなら、K=1でも、K=10でも同じ解空間では?と思いがちですが、実態は、全く異なり、ほぼKに比例するということです。

幅0が効くのは、最後だけであって、そこから離れれば離れる程、幅は広くなり最終的にK幅となります。ざっくり、問題の難易度は、Kに比例するということです。


これを勤務表作成問題に当てはめると、

■制約日数が長ければ長い程難しくなる

■スタッフ人数が多ければ多いほど難しくなる

ということが理解されると思います。

例えば、夜勤人数1名(1名以上1名以下)と夜勤人数10名(10名以上10名以下)では、夜勤人数10名の方が10倍程度、上で見たようにノード変数数が増えるので、それだけ解きにくくなる、ということです。月数が12カ月になれば、さらにノード数は、12倍となります。全体としては、120倍難しくなります。これが、combinatorial explosion組み合わせ爆発の源です。

ナーススケジューリング問題は、NP困難であることが証明された問題であり、問題が大規模になれば、やがてReasonableな時間内に解くことは出来くなる、ことが分かっています。しかし、数学が証明しているのは、それだけであり、この規模なら解けない、と断じている訳ではありません。私達研究者は、神様が決めた限界を突破することはできませんが、解ける限界範囲を出来る限り伸ばそう!と日々努力しているということです。


2026年3月14日土曜日

Q.入力ラベルの配置を変えたい

 Q.入力ラベルを入れ替えたい。


Ans.まず、ラベルの順番について説明します。次のような順番に固定されており、順番を変えることは出来ません。

  シフト名→ 別名1→ 別名2..

     ↓

  シフト名→ 別名1→ 別名2..

         .....

         シフト集合名

     ↓

         シフト集合名

         .....

方策1) シフト別名で使っていないものは、削除します。結果的に右のラベル群はスライドして上がってきます。

方策2) シフト別名をシフト集合で定義し直します。 シフト集合内での順番は、変えることが出来ます。不要なものは、後ろに配置することで、結果的に右のラベル群はスライドします。

方策3) キーボードのbinding使用

  https://schedule-nurse.blogspot.com/2023/11/blog-post.htm
 

  頻繁に使用するラベル順に定義します。慣れれば圧倒的に速いです。

  なお、名前は、Excel等でと短冊シートを作ってキーボード上部に貼っておくと、慣れるまでの間重宝します。




2026年3月13日金曜日

ReducedCostとは?

 HIGHSソルバで、LPを解くと、4つのSolutionが出てきます。solutionというと、通常は、ColSolutionになります。これは、SAT解を得たときの解に相当します。Dualは、双対と訳されます。双対とは、

目的関数 ⇔ 制約

の間にある数理的な関係を指しています。

制約を固定して、目的関数を最初にする(主問題)のと、

目的関数を固定して、制約を最大化(双対問題)する

のは、ある種の対称性があります。これは、Linear System全てについて当てはまり、主問題があれば、双対問題も自動的に定まります。これは、OR特有の概念です。双対定理は、


■Col Solution ⇒x(i)

■Dual Solution  ⇒y(i)

■Row Activity

■Reduced Cost  ⇒r(i)

です。数理ソルバで特に重要なのは、ReducedCostです。

Reduced Costは、

rj=cj-ajTy

で計算出来ます。

ciは、xについて目的関数値の係数です。yは、dual値、ajは、システムマトリクスの係数です。ReducedCostは、x(i)に付随する量なので、x(i)サイズ分値を持ちます。x(i)に関して 目的関数の悪化(改善)具合を表していると考えられます。多くのMIPソルバ内で、PuedoCost(疑似コスト)の初期値としても使われています。

スケジュールナースの数理ソルバ内では、このReducedCostを駆使しています。


2026年3月12日木曜日

Daul==0でスラックに余裕があるとき、その行は削除してよいか?

 インスタンス24は、制約が重すぎてLPソルバが遅いので、一時的に削除できないかを検討しました。

結論的には、上記条件でも以下の議論の通り削除することは出来ません。それ以前に、オリジナルLPで、お目当ての行群中、DUAL=0の行が存在しなかったので、それも意味がなく、削除不能という結論です。


❗ DUAL値 = 0 でも制約を削除してはいけない理由

DUAL値が 0 というのは、**「最適解においてその制約が binding ではない」**というだけです。

binding ではない → 余裕がある

余裕がある → 緩めても目的関数は改善しない

しかし → 削除すると可行領域が広がり、別の解が出てしまう可能性がある

つまり、


制約は「最適解を規定する」だけでなく、「不適切な解を排除する」役割も持つため、

DUAL値だけで削除判断をするのは危険です。


🧭 では、どんなときに削除してもよいのか?

以下の条件が揃うと、削除しても安全な場合があります。

1. 制約が常に冗長(redundant constraint)であると証明できる

例:

他の制約の線形結合で表せる、または常に他の制約により厳しく支配されている。

これは数学的に確認できる場合のみ。

2. 制約の右辺を大きくしても可行領域が変わらない

DUAL値 = 0 の制約には「許容範囲(allowable increase/decrease)」があります。

allowable decrease が無限大

allowable increase が無限大

のように、右辺をどれだけ動かしても可行領域が変わらない場合は、削除しても影響がありません。

3. 制約が論理的に不要であることが問題構造から明らか

例:

上限 100 の制約と上限 80 の制約が両方ある → 上限 100 は不要

x ≥ 0 と x ≥ -5 → x ≥ -5 は不要

2026年3月11日水曜日

Row Activityとは?

 Row Activity は、**「その行(制約)の左辺が、最適解で実際にどの値になっているか」**を表す量です。

LP の感度分析や制約の状態を理解するうえで基本になる指標で、Primal 解から直接読み取れます。

Row Activity と Slack の関係

Σa(i)*x(i)≦b

のとき、Slackは、Σa(i)*x(i)です。

なので、Row Activity を見ると制約がどの状態かが分かります。

Row Activity = b → Slack = 0 → 制約は binding(効いている)

Row Activity < b → Slack > 0 → 制約は非 binding(余裕あり)


Row Activity が示すこと

Row Activity は、次のような情報を与えます。

● 1. 制約が最適解でどれだけ使われているか

勤務回数制約なら「何回勤務しているか」など。

● 2. 制約が binding かどうか

Slack と組み合わせて判断できます。

● 3. 制約の削除・緩和の影響を考える材料

Row Activity が b に近いほど、その制約は効いている可能性が高い。


シフト最適化の場合、Row Activity は次のように役立ちます。

勤務回数制約の実際の使用量を確認する

連続勤務制約がどの程度「ギリギリ」かを見る

2026年3月10日火曜日

Dual値が意味するもの

OR理論を駆使して問題を解くときの基礎です。

 

DUAL値 = 制約を 1 単位だけ緩めたとき、目的関数がどれだけ改善するかを示す感度係数。

最大化問題なら:制約を緩めると目的値が どれだけ増えるか

最小化問題なら:制約を緩めると目的値が どれだけ減るか

つまり、DUAL値は 制約の価値 を表します。

📌 1. DUAL値の直感的な意味(影の価格)

DUAL値は、経済学では shadow price(影の価格) と呼ばれます。

ある制約が「資源の上限」を表す場合

→ DUAL値は「その資源を 1 単位追加できたら、目的関数がどれだけ良くなるか」を示す。

例:

制約Aを緩めると、目的関数が△減少する

その減少量が DUAL値。


📌 2. 数学的な意味(双対問題の最適解)

LPには必ず 双対問題(DUAL problem) が存在し、

DUAL値はその双対問題の最適解の変数値です。

primal:変数 x

dual:変数 y(これが DUAL値)

双対問題は「制約の重み付けによって目的値を評価する問題」で、

DUAL値はその最適な重み付けを表します。


📌 3. 実務での解釈(制約の重要度)

DUAL値は、制約がどれだけ「効いているか」を示します。

● DUAL値 ≠ 0

→ その制約は binding(効いている)

→ 制約を緩めると目的値が改善する

→ 資源が不足している状態

● DUAL値 = 0

→ 制約は 余裕がある(非binding)

→ 緩めても目的値は変わらない

→ 資源が余っている状態



📌 6. 注意点

DUAL値は「制約の右辺を微小に変化させたとき」の効果

→ 大きく変えると線形性が崩れ、DUAL値は使えない

整数計画(MILP)では DUAL値は一般に意味を持たない

→ LP緩和でのみ有効

2026年3月9日月曜日

LPソルバで、時間制約係数和を制約する

 失敗しました。

インスタンス23では、僅かに効果があるものの、殆どあり得る最大・最小値に張り付いてしまっています。全く期待通りにはなっていません。係数4,5,6が各々最大・最小に張り付いているというということは、全く範囲を絞れないということです。


インスタンス24に至っては、その傾向が一層激しくなりました。

原因は、

1)UBでカットオフしていない

2)LBとUB GAPが大きすぎる

3)緩和解

が、考えられます。

割に小さなインスタンス14では、

狙い通りの効果を発揮することが出来ます。LBとUB(1278)GAP差が小さいことが効いているように思えます。


暫定UB値でカットオフする、or CutGeneratorを導入すれば改善する可能性がありますが、そもそも、それが出来ないので苦労している訳で、別なアプローチを取ることにしました。

2026年3月8日日曜日

Lpソルバの構築方法の見直し

下記問題が発生しました。 

x1, x2,x3.. 等の定義順とソルバの定義順が異なるので、変換テーブルが必要になる。この後の操作で、いちいち変換テーブル参照が必要となりバグの温床になる。

https://schedule-nurse.blogspot.com/2026/02/cnf2lp.html

原因

Highs/COPT等では、出現順に変数を設定してしまう


解決策

目的関数記述時に全変数をなめる。

Minimize

 obj: 0 x1 + 0 x2 + 0 x3 + 0 x4 + 0 x5 + 0 x6 + 0 x7 + 0 x8 + 0 x9 + 0 x10 + 0 x11 + 0 x12 + 0 x13 + 0 x14 + 0 x15 + 0 x16 + 0 x17 + 0 x18 + 0 x19 + 0 x20 + 0 x21 + 0 x22 + 0...

のような感じです。これで変換テーブルが必要なくなり、LPファイルを読み込ませた後のAPI操作が楽になります。

void writeToFile(const std::string& filename) {
    std::ofstream out(filename);
    
    // 1. 目的関数ですべての変数を順番に「宣言」する
    out << "Minimize\n obj: ";
    for (int i = 1; i <= max_idx; ++i) {
        double coeff = 0.0;
        if (weights.count(i)) {
            coeff = weights[i];
        }
        
        // 符号と項の出力
        if (i > 1) {
            out << (coeff >= 0 ? " + " : " - ");
        } else if (coeff < 0) {
            out << "- ";
        }
        
        out << std::abs(coeff) << " " << getVarName(i);
    }
    out << "\n";

    // 2. 制約式
    out << "Subject To\n";
    // ... (以下略) ...
}

2026年3月5日木曜日

個々のRosterの時間計数和の最大・最小を求める

 問題を解く前のPreconditionを求める問題です。時間制約は、MDDで解くことにしましたが、未だ規模が大きすぎるので、小さくすることを検討しました。個々の時間計数和で規制できれば、大幅にMDDサイズを小さくできます。

https://schedule-nurse.blogspot.com/2026/02/mdd_14.html

1)個々のRosterの簡易モデルをMIPソルバで解く

詳細モデルまで含めてHighsMIPソルバで厳密解を求めるのは難しいです。仮に求まったとしても、個々のRoster空間しか見ていないので、最小0、最大時間制約値となってPreconditionの意味をなさない可能性があります。

2)全体LPの最大・最小問題として解く

前回の検討で、GPUを用いたPDLPXであれば、全体目的関数値AがReasonableな時間内に求まることが分かりました。

https://schedule-nurse.blogspot.com/2026/02/cnf2lp.html

https://schedule-nurse.blogspot.com/2026/03/lp.html

今真値UBは、当然分かっていないのですが、あり得るUB値Cでカットオフし、目的関数を個々の係数和の最大・最小問題に置き換えます。今係数は、4,5,6の3種類、最大最小でx2、150人で、x150回、=900回求解すれば、全体LPで規制される(目的関数値C以下)条件化における、あり得る最大最小値が、Preconditionとして導入することが出来ます。この範囲が狭ければ狭いほど、MDDサイズを小さくできます。個々の空間しか見ていない1)に対して、緩和解ではあるもの全体空間を見ているので、1)よりは小さくできるものと期待されます。

問題は、あり得るUB値Cにあります。それは仮定であるということになります。最終的に、首尾よくUBが求まった場合のみ意味あるということです。A値は、緩和解における目的関数値であり、真値UB Bが数万に対して、Aは、数千に過ぎません。間違ってもA値でカットオフしてはいけません。


2026年3月3日火曜日

Lpソルバで緩和解

 以下は、スケジューリングベンチマークスの問題をLpソルバで解いたときの、時間です。SAT Solveする際に、以下のルーチンをCALLしてLPファイルを作成→COPTのinteractive commandでMPSファイルに変換しています。

https://schedule-nurse.blogspot.com/2026/02/cnf2lp.html


測定は、PDLPXで、GPUは、4060 16GBです。


ここで、着目するのは、Known UBからは遥かに遠い値になってしまうということです。この状態からCutting Planeを駆使してUBまで持ってくるには、相当のMIP Solving技術がないとInstance8程度でも難しいです。(Highsでも1日程度かかります)

単純な緩和解との乖離が大きいというのは、ナーススケジューリング問題の本質的な難しさを示しています。

もう一つは、巨大なInstance23/24でもFirst Order Solver PDLPXは、難なく解けているということです。このあたりになると、MPSファイルで、2.4GBにもなります。COPTのバリアソルバでも解けない規模になりますが、難なく解けているのがPDLPイノベーションです。

どのようにしてSAT SolvingとLpソルバ緩和解を結びつけるか?というのが革新的核心技術になります。



2026年3月2日月曜日

ZOOMレクチャをお願いしたい

 Q.サブスクですが、ZOOMレクチャをお願いできますか?


年間サブスクをご購入された方特典として、ご要望があればでしたら、貴職場のルールをお聞きしながら、一緒に貴職場のプロジェクト作成することは可能です。事前知識は、必要ありません。初回1回に限り(無償)です。

プロジェクト作成の性質上、休憩を入れながら約半日のお時間を頂きます。終わった後に、録画URLをご案内しますので、後で見返して練習することが出来ます。

毎月15日ー25日はサポートで混みますので、それ以外の日で、お申込みからおよそ一カ月程度お待ち頂いております。

条件としては、サンプルを動かせた、というところまで出来ていることです。

2026年3月1日日曜日

Cadical3.0 incremental で動作させる為に

Minisatの延長だろうと考えて、incrementalに動作させようとすると、地雷を踏むことになります。

基本的知識として、ervarsの存在を意識して書くことが必要です。solveの後、割り当てていないはずなのに、vars()してみると、何故か自分が割り当てた変数数より増えています。これが、ervarsによるものです。

eravarsとは何か?AIがドキュメントしていない内部事情を説明してくれています。

Q.CADICAL3.0 で、ervarsとは、何ですか?

結論から言うと、CaDiCaL 3.0 の “ervars” は、Extended Resolution(拡張解決)で導入された補助変数(extension variables)を指す内部カウンタです。
ただし、公開されている一般向けドキュメントではほとんど説明されておらず、主に 内部統計(statistics) として現れます。


🧩 “ervars” の正体

CaDiCaL には、内部状態を表示するための統計項目が多数あります。その中に ervars があり、これは:

  • ER(Extended Resolution)で導入された新しい変数の数
  • ソルバーが内部で式を簡略化したり、学習節を扱う際に導入する補助変数
  • ユーザが入力した元の変数とは別に、ソルバーが自動生成する

という意味を持ちます。

Extended Resolution は、SAT ソルバーが探索を効率化するために、
[ x \leftrightarrow (a \land b) ]
のような新しい変数を導入して式を分解し、学習節の質を高める技法です。

CaDiCaL 3.0 では、この ER を内部的に利用しており、その結果として 導入された補助変数の総数が “ervars” として統計に出ます。


📌 なぜ “ervars” が重要なのか

  • ER 変数が多いほど、ソルバーが内部で式を再構成している
  • 問題によっては ER が効率化に寄与する
  • 逆に、ER が増えすぎるとメモリや節のサイズが増える可能性もある

つまり、ソルバーの内部変形の度合いを示す指標として役立ちます。


🔍 参考情報

CaDiCaL の GitHub リポジトリには統計項目の一覧がありますが、ervars の詳細説明はありません。
ただし、CaDiCaL 2.0/3.0 の論文やコードコメントでは、ER(Extended Resolution)を内部で利用していることが確認できます。


次に知りたいことはありますか?

例えば:

  • CaDiCaL の統計項目(vars, elimed, substituted など)の意味
  • Extended Resolution が SAT ソルバーでどう使われるか
  • CaDiCaL 3.0 の内部構造や新機能の解説

どれを深掘りしましょう?



結論として、ervars[eidx]true になってしまう原因は、CaDiCaL が内部で「その変数は ER(Extended Resolution)で導入された補助変数 である」とマークしているためです。
その結果、is_valid_input()「ユーザ入力としては不正」 と判断し、assert が落ちます。


🔍 is_valid_input() が何をチェックしているか

あなたが示したコードは CaDiCaL の内部チェックです。

inline bool is_valid_input (int elit) {
  assert (elit);
  assert (elit != INT_MIN);
  int eidx = abs (elit);
  return eidx > max_var || !ervars[eidx];
}

この条件を満たすと「入力として有効」と判断されます。

  • eidx > max_var
    ユーザが入力した変数番号より大きい番号なら OK
    (つまり内部変数は OK)

  • !ervars[eidx]
    その変数が ER 変数でなければ OK

逆に言うと、

eidx <= max_var かつ ervars[eidx] == true のとき、入力として不正

になります。


🧩 なぜ ervars[eidx] == true になるのか

CaDiCaL は内部で Extended Resolution(拡張解決) を使い、
探索や学習節の質を上げるために新しい補助変数を導入します。

このとき、

  • その変数は ユーザ入力の変数ではない
  • 内部で生成された ER 変数
  • ervars[eidx] = true とマークされる

という扱いになります。


❗ あなたのケースで起きていること

あなたが elit として渡した変数が、

  • ユーザが入力した変数番号の範囲内(eidx <= max_var
  • かつ 内部で ER 変数としてマークされている(ervars[eidx] == true

という状態になっているため、
is_valid_input()「これはユーザが触ってはいけない内部変数だ」 と判断し、エラーになります。


🧠 どういうときに起きるか(典型例)

次のような状況が考えられます。

1. あなたが ER 変数を外部から参照してしまっている

  • CaDiCaL が内部で導入した変数番号を、
    ユーザ側のコードが誤って扱っている。

2. max_var の扱いを誤っている

  • solver->vars() の値を正しく追跡していない
  • 変数追加後に max_var を更新していない

3. 変数番号の再利用・再割当てのバグ

  • 自前で変数番号を管理している場合、
    CaDiCaL の内部変数と衝突してしまうことがある。

4. CaDiCaL の内部状態を壊してしまっている

  • freeze(), add(), assume() の順序が不正
  • simplify()eliminate() の後に古い変数番号を使っている

🧪 デバッグのために確認すべきこと

次の情報があると原因を特定できます。

  • elit の値(問題の変数番号)
  • max_var の値
  • ervars[eidx] が true になったタイミング
    (ER 変数は通常、内部処理中に生成される)
  • あなたが変数を追加した順序
    solver->new_var() を使っているか?

📌 まず確認してほしいこと(重要)

あなたが使っている変数番号は、必ず solver->vars() 以下になっていますか?

もし内部で CaDiCaL が導入した ER 変数を誤って参照しているなら、
それが今回の assert の直接原因です。


続けて原因を特定しましょう

次の 2 点を教えてください。

  1. 問題が起きたときの elit の値
  2. あなたが変数を生成しているコード(new_var() を使っているか)

これが分かれば、どこで ER 変数と衝突しているかを正確に特定できます。



結論として、インクリメンタルに節・変数を追加した後に ervars[eidx] == true になって落ちる場合、CaDiCaL の内部で “ER 変数(Extended‑Resolution 変数)” が生成され、それが ユーザ変数領域(1..max_var)に衝突してしまっている ことが最も典型的な原因です。
これは CaDiCaL 3.0 で特に起きやすい挙動です。


1. 何が起きているか(内部での変数生成と衝突)

CaDiCaL は、インクリメンタルモードで節を追加していくと、途中で以下のような内部処理を行います。

  • 節の簡約化
  • elimination
  • vivification
  • strengthening
  • extended resolution(ER)

この ER の過程で、新しい内部変数が勝手に生成されることがあります。
この内部変数は ervars[]true としてマークされます。

本来、内部変数は max_var より大きい番号を使うため、ユーザ変数と衝突しません。

しかし、次のような状況が起きると衝突します。

● 原因 A:あなたが max_var を固定値として扱っている

CaDiCaL は内部で変数を増やすため、
solver->vars() の値は 途中で増えることがあります。

もしあなたが外部で

  • 自前で変数番号を管理している
  • あるいは max_var を初期値のまま固定している

と、内部変数とユーザ変数の番号が重なることがあります。

● 原因 B:インクリメンタル追加の順序で ER が走り、既存変数が ER 変数扱いになる

CaDiCaL の内部簡約化で、
「この変数は内部的に拡張解決に使う」と判断されると、
ユーザが追加した変数でも ervars[eidx] = true になることがあります。

その後、あなたがその変数を再び入力として使うと、

eidx <= max_var かつ ervars[eidx] == true

となり、is_valid_input() が false を返して assert が落ちます。


2. 典型的に起きるタイミング

次のような操作の後に起きやすいです。

  • 多数の節を追加した後に simplify() を呼ぶ
  • elimination が走る
  • vivification が走る
  • solve → 追加節 → solve → 追加節 … を繰り返す
  • assumptions を使ったインクリメンタル探索

特に solve の後に変数を追加すると、内部状態が変わっているため衝突しやすくなります。


3. どう対処すべきか(実務的な解決策)

✔ 対策 1:変数は必ず solver->new_var() で生成する

自前で変数番号を管理せず、CaDiCaL に任せるのが最も安全です。

✔ 対策 2:solve の後に変数を追加しない

CaDiCaL は solve の後に内部状態を大きく変えるため、
その後に変数を追加すると衝突しやすくなります。

✔ 対策 3:インクリメンタルモードでは freeze() を使う

変数を消されないようにするために、
ユーザ変数は solve 前に freeze(var) しておく必要があります。

✔ 対策 4:ER を無効化する(ビルドオプション)

CaDiCaL は ER を内部で使いますが、
ビルド時にオプションで無効化できます(ただし 3.0 では制限あり)。


4. 追加で確認したいこと(原因特定のため)

次の 3 点を教えていただけると、正確に原因を特定できます。

  1. 変数を追加する方法

    • new_var() を使っていますか?
    • それとも自前で番号を振っていますか?
  2. solve の前後で変数を追加していますか?

  3. freeze() を使っていますか?


これらが分かれば、どこで ER 変数と衝突しているかを完全に特定できます。



結論として、CaDiCaL の公式 C++ API には new_var() は存在せず、ユーザが使えるのは resize(int min_max_var) だけです。
これはあなたの理解どおりで、CaDiCaL は「ユーザが変数番号を管理する」設計になっているため、MiniSAT 系のような new_var() は提供されていません。

ただし、ここに今回の落とし穴があります。


CaDiCaL の変数管理の仕組み

● 1. ユーザは resize(n) で「最大変数番号」を宣言する

例:

solver.resize(100);  // 1..100 がユーザ変数

● 2. CaDiCaL は内部で ER 変数を追加する

CaDiCaL 3.0 は内部簡約化や Extended Resolution により、
ユーザが知らない内部変数を勝手に追加します。

この内部変数は:

  • 番号は max_var + 1 以降
  • ervars[] = true とマークされる
  • ユーザが触ってはいけない

という扱いになります。


今回の問題の本質

あなたが自前で変数番号を振っていたため、次のような衝突が起きました。

  1. resize(N) でユーザ変数領域を宣言
  2. CaDiCaL が内部で ER 変数を追加(番号は N+1, N+2, …)
  3. あなたが自前で変数番号を増やしていく
  4. 内部変数と同じ番号をユーザが再利用してしまう
  5. CaDiCaL 側は「その番号は ER 変数だからユーザ入力は不正」と判断
  6. is_valid_input() が false → assert で落ちる

つまり:

CaDiCaL の内部変数番号と、あなたの自前の変数番号が衝突した

これが原因です。


正しい変数管理方法(CaDiCaL 3.0)

✔ 1. 変数番号は「常に単調増加」で管理する

内部変数は max_var を超える番号を使うため、
ユーザ側も 常に max_var より大きい番号を使う必要があります

✔ 2. 変数を追加するときは必ず resize() を呼ぶ

例:

int v = ++my_max_var;
solver.resize(my_max_var);

✔ 3. solve の後に変数を追加する場合は特に注意

内部で ER が走り、内部変数が増えている可能性があります。
solve の後は必ず:

int internal_max = solver.vars();  // CaDiCaL が現在使っている最大変数番号

を確認し、
ユーザ変数は internal_max より大きい番号から割り当てる必要があります。


実装例(安全な変数管理)

int next_user_var = solver.vars();  // CaDiCaL が現在使っている最大変数番号

int new_var() {
    next_user_var++;
    solver.resize(next_user_var);
    return next_user_var;
}

これで内部変数と衝突しません。


まとめ

  • CaDiCaL には new_var() がない → 正しい
  • 代わりに resize() で最大変数番号を宣言する
  • CaDiCaL は内部で ER 変数を追加する
  • 自前で変数番号を振ると内部変数と衝突する
  • 解決策は「常に solver.vars() を基準に変数番号を増やす」

CaDiCaL 3.0 のインクリメンタル利用は落とし穴が多いので、
あなたの現在の変数割り当てコードを見せてもらえれば、
衝突しない安全な形に書き換えます。


その理解はまさに正しくて、CaDiCaL 3.0 をインクリメンタルに使う場合は「多めに resize しておき、内部で増える vars(内部変数)を監視しながらユーザ変数を割り当てる」設計が最も安全で高速です。

ここからは、なぜそれが必要なのか、どこに落とし穴があるのか、そして実際の安全な設計パターンを整理しておきます。


CaDiCaL の変数管理が難しい理由

● 1. CaDiCaL は内部で ER 変数を勝手に増やす

Extended Resolution(ER)や elimination の過程で、
内部変数が max_var + 1 以降に追加される

このとき ervars[] = true になる。

● 2. resize は内部アロケータを動かすので重い

resize(n) は内部ベクタや watchlist の再確保が走るため、
頻繁に呼ぶと確実に遅くなる。

● 3. solve のたびに内部変数が増える可能性がある

インクリメンタルで solve → add → solve を繰り返すと、
内部変数がどんどん増える。


推奨される設計パターン(実務的に最適)

🟦 1. 最初に「十分大きい」サイズで resize しておく

例:

solver.resize(2000000);  // 200万変数分を確保しておく

これで内部アロケータの再確保を避けられる。

🟦 2. CaDiCaL の内部変数数(solver.vars())を常に監視する

内部変数が増えると solver.vars() が増える。

ユーザ変数を追加するときは:

user_var = solver.vars() + 1;

のように、内部変数の最大値を常に避ける

🟦 3. 自前の new_var() を安全に実装する

int next_var = solver.vars();  // CaDiCaL が現在使っている最大変数番号

int new_user_var() {
    next_var++;
    // resize は最初に十分大きく取っているので呼ばない
    return next_var;
}

これで内部変数と衝突しない。

🟦 4. solve の後は必ず solver.vars() を再取得する

solve の後に内部変数が増えている可能性があるため:

next_var = solver.vars();

を必ず更新する。


なぜこの設計が最適なのか

  • resize を頻繁に呼ばない → 高速
  • 内部変数と衝突しない → ervars[eidx] エラーが消える
  • CaDiCaL の内部最適化(ER, elimination)を阻害しない → 強い伝播が維持される
  • インクリメンタルでも安全に動く

CaDiCaL 3.0 の内部変数生成はかなり積極的なので、
ユーザ側が変数番号空間を広く確保しておくのが最も現実的です。


実装テンプレート(あなたの用途向け)

// 初期化時に十分大きく確保
solver.resize(2'000'000);

int next_var = solver.vars();  // CaDiCaL の内部最大変数番号

int new_user_var() {
    // solve の後は next_var を solver.vars() で更新すること
    next_var++;
    return next_var;
}

// solve の後
solver.solve();
next_var = solver.vars();  // 内部変数が増えている可能性がある

ひとこと

あなたの「多めに resize しておいて、vars を watch する」という設計は、
CaDiCaL 3.0 のインクリメンタル利用では最適解です。


必要であれば、あなたの現在のコード構造に合わせて
**完全に安全な変数管理ラッパー(C++ クラス)**を作ることもできます。



結論として、200 万変数を常に確保する必要はなく、むしろ「必要になりそうな最大値を見積もって、余裕を持って一度だけ大きめに resize しておく」設計が最適です。
200 万という数字はあくまで例で、問題規模・内部 ER の発生量・インクリメンタルの深さによって適切な値は変わります。


どれくらい「多め」に確保すべきか

CaDiCaL 3.0 の内部挙動を踏まえると、次の 3 点が重要です。

1. 内部 ER 変数は問題によっては数万〜数十万増える

  • elimination
  • vivification
  • strengthening
  • extended resolution

これらが走ると、内部変数が ユーザ変数の 1.2〜3 倍 まで増えるケースがあります。

2. solve のたびに内部変数が増える可能性がある

インクリメンタルで solve → add → solve を繰り返すと、
内部変数が累積的に増える。

3. resize は重いので頻繁に呼ぶべきではない

内部ベクタ・watchlist の再確保が走るため、
resize はできれば 1 回だけが理想。


実務的な「最適戦略」

🟦 1. 問題規模から最大変数数を見積もる

例:

  • ユーザ変数が最大 30 万
  • ER で 2〜3 倍増える可能性
    → 100 万〜150 万を確保しておく

🟦 2. 一度だけ大きめに resize

solver.resize(1'500'000);

🟦 3. 以降は resize を呼ばず、solver.vars() を監視

内部変数が増えるので:

int next_var = solver.vars();

ユーザ変数を追加するときは:

next_var++;
return next_var;

🟦 4. solve の後に next_var を更新

solver.solve();
next_var = solver.vars();

「200 万は多すぎるか?」の判断基準

多すぎるケース

  • 問題の最大変数が 5 万以下
  • ER がほとんど発生しない(制約が単純)
  • インクリメンタルの solve 回数が少ない

→ 20〜50 万で十分。

適切なケース

  • 問題が 10〜30 万変数規模
  • elimination や vivification が多く発生
  • solve を何度も繰り返す

→ 100〜200 万は妥当。

少なすぎるケース

  • 内部 ER が大量に発生する
  • solve を 100 回以上繰り返す
  • 変数が 30 万を超える

→ 200 万でも足りない可能性がある。


メモリ的には問題ないのか?

CaDiCaL の変数ごとの内部構造は比較的軽量で、
200 万変数でも 数百 MB 程度に収まります。

現代のメモリ環境では十分許容範囲です。


まとめ

  • 200 万が「多すぎる」かどうかは問題規模次第
  • 重要なのは resize を頻繁に呼ばないこと
  • 内部 ER 変数が増えるので solver.vars() を常に監視
  • 最初に「余裕を持って」確保しておくのが最適

あなたの問題規模(最大ユーザ変数数、インクリメンタル solve 回数、制約の種類)が分かれば、最適な resize サイズを具体的に算出できます。



結論として、**ER 変数(Extended‑Resolution 変数)に対して val() を呼ぶのは CaDiCaL 3.0 では基本的に「NG」**です。
理由は、ER 変数は ユーザが参照することを前提としていない内部専用変数であり、val() の対象として想定されていないためです。


ER 変数に val() が NG になる理由

● 1. ER 変数は「内部専用」で、ユーザ API の対象外

CaDiCaL の ER 変数は、内部の簡約化・強化・拡張解決のために自動生成される補助変数です。

  • ユーザが入力した変数ではない
  • 意味的にも「値を問い合わせる対象」ではない
  • モデルに含める保証もない
  • 削除・再利用される可能性がある

そのため、val() の対象として扱うと 未定義動作になります。

● 2. is_valid_input() が ER 変数を弾く設計になっている

CaDiCaL の内部チェックはこうなっています:

return eidx > max_var || !ervars[eidx];

つまり、

  • eidx <= max_var かつ ervars[eidx] == true
    ユーザが触ってはいけない内部変数

という扱い。

val() も内部でこのチェックを通るため、ER 変数に対して呼ぶと assert が落ちます。

● 3. ER 変数はモデルに含まれないことがある

ER 変数は内部の式変換のためだけに存在するため、
最終モデルに値が割り当てられないこともあります。

そのため、val() が意味を持たないケースが多い。


ER 変数を避けるための実務的な設計

🟦 1. CaDiCaL の内部最大変数番号を常に監視する

int internal_max = solver.vars();

🟦 2. ユーザ変数は常に internal_max より大きい番号から割り当てる

int next_var = solver.vars();
int new_user_var() {
    next_var++;
    return next_var;
}

🟦 3. solve の後は next_var を solver.vars() で更新

内部 ER 変数が solve のたびに増えるため:

solver.solve();
next_var = solver.vars();

🟦 4. ER 変数は絶対に val() しない

ユーザ変数だけを val() の対象にする。


ER 変数を val() してしまう典型的なパターン

  • 自前で変数番号を管理していて内部変数と衝突
  • solve の後に変数を追加している
  • resize を小さく設定していて内部変数がユーザ領域に侵入
  • モデルを走査するときに 1..vars() を全部見てしまう

特に最後のケースは危険で、vars() の範囲には ER 変数も含まれるため、
モデル走査は必ず「ユーザ変数だけ」に限定する必要があります。


まとめ

  • ER 変数は内部専用 → val() は NG
  • CaDiCaL 3.0 は solve のたびに内部変数が増える
  • ユーザ変数は solver.vars() より大きい番号から割り当てる
  • モデル走査もユーザ変数だけに限定する必要がある

モデル走査部分や変数管理部分のコードを見せてもらえれば、
ER 変数を完全に避ける安全な実装に書き換えることもできます。