数理計画法(数理最適化)をはじめよう

# 数理計画法(数理最適化)をはじめよう
数理計画法とはなんぞやっていうのは、詳しいところは詳しい人・資料に譲ります

[日本オペレーションズリサーチ学会](https://orsj.org/)

[しっかり学ぶ数理最適化 モデルからアルゴリズムまで (KS情報科学専門書)](https://www.amazon.co.jp/%E3%81%97%E3%81%A3%E3%81%8B%E3%82%8A%E5%AD%A6%E3%81%B6%E6%95%B0%E7%90%86%E6%9C%80%E9%81%A9%E5%8C%96-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%8B%E3%82%89%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E3%81%BE%E3%81%A7-KS%E6%83%85%E5%A0%B1%E7%A7%91%E5%AD%A6%E5%B0%82%E9%96%80%E6%9B%B8-%E6%A2%85%E8%B0%B7-%E4%BF%8A%E6%B2%BB/dp/4065212707)

[組み合わせ最適化による問題解決の実践的アプローチ](https://speakerdeck.com/umepon/how-to-tackle-hard-combinatorial-optimization-problems-arising-in-real-applications)

# 数理最適化のここが良いと最近思っている
私はプログラマなので、何か問題を解こうと思ったら、長々とfor文をガリガリとコードを書いてしまうのだけど、コードを書くってやっぱり難しいし、いわゆる良いコードって曖昧だなと思う。
いわゆるフレームワークを忠実に使うことが一番最善なのだけど、何かを最適化するという意味で、フレームワークの究極系(というと雑すぎて怒られるかもしれない)のが、この数理最適化。問題を下記の数式にとにかく落とし込めば、あとはソルバーと呼ばれるものが解いてくれる。


``` math
\displaylines{
\text{min.} \quad \sum_{i=1}^{n} c_i x_i \\
\text{st.} \quad \sum_{j=1}^{n} a_{ij} x_j \leq b_i, \quad \quad i = 1, 2, \dots, m \\
x_j \geq 0, \quad  \quad j = 1, 2, \dots, n
}
```

# 実際に使ってみよう
といっても、数式とか考えたこともないって人にはなんのことやらさっぱりだと思うので、実際にプログラムとして使ってみる。まず、ソルバーを用意する。SCIPというのが無料で有名らしい(ソルバーは有料もいっぱいある、有料が主流かもしれない）、SCIPをpythonでラップした[pyscipopt](https://github.com/scipopt/PySCIPOpt)を使うと楽。
* [SCIP](https://www.scipopt.org/)
* [pyscipopt](https://github.com/scipopt/PySCIPOpt)

## 長方形箱詰め問題
複数の長方形があるときにそれを四角い箱にみっちり詰め込みたいという問題を下記の資料の定式化をもとに解く。なんとこれを実行するだけで、四角い箱に長方形をみっちり詰めてくれる。
[長方形箱詰め問題](https://speakerdeck.com/umepon/how-to-tackle-hard-combinatorial-optimization-problems-arising-in-real-applications?slide=50)、[長方形箱詰め問題とその定式化](https://speakerdeck.com/umepon/how-to-tackle-hard-combinatorial-optimization-problems-arising-in-real-applications?slide=52)


``` python
import pyscipopt
class Box:
    w: int
    h: int
    x: pyscipopt.Variable
    y: pyscipopt.Variable
    topz: list[pyscipopt.Variable]
    bottomz: list[pyscipopt.Variable]
    leftz: list[pyscipopt.Variable]
    rightz: list[pyscipopt.Variable]

    def __init__(self, w, h, x, y, topz, bottomz, leftz, rightz):
        self.w = w
        self.h = h
        self.x = x
        self.y = y
        self.topz = topz
        self.bottomz = bottomz
        self.leftz = leftz
        self.rightz = rightz

    @classmethod
    def create(cls, x, y, topz, bottomz, leftz, rightz):
        return cls(randint(1, 10), randint(1, 10), x, y, topz, bottomz, leftz, rightz)

N = 10
BIG_M = 10000

def main():
    model = pyscipopt.Model("packing")
    boxes = [
        # ランダムなサイズの長方形を作成
        Box.create(
            model.addVar(name=f"x_{idx}", vtype="C"),
            model.addVar(name=f"y_{idx}", vtype="C"),
            [model.addVar(name=f"top_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"bottom_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"left_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"right_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
        ) for idx in range(N)
    ]
    # 箱のサイズを決定
    W, H = 20, model.addVar(name="H", vtype="C")
    
    # 高さを最小化する
    model.setObjective(H, sense="minimize")
    for idx0, target_box in enumerate(boxes):
        # 箱の中におさまるようにする
        model.addCons(0.0 <= target_box.y)
        model.addCons(target_box.y + target_box.h <= H)
        model.addCons(0 <= target_box.x)
        model.addCons(target_box.x + target_box.w <= W)
        for idx1, occupied_box in enumerate(boxes):
            if idx0 != idx1:
                # 長方形同士が重ならないようにする
                model.addCons(target_box.y + target_box.h <= occupied_box.y + BIG_M * (1 - target_box.bottomz[idx1]))
                model.addCons(occupied_box.y + occupied_box.h <= target_box.h + BIG_M * (1 - target_box.topz[idx1]))
                model.addCons(target_box.x + target_box.w <= occupied_box.x + BIG_M * (1 - target_box.rightz[idx1]))
                model.addCons(occupied_box.x + occupied_box.w <= target_box.x + BIG_M * (1 - target_box.leftz[idx1]))
                model.addCons(target_box.topz[idx1] + target_box.bottomz[idx1] + target_box.leftz[idx1] + target_box.rightz[idx1] == 1)

    model.optimize()
```

## 回転も考慮にいれてみよう
上記は回転を考慮に入れていないが、考慮に入れた場合どうすればよいか。
１つの長方形を、縦横が反転した２つの長方形をスイッチすることで表現することでうまいことできる。

```python
import pyscipopt
class Box:
    w0: int
    h0: int
    w1: int
    h1: int
    x: pyscipopt.Variable
    y: pyscipopt.Variable
    topz: list[pyscipopt.Variable]
    bottomz: list[pyscipopt.Variable]
    leftz: list[pyscipopt.Variable]
    rightz: list[pyscipopt.Variable]

    def __init__(self, w, h, x, y, topz, bottomz, leftz, rightz, sw):
        self.w0, self.h0 = w, h
        self.w1, self.h1 = h, w
        self.sw = sw
        self.x = x
        self.y = y
        self.topz = topz
        self.bottomz = bottomz
        self.leftz = leftz
        self.rightz = rightz

    @classmethod
    def create(cls, x, y, topz, bottomz, leftz, rightz, sw):
        w, h = randint(1, 10), randint(1, 10)
        return cls(w, h, x, y, topz, bottomz, leftz, rightz, sw)

N = 10
BIG_M = 10000

def main():
    model = pyscipopt.Model("packing")
    boxes = [
        # ランダムなサイズの長方形を作成
        Box.create(
            model.addVar(name=f"x_{idx}", vtype="C"),
            model.addVar(name=f"y_{idx}", vtype="C"),
            [model.addVar(name=f"top_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"bottom_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"left_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            [model.addVar(name=f"right_z_{idx}_{zidx}", vtype="B") for zidx in range(N)],
            model.addVar(name=f"sw_{idx}", vtype="B"),
        ) for idx in range(N)
    ]
    # 箱のサイズを決定
    W, H = 20, model.addVar(name="H", vtype="C")
    
    # 高さを最小化する
    model.setObjective(H, sense="minimize")
    for idx0, target_box in enumerate(boxes):
        # 箱の中におさまるようにする
        # 長方形の向きによって(w0, h0)/(w1, h1)のどちらかが選択される
        model.addCons(0.0 <= target_box.y)
        model.addCons(target_box.y + target_box.h0 <= H + BIG_M * (1 - target_box.sw))
        model.addCons(target_box.y + target_box.h1 <= H + BIG_M * target_box.sw)
        model.addCons(0 <= target_box.x)
        model.addCons(target_box.x + target_box.w0 <= W + BIG_M * (1 - target_box.sw))
        model.addCons(target_box.x + target_box.w1 <= W + BIG_M * target_box.sw)
        for idx1, occupied_box in enumerate(boxes):
            if idx0 != idx1:
                # 長方形同士が重ならないようにする
                # 長方形の向きによって(w0, h0)/(w1, h1)のどちらかが選択される
                model.addCons(target_box.y + target_box.h0 <= occupied_box.y + BIG_M * (1 - target_box.bottomz[idx1]) + BIG_M * (1 - target_box.sw))
                model.addCons(target_box.y + target_box.h1 <= occupied_box.y + BIG_M * (1 - target_box.bottomz[idx1]) + BIG_M * target_box.sw)
                model.addCons(occupied_box.y + occupied_box.h0 <= target_box.y + BIG_M * (1 - target_box.topz[idx1]) + BIG_M * (1 - occupied_box.sw))
                model.addCons(occupied_box.y + occupied_box.h1 <= target_box.y + BIG_M * (1 - target_box.topz[idx1]) + BIG_M * occupied_box.sw)
                model.addCons(target_box.x + target_box.w0 <= occupied_box.x + BIG_M * (1 - target_box.rightz[idx1]) + BIG_M * (1 - target_box.sw))
                model.addCons(target_box.x + target_box.w1 <= occupied_box.x + BIG_M * (1 - target_box.rightz[idx1]) + BIG_M * target_box.sw)
                model.addCons(occupied_box.x + occupied_box.w0 <= target_box.x + BIG_M * (1 - target_box.leftz[idx1]) + BIG_M * (1 - occupied_box.sw))
                model.addCons(occupied_box.x + occupied_box.w1 <= target_box.x + BIG_M * (1 - target_box.leftz[idx1]) + BIG_M * occupied_box.sw)
                
                model.addCons(target_box.topz[idx1] + target_box.bottomz[idx1] + target_box.leftz[idx1] + target_box.rightz[idx1] == 1)

    model.optimize()
```

## BIG_Mがおもしろい
数理計画法でビッグMと呼ばれるテクニックがおもしろい。
下記の部分がそれにあたる。
`sw=1`のときは、条件AのBIG_Mが`BIG_M * (1 - 1)`で0になる一方、条件BのBIG_Mは`BIG_M * 1`で100000になる。つまり`sw=1`のとき条件Aは難しくなり、条件Bは簡単(無視できる)になる。`sw=0`のときはその逆。という仕組み

``` python
BIG_M = 100000
# 条件A
target_box.y + target_box.h0 <= H + BIG_M * (1 - target_box.sw)
# 条件B
target_box.y + target_box.h1 <= H + BIG_M * target_box.sw
```

# 実際使う上での問題
そもそも長方形箱詰め問題というのは個数が増えれば増えるほど計算がびっくりするぐらい遅くなる問題なので、数理計画法で解こうと思うと20個ぐらいでいっきに計算が辛い感じになっちゃう。
詳しい人からすると、ちゃんと適切なアルゴリズムを使いましょうと言われちゃうかもしれない。

**でも、同じフレームワークに押し込むだけで、いろいろな問題が解けるって大事なことだと思う**

なんか、うまいやり方ないかなぁ。