LoginSignup
32
27

More than 5 years have passed since last update.

図で見る分枝限定法

Last updated at Posted at 2017-10-18

分枝限定法とは

各種最適化問題の最適解を求める汎用アルゴリズムである。分枝操作限定操作から構成される。全ての解候補を体系的に列挙するもので、最適化された量の上限と下限の概算を使って、最適でない候補は「ひとまとめに」捨てられる。 ――― wikipediaより

組合せ最適化の中の混合整数最適化問題を解くソルバーにおいて、よく使われる手法です。
全ての可能性を調べるので、厳密な最適解を求められます。しかし、下記の限定操作により、効率よく計算することができます1
以下では、ナップサック問題を例に説明するので、最大化問題とします。

  • 暫定解:現在までで最良となる解。以下の例では、最初に貪欲法で求めています。
  • 分枝操作:問題を分割する操作。以下の例では、1個の(0または1をとる)バイナリ変数の値を固定して2つの問題に分けています。
  • 限定操作:分割された子問題に対し以下を考えます。
    • 上界を求める:求めた上界が暫定解値以下であれば、子問題を解いても、暫定解を更新できませんので、その子問題に対しては分枝操作はしません。
    • 下界を求める:求めた下界が暫定界値以上であれば、暫定界を下界で更新します。以下の例では、実行可能解を下界にしています。

上界を求める方法としては、線形緩和がよく使われます。

例題(ナップサック問題)で確認

荷物が6個のナップサック問題を考えて見ましょう。全て列挙すると、$2^6 = 64$通りを調べることになります。Pythonで図示してみましょう。

python
from PIL import Image, ImageDraw, ImageFont
fn = ImageFont.truetype(r'C:\Windows\Fonts\ipaexg.ttf', 16)
def func1(dr, fn, ini, pos, x, pr, lab):
    y = pos*62+10
    if pr:
        dr.line((*pr,x,y-4),'black')
    dr.rectangle((x-4,y-4,x+4,y+4),f'#{"ff"if pos==6 else "40"}4040')
    dr.text((x-4,y+6), f'{lab}', 'black', fn)
    if pos < len(ini):
        w = 3*64>>pos
        ini[pos] = 1
        func1(dr, fn, ini, pos+1, x-w, (x,y+4), '1')
        ini[pos] = 0
        func1(dr, fn, ini, pos+1, x+w, (x,y+4), '0')
        ini[pos] = -1
im = Image.new('RGB', (780,408), (255,255,255))
dr = ImageDraw.Draw(im)
func1(dr, fn, [-1]*6, 0, 390, None, ' ')
im

image.png

  • 四角が1つの問題を表し、一番上の四角が元の問題を表しています。
  • 四角の下から出ている2本の線は、分枝操作で2つの子問題に分けていることを表しています。
  • 元の問題の左下の四角は、最初の荷物を「必ず選ぶ」(=1)ように固定した問題を、右下の問題は「必ず選ばない」(=0)ように固定した問題を表しています。
  • 最下段の赤い四角は、全ての荷物が0か1に固定された問題を表しており、$2^6 = 64$個あります。

上界を求める

線形緩和して、上界を求めるknapsackを定義します。iniは、固定されている状態を表す配列です(負ならば非固定)。
また、実行可能解がない場合は、0を返します。

python
from pulp import *
def knapsack(ini):
    m = LpProblem(sense=LpMaximize) # 数理モデル
    x = [LpVariable(f'x{i}',lowBound=0,upBound=1) for i in range(6)] # 変数
    m += lpDot([22,24,26,28,29,30], x) # 目的関数
    m += lpDot([10,11,12,13,14,15], x) <= 48 # 制約条件
    for i,v in zip(ini,x):
        if i >= 0:
            m += v == i
    m.solve() # 求解
    return value(m.objective) if m.status==1 else 0

限定操作を組み込んでツリーを描く

上界と暫定解を比較し、最適解が更新できないことがわかれば、描画しないようにします。
なお、暫定解の初期値は、貪欲法102 とわかっているものとします。

python
def func2(dr, fn, ini, pos, x, pr, lab, zantei):
    r = knapsack(ini)
    if r < zantei[0]-1e-4:
        return
    y = pos*62+10
    if pr:
        dr.line((*pr,x,y-4),'black')
    dr.rectangle((x-4,y-4,x+4,y+4),f'#ff4040')
    dr.text((x-4,y+6), f'{lab}', 'black', fn)
    if pos < len(ini):
        w = 3*64>>pos
        ini[pos] = 1
        func2(dr, fn, ini, pos+1, x-w, (x,y+4), '1', zantei)
        ini[pos] = 0
        func2(dr, fn, ini, pos+1, x+w, (x,y+4), '0', zantei)
        ini[pos] = -1
    else:
        if zantei[0] < r:
            zantei[0] = r
im = Image.new('RGB', (780,408), (255,255,255))
dr = ImageDraw.Draw(im)
func2(dr, fn, [-1]*6, 0, 390, None, ' ', [102])
im

image.png

大部分の子問題を解かなくてもよいことがわかります。なお、厳密な最適解は、[1,1,0,1,1,0](最下段右側)の時の 103 であり、この時点で暫定解は 103 になります。

ツリーの右側(最初の荷物を0に固定した問題)は、全て消えています。確認してみましょう。

python
print(f'{knapsack([0,-1,-1,-1,-1,-1]):.2f}')
>>>
102.86

最初の荷物を0で固定にして、残りを-1で非固定にして求めます。
上界が 102.86 なので暫定解 103 より悪いため、限定操作で、この子問題で探索は終わりになります。

以上


  1. うまく分枝操作をすることによって、効率がよくなります。 

32
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
27