すべての島を結ぶための最低費用はいくらですか?


84

サイズのグリッドがあるN X Mは。一部のセルは「0」で示される島であり、他のセルはです。各ウォーターセルには、そのセルに作成された橋のコストを示す番号が付いています。すべての島を接続できる最小コストを見つける必要があります。セルがエッジまたは頂点を共有している場合、そのセルは別のセルに接続されます。

この問題を解決するためにどのアルゴリズムを使用できますか?N、Mの値が非常に小さい場合、たとえばNxM <= 100の場合、ブルートフォースアプローチとして何を使用できますか?

:指定された画像で、緑色のセルは島を示し、青色のセルは水を示し、水色のセルはブリッジを作成する必要があるセルを示します。したがって、次の画像の場合、答えは17になります。

http://i.imgur.com/ClcboBy.png

最初は、すべての島をノードとしてマークし、島のすべてのペアを最短のブリッジで接続することを考えました。次に、問題を最小スパニングツリーに減らすことができますが、このアプローチでは、エッジがオーバーラップしている場合を見逃しました。たとえば、次の画像では、任意の2つの島の間の最短距離は7(黄色でマーク)であるため、最小スパニングツリーを使用すると答えは14になりますが、答えは11(水色でマーク)になります。

image2


質問で説明した解決策のアプローチは正しいようです。「エッジが重なっているケースを見逃した」とはどういう意味ですか?
Asad Saeeduddin 2015年

@Asad:MSTアプローチの問題を説明する画像を追加しました。
Atul Vaibhav 2015年

「最短の橋で2つのごとに接続する」-ご覧のとおり、これは明らかに悪いアプローチです。
Karoly Horvath 2015年

1
現在使用しているコードを教えてください。これにより、答えが少し簡単になり、現在のアプローチが正確に示されます。
Asad Saeeduddin 2015年

7
これはシュタイナー木問題の変形です。いくつかの洞察については、ウィキペディアへのリンクをたどってください。要するに、正確な解はおそらく多項式時間で見つけることができませんが、最小全域木はそれほど悪くない近似です。
ガッサ2015年

回答:


67

この問題に取り組むために、整数計画フレームワークを使用して、3セットの決定変数を定義します。

  • x_ij:水の場所(i、j)に橋を建設するかどうかを示すバイナリインジケーター変数。
  • y_ijbcn:水の場所(i、j)が島bと島cを結ぶn番目の場所であるかどうかを示すバイナリインジケーター。
  • l_bc:島bとcが直接リンクされているかどうかを示すバイナリインジケーター変数(別名、bからcまでの橋の広場でのみ歩くことができます)。

橋梁建設費c_ijの場合、最小化する目標値はsum_ij c_ij * x_ijです。モデルに次の制約を追加する必要があります。

  • y_ijbcn変数が有効であることを確認する必要があります。そこに橋を架けた場合にのみ、常にウォータースクエアに到達できるためy_ijbcn <= x_ij、すべてのウォーターロケーション(i、j)についてです。さらに、y_ijbc1(i、j)が島bに隣接していない場合は、0に等しくなければなりません。最後に、n> 1のy_ijbcn場合、ステップn-1で隣接する水の場所が使用された場合にのみ使用できます。N(i, j)(i、j)に隣接する水の正方形であると定義すると、これはと同等y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1)です。
  • bとcがリンクされている場合にのみ、l_bc変数が設定されるようにする必要があります。I(c)島cに隣接する場所として定義する場合、これはで実行できますl_bc <= sum_{(i, j) in I(c), n} y_ijbcn
  • すべての島が直接的または間接的にリンクされていることを確認する必要があります。これは、次の方法で実行できます。島の空でない適切なサブセットSごとに、Sの少なくとも1つの島がSの補数の少なくとも1つの島にリンクされている必要があります。これをS 'と呼びます。制約では、サイズ<= K / 2(Kは島の数)の空でないセットSごとに制約を追加することでこれを実装できますsum_{b in S} sum_{c in S'} l_bc >= 1

