背景
ノートPCでオンライン会議をしようとしています。
ノートPCの位置が低いので、カメラの角度が下からになっています。
急いでノートPCの高さを高くしたいのですが、手元で使えるのがいくつかの書籍しかないです。
ノートPCは書籍より大きいので、書籍を2列に積む必要があります。また、2列の書籍の高さが違うとバランスが悪いので、2列の書籍の高さは同じくらいにしたいです。
問題
いくつかの書籍を2列に積んでなるべく高くしてください。
そのとき2列の書籍の高さの差は1ミリメートルまでにしてください。
考え方
組合せ最適化を使って解きます。
手順としては、問題を定式化して、Pythonでモデルを作成してソルバーで解きます。
「最適化におけるPython」も参考にしてください。
定式化
- 入力パラメーター
-
books
:各書籍の高さ -
limit
:2列の高さの差の上限
-
- 変数
-
obj
:2列の高さのうち低い方 -
suml
:左の列の高さ -
sumr
:右の列の高さ -
vl
:各書籍ごとに左の列に積むかどうか(0:積まない、1:積む) -
vr
:各書籍ごとに右の列に積むかどうか(0:積まない、1:積む)
-
- 目的関数:2列の高さのうち低い方を最大化
- 制約条件
-
obj
はsuml
とsumr
のうち小さい方(1) -
suml
は左の列の高さ(2) -
sumr
は右の列の高さ(3) -
suml
とsumr
の差がlimit
以下(4) - 書籍は左右のどちらかにしか置けない(5)
-
Pythonで解いてみよう
入力パラメーターは、乱数で適当に作成します。
import random
from ortoolpy import model_max, addvars, addbinvars, lpDot, value
random.seed(1)
books = [random.randint(10, 20) for _ in range(20)] # 本の厚さ(ミリメートル)
limit = 1 # 左右の高さの差の許容値(ミリメートル)
n = len(books)
m = model_max() # 数理モデル
obj, suml, sumr = addvars(3) # 低い方の高さ、左の高さ、右の高さ
vl = addbinvars(n) # 左に本を置くか
vr = addbinvars(n) # 右に本を置くか
m += obj # 目的関数(なるべく高くする)
m += obj <= suml # (1)
m += obj <= sumr # (1)
m += suml == lpDot(books, vl) # (2)
m += sumr == lpDot(books, vr) # (3)
m += suml - sumr <= limit # (4)
m += sumr - suml <= limit # (4)
for vli, vri in zip(vl, vr):
m += vli + vri <= 1 # (5)
m.solve() # ソルバーで求解
print(f'{m.status = }')
print(f'{value(suml) = }')
print(f'{value(sumr) = }')
print(f'{[int(value(vli) - value(vri)) for vli, vri in zip(vl, vr)]}')
lpDot(X, Y)
は、X
とY
の内積です。つまりlpDot(books, vl)
は、左の列の高さになります。
出力
m.status = 1
value(suml) = 149.0
value(sumr) = 148.0
[-1, -1, -1, 1, -1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, -1, -1, 1, 1, -1]
m.status = 1
は「最適解が得られている」という意味です。
value(X)
は変数X
の値を取得します。
左の列の高さは149ミリメートル、右の列の高さは148ミリメートルで、差は1ミリメートルになっています。
最後のリストは、1が左の列に置いて、-1が右の列に置くことを表しています。
余談
上記は0.1秒で計算できますが、limit
を0にすると、計算時間は24秒になります(240倍)。
このように、組合せ最適化では、ちょっとパラメーターが変わると計算時間が大きく変わることがあります。
limit
を色々変えたいときは、「ビンパッキング問題の解き方」のような工夫が必要かもしれません。