PuLP での論理和と論理積の線形化
前回の記事では補助変数を利用することで、論理包含のある一見すると非線形な制約を線形制約へ変形し、PuLP で整数制約問題を解くことができました。
現実の問題を扱う際には、「ある特定の状況が望ましくないので、この条件を満たす場合にのみペナルティを足そう」というモデリングを行いたくなることが容易に想像できます。
そこで、論理積の線形表現を PuLP で実現する抽象を導出してみました。論理和もついでにやっておきました。
二項論理和 (OR) の線形化
論理和の方が簡単なので、先にやります。考え方は真理値表をそのまま実現することです。
を実現するために、次のように制約を置きます。
と により どちらかが であれば は になります。 は がともに のときに、 を にするための制約です。
の右辺の最大値は ですが、 は か なので で止まります。
制約から許される | |||
---|---|---|---|
このように制約付けることで、上真理値表のとおり唯一の が決まり となります。
二項論理積 (AND) の線形化
OR より難しいですが、考え方は同じです。
と で偽値があるときに となるように上から抑え、そうでないときは によって にします。
制約から許される | |||
---|---|---|---|
これで となります。
定数 を決めて、目的関数に を足してやれば、「 と が両方 ならペナルティ 」を表現できます。
一般化
n 項の OR
n 項の AND
PuLP 実装サンプル
適当な最小化問題を作成してみました。 1 をとる変数を増やすとコストが減りますが、全部 1 だと大きなペナルティを食らう目的関数になっています。
import pulp as pl
prob = pl.LpProblem("DEMO", pl.LpMinimize)
# 既存の決定変数
a = pl.LpVariable("a", lowBound=0, upBound=1, cat="Binary")
b = pl.LpVariable("b", lowBound=0, upBound=1, cat="Binary")
c = pl.LpVariable("c", lowBound=0, upBound=1, cat="Binary")
# OR に条件付けするユーティリティ
def choreograph_or(prob, dest_var, *in_vars):
for v in in_vars:
prob += dest_var >= v
prob += dest_var <= pl.lpSum(in_vars)
# AND に条件付けするユーティリティ
def choreograph_and(prob, dest_var, *in_vars):
n = len(in_vars)
for v in in_vars:
prob += dest_var <= v
prob += dest_var >= pl.lpSum(in_vars) - (n - 1)
# OR(a,b,c) を表す補助変数
y = pl.LpVariable("y", lowBound=0, upBound=1, cat="Binary")
choreograph_or(prob, y, a, b, c)
# AND(a,b,c) を表す補助変数
z = pl.LpVariable("z", lowBound=0, upBound=1, cat="Binary")
choreograph_and(prob, z, a, b, c)
# 目的関数(真なる変数を増やすとコストが減るが、全部真だと大きなペナルティを食らう)
prob += 5 * z - 2 * y - a - b - c
prob.solve()
print(
f"{pl.value(a)=}",
f"{pl.value(b)=}",
f"{pl.value(c)=}",
f"{pl.value(y)=}",
f"{pl.value(z)=}",
f"{pl.LpStatus[prob.status]=}",
f"{pl.value(prob.objective)=}",
sep="\n",
)
出力
pl.value(a)=0.0
pl.value(b)=1.0
pl.value(c)=1.0
pl.value(y)=1.0
pl.value(z)=0.0
pl.LpStatus[prob.status]='Optimal'
pl.value(prob.objective)=-4.0
AND が機能し a, b, c すべてを 1 にしないよう最適化されたことが分かります。
SCIP の場合
ところで SCIP では、AND/OR/XOR 制約などをそのまま表現でき、これらの制約に対応する効率的な最適化メソッドが用意されています1。
model.addConsAnd([a, b, c], z)
しかし、目的関数を非線形にすることは SCIP でもできない2ので、補助変数にコンディションを表現させて係数をかけるというやり方は SCIP においても使います。
https://pyscipopt.readthedocs.io/en/latest/tutorials/constypes.html#constraint-types ↩︎
https://pyscipopt.readthedocs.io/en/latest/faq.html#why-can-i-not-add-a-non-linear-objective
↩︎Why can I not add a non-linear objective?
SCIP does not support non-linear objectives. However, an equivalent optimization problem can easily be constructed by introducing a single new variable and a constraint. Please see this page for a guide.