K個の島、W個の水二乗、および指定された最大パス長Nの問題インスタンスの場合、これはO(K^2WN)変数とO(K^2WN + 2^K)制約のある混合整数計画モデルです。問題のサイズが大きくなると、明らかにこれは手に負えなくなりますが、気になるサイズでは解決できる場合があります。スケーラビリティを理解するために、パルプパッケージを使用してPythonで実装します。まず、質問の下部に3つの島がある小さい7 x9のマップから始めましょう。

import itertools
import pulp
water = {(0, 2): 2.0, (0, 3): 1.0, (0, 4): 1.0, (0, 5): 1.0, (0, 6): 2.0,
         (1, 0): 2.0, (1, 1): 9.0, (1, 2): 1.0, (1, 3): 9.0, (1, 4): 9.0,
         (1, 5): 9.0, (1, 6): 1.0, (1, 7): 9.0, (1, 8): 2.0,
         (2, 0): 1.0, (2, 1): 9.0, (2, 2): 9.0, (2, 3): 1.0, (2, 4): 9.0,
         (2, 5): 1.0, (2, 6): 9.0, (2, 7): 9.0, (2, 8): 1.0,
         (3, 0): 9.0, (3, 1): 1.0, (3, 2): 9.0, (3, 3): 9.0, (3, 4): 5.0,
         (3, 5): 9.0, (3, 6): 9.0, (3, 7): 1.0, (3, 8): 9.0,
         (4, 0): 9.0, (4, 1): 9.0, (4, 2): 1.0, (4, 3): 9.0, (4, 4): 1.0,
         (4, 5): 9.0, (4, 6): 1.0, (4, 7): 9.0, (4, 8): 9.0,
         (5, 0): 9.0, (5, 1): 9.0, (5, 2): 9.0, (5, 3): 2.0, (5, 4): 1.0,
         (5, 5): 2.0, (5, 6): 9.0, (5, 7): 9.0, (5, 8): 9.0,
         (6, 0): 9.0, (6, 1): 9.0, (6, 2): 9.0, (6, 6): 9.0, (6, 7): 9.0,
         (6, 8): 9.0}
islands = {0: [(0, 0), (0, 1)], 1: [(0, 7), (0, 8)], 2: [(6, 3), (6, 4), (6, 5)]}
N = 6

# Island borders
iborders = {}
for k in islands:
    iborders[k] = {}
    for i, j in islands[k]:
        for dx in [-1, 0, 1]:
            for dy in [-1, 0, 1]:
                if (i+dx, j+dy) in water:
                    iborders[k][(i+dx, j+dy)] = True

# Create models with specified variables
x = pulp.LpVariable.dicts("x", water.keys(), lowBound=0, upBound=1, cat=pulp.LpInteger)
pairs = [(b, c) for b in islands for c in islands if b < c]
yvals = []
for i, j in water:
    for b, c in pairs:
        for n in range(N):
            yvals.append((i, j, b, c, n))

y = pulp.LpVariable.dicts("y", yvals, lowBound=0, upBound=1)
l = pulp.LpVariable.dicts("l", pairs, lowBound=0, upBound=1)
mod = pulp.LpProblem("Islands", pulp.LpMinimize)

# Objective
mod += sum([water[k] * x[k] for k in water])

# Valid y
for k in yvals:
    i, j, b, c, n = k
    mod += y[k] <= x[(i, j)]
    if n == 0 and not (i, j) in iborders[b]:
        mod += y[k] == 0
    elif n > 0:
        mod += y[k] <= sum([y[(i+dx, j+dy, b, c, n-1)] for dx in [-1, 0, 1] for dy in [-1, 0, 1] if (i+dx, j+dy) in water])

# Valid l
for b, c in pairs:
    mod += l[(b, c)] <= sum([y[(i, j, B, C, n)] for i, j, B, C, n in yvals if (i, j) in iborders[c] and B==b and C==c])

# All islands connected (directly or indirectly)
ikeys = islands.keys()
for size in range(1, len(ikeys)/2+1):
    for S in itertools.combinations(ikeys, size):
        thisSubset = {m: True for m in S}
        Sprime = [m for m in ikeys if not m in thisSubset]
        mod += sum([l[(min(b, c), max(b, c))] for b in S for c in Sprime]) >= 1

