web-dev-qa-db-ja.com

すべての島を接続するための最低コストはいくらですか?

サイズN x Mのグリッドがあります。一部のセルはislands '0'で示され、他のセルはwater 。各水セルには、そのセル上に作られた橋のコストを示す数字があります。すべての島を接続できる最小コストを見つける必要があります。セルがエッジまたは頂点を共有している場合、そのセルは別のセルに接続されます。

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

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

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

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

image2

81
Atul Vaibhav

この問題に取り組むには、整数プログラミングフレームワークを使用して、決定変数の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変数が有効であることを確認する必要があります。そこに橋を架けて初めて水辺に到達できるので、水位(i、j)ごとに_y_ijbcn <= x_ij_になります。さらに、(i、j)が島bに接していない場合、_y_ijbc1_は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)と同等です。
  • l_bc変数は、bとcがリンクされている場合にのみ設定されるようにする必要があります。 I(c)を島cに接する場所として定義する場合、これはl_bc <= sum_{(i, j) in I(c), n} y_ijbcnで実現できます。
  • すべての島が直接または間接的にリンクされていることを確認する必要があります。これは、次の方法で実現できます。島の空でない適切なサブセットSごとに、Sの少なくとも1つの島がSの補数の少なくとも1つの島にリンクされていることを要求します。制約では、サイズ<= K/2(Kは島の数)、_sum_{b in S} sum_{c in S'} l_bc >= 1_のすべての空でないセットSに制約を追加することでこれを実装できます。

K個の島、W個の水平方、および指定された最大パス長Nの問題インスタンスの場合、これはO(K^2WN)変数とO(K^2WN + 2^K)制約のある混合整数プログラミングモデルです。明らかに、これは問題のサイズが大きくなると扱いにくいものになりますが、関心のあるサイズでは解決できる場合があります。スケーラビリティの感覚をつかむために、パルプパッケージを使用してpythonで実装します。まず、質問の下部に3つの島がある小さな7 x 9マップから始めましょう。

_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 x 14グリッドです。

_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))
_

これにより、実際に客観的な値16の解決策が見つかります。OPが手で見つけることができたよりも1つ優れています。

_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 
_
65
josliber

擬似コードでのブルートフォースアプローチ:

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 trees の既存の結果を使用する方法があります。ただし、直交グリッド上でシュタイナーツリーを構築する問題は、NP-Completeであることに注意してください。これは Garey and Johnsonによる論文 です。

「十分」で十分であれば、優先ブリッジの配置に関するいくつかの重要なヒューリスティックを追加する限り、遺伝的アルゴリズムはおそらく許容可能なソリューションをすばやく見つけることができます。

4
tucuxi

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

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

3
David Eisenstat