CP-SATの紹介 ― SATの技術で解く制約プログラミング#
この章で学ぶこと#
OR-Tools の CP-SAT は、Google が作った制約プログラミングのソルバーです。Z3 の章と同じく「条件を書いて解を求める」道具ですが、最適化(利益を最大にする、距離を最小にする)が得意です。
CP-SAT がどんな仕組みで解いているか(SAT の技術+線形緩和)
一番基本的な使い方(
CpModel/new_int_var/add/solve)線形計画法の例(利益の最大化)
論理クイズを
assumptionsで推理する
CP-SAT とは#
CP-SAT は、SAT(充足可能性問題)の技術を土台にした制約プログラミングのソルバーです。
SAT ソルバーの CDCL(衝突を学習して枝刈りする)や LCG(Lazy Clause Generation)といった技術で、探索を賢く進めます。
線形緩和も組み合わせているので、MIP(混合整数計画)ソルバーのように「目的関数を最大・最小にする」最適化もできます。
OR-Tools には古くから constraint_solver(古典的な CP ソルバー)もありますが、そちらは深さ優先探索が中心で、最適化が苦手でした。新しく OR-Tools を使うなら CP-SAT を選ぶのが定石です。この本の OrTools の章は、すべて CP-SAT を使います。
例題:SEND + MORE = MONEY#
Z3 の章でも扱った覆面算を、CP-SAT で解いてみます。使い方の流れは、このコードに全部入っています。
from ortools.sat.python import cp_model
model = cp_model.CpModel()
s, e, n, d, m, o, r, y = [model.new_int_var(0, 10, name) for name in "sendmory"]
model.add(s != 0)
model.add(m != 0)
model.add_all_different([s, e, n, d, m, o, r, y])
send = s * 1000 + e * 100 + n * 10 + d
more = m * 1000 + o * 100 + r * 10 + e
money = m * 10000 + o * 1000 + n * 100 + e * 10 + y
model.add(send + more == money)
solver = cp_model.CpSolver()
solver.Solve(model)
print(f"{solver.value(send)} + {solver.value(more)} = {solver.value(money)}")
9567 + 1085 = 10652
コードの説明#
CpModel()… モデル(制約の入れ物)を作るmodel.new_int_var(下限, 上限, 名前)… 整数変数を作るmodel.add(条件)… 制約を追加する(!=,==,>=などの比較式がそのまま書ける)model.add_all_different([...])… すべて違う値にするsolver.solve(model)… 解を求めるsolver.value(式)… 式の値を取り出す
* や + で式を作れるので、s * 1000 + e * 100 + ... のように「桁」をそのまま書けます。積和は LinearExpr.weighted_sum() でも書けます。
send = cp_model.LinearExpr.weighted_sum([s, e, n, d], [1000, 100, 10, 1])
more = cp_model.LinearExpr.weighted_sum([m, o, r, e], [1000, 100, 10, 1])
money = cp_model.LinearExpr.weighted_sum([m, o, n, e, y], [10000, 1000, 100, 10, 1])
線形計画法#
次は「利益を最大にする」問題です。線形計画法の例として、次の問題を考えます。
ある製品 A と B を作るのに、3種類の原料 a, b, c が必要です。1単位作るのに必要な量と在庫は次のとおりで、1単位売ると A は3万円、B は2万円の利益が出ます。利益が最大になる生産量を決めてください。
原料 |
製品 A |
製品 B |
在庫 |
|---|---|---|---|
a |
3 |
1 |
9 |
b |
2.5 |
2 |
12.5 |
c |
1 |
2 |
8 |
生産量を \(x_1\), \(x_2\) とすると、問題はこう書けます。
目的関数
制約条件
CP-SAT は整数しか扱えません。2.5 が入った式は、両辺を2倍して整数にします(2.5x1 + 2x2 <= 12.5 → 5x1 + 4x2 <= 25)。不等式を2倍しても、条件の意味は変わりません。
model.maximize(z) は「\(z = 3x_1 + 2x_2\) を最大にする \(x_1, x_2\) を探す」という意味です。
model = cp_model.CpModel()
x1, x2 = [model.new_int_var(0, 10000, f"x{i}") for i in range(1, 3)]
model.add(3 * x1 + x2 <= 9)
model.add(5 * x1 + 4 * x2 <= 25)
model.add(x1 + 2 * x2 <= 8)
z = 3 * x1 + 2 * x2
model.maximize(z)
solver = cp_model.CpSolver()
solver.Solve(model)
print(f"x1 = {solver.value(x1)}, x2 = {solver.value(x2)}, z = {solver.value(z)}")
x1 = 2, x2 = 3, z = 12
論理クイズ#
赤、青、黄、緑の4つの箱があります。このうち、いくつかにりんごが入っています(少なくとも1つ、2つ以上でもかまいません)。
どれか1つの箱を選んで、中身をもらえることになりました。次の情報から、必ずりんごが入っている箱を選んでください。
赤の箱にりんごが入っているなら、青の箱にもりんごが入っている。
緑の箱にりんごが入っていないなら、黄色の箱にも青の箱にもりんごは入っていない。
CP-SAT では、ブール変数と論理制約でそのまま書けます。
model = cp_model.CpModel()
red, blue, yellow, green = [model.new_bool_var(name) for name in ['red', 'blue', 'yellow', 'green']]
model.add_bool_or([red, blue, yellow, green]) # 少なくとも1つは入っている
model.add_implication(red, blue) # 赤 → 青
model.add_bool_and(~yellow, ~blue).only_enforce_if(~green) # 緑がない → 黄も青もない
solver = cp_model.CpSolver()
for box in [red, blue, yellow, green]:
model.add_assumption(~box) # 「この箱には入っていない」と仮定する
res = solver.solve(model)
if res == cp_model.INFEASIBLE: # 矛盾 → 必ず入っている
print(box)
model.clear_assumptions() # 仮定を消す
green
コードの説明#
new_bool_var()… 0/1 のブール変数を作るadd_bool_or([...])… 少なくとも1つが真(論理和)add_implication(a, b)…aならb(論理包含)add_bool_and(a, b).only_enforce_if(c)…cのときだけ「aかつb」~は否定only_enforce_if()は「この条件のときだけ有効」を指定する
このクイズの目的は、解を1つ見つけることではなく、どの箱が必ずりんご入りかを推理することです。そこで各箱について「入っていない」と仮定して解き、矛盾(INFEASIBLE)すれば、その箱には必ず入っていると判断します。
model.add_assumption(条件)で仮定を追加するclear_assumptions()で仮定を消す(次の箱を試す前に必ず呼ぶ)
結果は green でした。緑がないとすると、黄も青もなくなり、赤→青も使えないので、りんごが1つもなくなるからです。
まとめ#
CP-SAT は SAT の技術+線形緩和で動く制約ソルバー。OR-Tools ならこれ一択
基本の流れは
CpModel()→ 変数 →add()→solve()→value()最適化は
model.maximize(式)/model.minimize(式)CP-SAT は整数だけ。小数がある式は、両辺を整数倍して消す
ブール変数と論理制約(
add_bool_or,add_implication,only_enforce_if)で条件分岐も書けるassumptionsを使うと「この条件を足したら矛盾するか」を調べられる(推理クイズの定石)
次の章では、CP-SAT の API をもう少し詳しく見ていきます。