# Solve and output
mod.solve()
for row in range(min([m[0] for m in water]), max([m[0] for m in water])+1):
    for col in range(min([m[1] for m in water]), max([m[1] for m in water])+1):
        if (row, col) in water:
            if x[(row, col)].value() > 0.999:
                print "B",
            else:
                print "-",
        else:
            print "I",
    print ""

これは、パルプパッケージのデフォルトソルバー(CBCソルバー)を使用して実行するのに1.4秒かかり、正しいソリューションを出力します。

I I - - - - - I I 
- - B - - - B - - 
- - - B - B - - - 
- - - - B - - - - 
- - - - B - - - - 
- - - - B - - - - 
- - - I I I - - - 

次に、質問の上部にある完全な問題について考えます。これは、7つの島を持つ13 x14のグリッドです。

water = {(i, j): 1.0 for i in range(13) for j in range(14)}
islands = {0: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)],
           1: [(9, 0), (9, 1), (10, 0), (10, 1), (10, 2), (11, 0), (11, 1),
               (11, 2), (12, 0)],
           2: [(0, 7), (0, 8), (1, 7), (1, 8), (2, 7)],
           3: [(7, 7), (8, 6), (8, 7), (8, 8), (9, 7)],
           4: [(0, 11), (0, 12), (0, 13), (1, 12)],
           5: [(4, 10), (4, 11), (5, 10), (5, 11)],
           6: [(11, 8), (11, 9), (11, 13), (12, 8), (12, 9), (12, 10), (12, 11),
               (12, 12), (12, 13)]}
for k in islands:
    for i, j in islands[k]:
        del water[(i, j)]

for i, j in [(10, 7), (10, 8), (10, 9), (10, 10), (10, 11), (10, 12),
             (11, 7), (12, 7)]:
    water[(i, j)] = 20.0

N = 7

MIPソルバーは、多くの場合、比較的迅速に優れたソリューションを取得し、ソリューションの最適性を証明するために膨大な時間を費やします。上記と同じソルバーコードを使用すると、プログラムは30分以内に完了しません。ただし、ソルバーにタイムアウトを指定して、おおよその解を得ることができます。

mod.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=120))

これにより、客観的な値が17のソリューションが得られます。

I I - - - - - I I - - I I I 
I I - - - - - I I - - - I - 
I I - - - - - I - B - B - - 
- - B - - - B - - - B - - - 
- - - B - B - - - - I I - - 
- - - - B - - - - - I I - - 
- - - - - B - - - - - B - - 
- - - - - B - I - - - - B - 
- - - - B - I I I - - B - - 
I I - B - - - I - - - - B - 
I I I - - - - - - - - - - B 
I I I - - - - - I I - - - I 
I - - - - - - - I I I I I I 

取得するソリューションの品質を向上させるために、市販のMIPソルバーを使用できます(これは、学術機関にいる場合は無料で、それ以外の場合は無料ではない可能性があります)。たとえば、これもGurobi 6.0.4のパフォーマンスで、制限時間は2分です(ただし、ソリューションログから、ソルバーが7秒以内に現在の最良のソリューションを見つけたことがわかりました)。

mod.solve(pulp.solvers.GUROBI(timeLimit=120))

これにより、OPが手動で見つけることができたよりも1つ優れた、客観的な値16のソリューションが実際に見つかります。

I I - - - - - I I - - I I I 
I I - - - - - I I - - - I - 
I I - - - - - I - B - B - - 
- - B - - - - - - - B - - - 
- - - B - - - - - - I I - - 
- - - - B - - - - - I I - - 
- - - - - B - - B B - - - - 
- - - - - B - I - - B - - - 
- - - - B - I I I - - B - - 
I I - B - - - I - - - - B - 
I I I - - - - - - - - - - B 
I I I - - - - - I I - - - I 
I - - - - - - - I I I I I I 

y_ijbcnの定式化の代わりに、フローに基づく定式化を試してみます(アイランドペアと正方形の隣接からなる各タプルの変数、シンクで1を超え、ソースで-1を超える保存制約、フローの合計の制限それが購入されたかどうかによって正方形で)。
David Eisenstat 2015年

1
@DavidEisenstat提案に感謝します-私はそれを試してみましたが、残念ながら、これらの問題のインスタンスについてはかなりゆっくりと解決しました。
–josliber

8
これはまさに私がバウンティを始めたときに私が探していたものです。このような些細な問題がMIPソルバーにこのような困難をもたらす可能性があることに驚かされます。次のことが当てはまるかどうか疑問に思いました。2つの島を結ぶパスは最短パスであり、セル(i、j)を通過する必要があるという追加の制約があります。たとえば、Gurobiのソリューションの左上と中央の島は、セルを通過するように制約されているSPにリンクされています(6、5)。これが本当かどうかはわかりませんが、ある時点でよく調べません。答えてくれてありがとう!
ioannis 2015

@Ioannis興味深い質問-あなたの推測が正しいかどうかはわかりませんが、私にはかなりもっともらしいようです。セル(i、j)は、これらの島からの橋が他の島にさらに接続するために行く必要がある場所と考えることができ、その調整ポイントに到達することを条件として、島を接続するために可能な限り最も安い橋を構築したいだけですペア。
josliber

5

擬似コードでの強引なアプローチ:

start with a horrible "best" answer
given an nxm map,
    try all 2^(n*m) combinations of bridge/no-bridge for each cell
        if the result is connected, and better than previous best, store it

return best

C ++では、これは次のように記述できます。

// map = linearized map; map[x*n + y] is the equivalent of map2d[y][x]
// nm = n*m
// bridged = true if bridge there, false if not. Also linearized
// nBridged = depth of recursion (= current bridge being considered)
// cost = total cost of bridges in 'bridged'
// best, bestCost = best answer so far. Initialized to "horrible"
void findBestBridges(char map[], int nm,
   bool bridged[], int nBridged, int cost, bool best[], int &bestCost) {
   if (nBridged == nm) {
      if (connected(map, nm, bridged) && cost < bestCost) {
          memcpy(best, bridged, nBridged);
          bestCost = best;
      }
      return;
   }
   if (map[nBridged] != 0) {
      // try with a bridge there
      bridged[nBridged] = true;
      cost += map[nBridged];

      // see how it turns out
      findBestBridges(map, nm, bridged, nBridged+1, cost, best, bestCost);         

      // remove bridge for further recursion
      bridged[nBridged] = false;
      cost -= map[nBridged];
   }
   // and try without a bridge there
   findBestBridges(map, nm, bridged, nBridged+1, cost, best, bestCost);
}

最初の呼び出しを行った後(コピーを容易にするために2Dマップを1D配列に変換していると想定しています)、bestCostベストアンサーのコストbestが含まれ、それを生成するブリッジのパターンが含まれます。ただし、これは非常に遅いです。

最適化:

  • 「ブリッジ制限」を使用し、ブリッジの最大数を増やすアルゴリズムを実行することで、ツリー全体を探索しなくても最小限の答えを見つけることができます。1ブリッジの答えが存在する場合、それを見つけることは、O(2 ^ nm)ではなくO(nm)になります。これは大幅な改善です。
  • を超えたbestCost後は、検索を回避できます(再帰を停止することで、これは「プルーニング」とも呼ばれます)。良くならない場合は、掘り続けないでください。
  • 上記の剪定は、「悪い」候補を見る前に「良い」候補を見るとうまく機能します(つまり、セルはすべて左から右、上から下の順序で見られます)。優れたヒューリスティックは、接続されていないいくつかのコンポーネントに近いセルを、接続されていないセルよりも優先度が高いと見なすことです。ただし、ヒューリスティックを追加すると、検索はA *に似たものになり始めます(また、何らかの優先度付きキューも必要です)。
  • 重複する橋やどこへの橋も避けるべきです。取り外されてもアイランドネットワークを切断しないブリッジは冗長です。

A *などの一般的な検索アルゴリズムでは、はるかに高速な検索が可能ですが、より優れたヒューリスティックを見つけることは簡単な作業ではありません。より問題固有のアプローチについては、@ Gassaによって提案されているように、Steinerツリーで既存の結果を使用するのが最善の方法です。ただし、GareyとJohnsonによるこの論文によると、直交グリッド上にシュタイナー木を構築する問題はNP完全であることに注意してください。

「十分に良い」で十分な場合、優先するブリッジの配置に関していくつかの重要なヒューリスティックを追加する限り、遺伝的アルゴリズムはおそらく許容できる解決策をすばやく見つけることができます。


「すべての2 ^(n * m)の組み合わせを試してください」ええと、2^(13*14) ~ 6.1299822e+54繰り返し。1秒あたり100万回の反復を実行できると仮定すると、それは... 〜194380460000000000000000000000000000000000`年しかかかりません。これらの最適化は非常に必要です。
Mooing Duck 2015

OP、「N、Mの値が非常に小さい場合、たとえばNxM <= 100の場合、ブルートフォースアプローチ」を要求しました。たとえば、20個のブリッジで十分であり、使用する最適化が上記のブリッジ制限ブリッジのみであるとすると、最適なソリューションはO(2 ^ 20)にあります。これは、仮想コンピューターの範囲内です。
tucuxi 2015年

ほとんどのバックトラッキングアルゴリズムは、プルーニングや反復深化などを追加するまで、ひどく非効率的です。これは、それらが役に立たないということではありません。たとえば、チェスエンジンは、これらのアルゴリズムでグランドマスターを日常的に打ち負かします(許可されています-本のすべてのトリックを使用して積極的に剪定します)
tucuxi 2015年

3

この問題は、ノード加重シュタイナー木と呼ばれるシュタイナー木の変形であり、特定のクラスのグラフに特化しています。コンパクトに、ノード加重シュタイナー木は、一部のノードが端末であるノード加重無向グラフが与えられた場合、接続されたサブグラフを誘発するすべての端末を含む最も安価なノードのセットを見つけます。悲しいことに、いくつかの大雑把な検索でソルバーが見つからないようです。

整数計画法として定式化するには、非ターミナルノードごとに0-1変数を作成し、開始グラフから削除すると2つのターミナルが切断される非ターミナルノードのすべてのサブセットについて、サブセット内の変数の合計が次のようになる必要があります。少なくとも1。これは非常に多くの制約を引き起こすため、ノード接続の効率的なアルゴリズム(基本的には最大フロー)を使用して、最大に違反した制約を検出するために、それらを遅延的に適用する必要があります。詳細が不足していることをお詫びしますが、整数計画法に既に精通している場合でも、これを実装するのは面倒です。


-1

この問題がグリッドで発生し、パラメーターが明確に定義されている場合、最小スパニングツリーを作成して問題空間を体系的に排除することで問題に取り組みます。そうすることで、プリムのアルゴリズムでこの問題に取り組むのは私には理にかなっています。

残念ながら、グリッドを抽象化してノードとエッジのセットを作成するという問題が発生しました...この投稿の本当の問題は、nxmグリッドを{V}と{E}に変換する方法です。

この変換プロセスは、可能な組み合わせの数が非常に多いため、一見するとNP困難である可能性があります(すべての水路コストが同じであると想定します)。パスが重複するインスタンスを処理するには、仮想アイランドの作成を検討する必要があります

これが完了したら、プリムのアルゴリズムを実行すると、最適なソリューションに到達するはずです。

観察可能な最適性の原則がないため、ここで動的計画法を効果的に実行できるとは思いません。2つの島の間の最小コストを見つけた場合、それは必ずしもそれらの2つの島と3番目の島の間の最小コスト、または(Primを介してMSTを見つけるための私の定義による)別の島のサブセットを見つけることができることを意味しません。接続されています。

グリッドを{V}と{E}のセットに変換するコード(擬似またはその他)が必要な場合は、プライベートメッセージを送ってください。実装をつなぎ合わせる方法を検討します。


すべての水コストは同じではありません(例を参照)。Primにはこれらの「仮想ノード」を作成する概念がないため、次のようなアルゴリズムを検討する必要があります。シュタイナーツリー(仮想ノードは「シュタイナーポイント」と呼ばれます)。
tucuxi 2015年

@tucuxi:最悪のケースの分析には、すべての水路コスト同じである可能性があることに言及する必要があります。これは、検索スペースを最大の可能性まで膨らませる条件だからです。これが私がそれを育てた理由です。Primに関しては、この問題のPrimの実装を担当するプログラマーは、Primが仮想ノードを作成しないことを認識し、実装レベルでこれを処理すると思います。私はまだシュタイナー木を見たことがありません(まだ学部生です)ので、新しい材料を学んでくれてありがとう!
karnesJ.R 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.