it-swarm.com.de

Was sind die Mindestkosten, um alle Inseln zu verbinden?

Es gibt ein Raster der Größe N x M. Einige Zellen sind Inseln , die mit '0' gekennzeichnet sind, und die anderen sind Wasser . Auf jeder Wasserzelle befindet sich eine Nummer, die die Kosten einer Brücke angibt, die auf dieser Zelle hergestellt wurde. Sie müssen die Mindestkosten ermitteln, für die alle Inseln verbunden werden können. Eine Zelle ist mit einer anderen Zelle verbunden, wenn sie eine Kante oder einen Scheitelpunkt teilt.

Welcher Algorithmus kann zur Lösung dieses Problems verwendet werden?
Edit: Was kann als Brute-Force-Ansatz verwendet werden, wenn die Werte von N, M sehr klein sind, z. B. NxM <= 100?

Beispiel : In dem gegebenen Bild bezeichnen grüne Zellen Inseln, blaue Zellen Wasser und hellblaue Zellen die Zellen, auf denen eine Brücke gebaut werden soll. Für das folgende Bild lautet die Antwort also 17.

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

Anfangs dachte ich daran, alle Inseln als Knoten zu markieren und jedes Inselpaar durch eine kürzeste Brücke zu verbinden. Dann könnte das Problem auf Minimum Spanning Tree reduziert werden, aber bei diesem Ansatz habe ich den Fall übersehen, in dem sich Kanten überlappen. Zum Beispiel ist im folgenden Bild der kürzeste Abstand zwischen zwei Inseln 7 (gelb markiert), also durch unter Verwendung von Minimum Spanning Trees wäre die Antwort 14 , aber die Antwort sollte 11 (hellblau markiert) sein.

image2

81
Atul Vaibhav

Um dieses Problem zu lösen, würde ich ein ganzzahliges Programmierframework verwenden und drei Sätze von Entscheidungsvariablen definieren:

  • x_ij: Eine binäre Indikatorvariable, die angibt, ob wir am Wasserort eine Brücke bauen (i, j).
  • y_ijbcn: Ein binärer Indikator dafür, ob der Wasserort (i, j) der n-te Ort ist, der Insel b mit Insel c verbindet.
  • l_bc: Eine binäre Indikatorvariable, die angibt, ob die Inseln b und c direkt miteinander verbunden sind (auch bekannt als Brückenfelder von b nach c).

Für Brückenbaukosten c_ij ist der zu minimierende Zielwert sum_ij c_ij * x_ij. Wir müssen dem Modell die folgenden Einschränkungen hinzufügen:

  • Wir müssen sicherstellen, dass die Variablen y_ijbcn gültig sind. Wir können immer nur ein Wasserquadrat erreichen, wenn wir dort eine Brücke bauen, also y_ijbcn <= x_ij Für jeden Wasserort (i, j). Weiterhin muss y_ijbc1 Gleich 0 sein, wenn (i, j) nicht an Insel b grenzt. Schließlich kann für n> 1 y_ijbcn Nur verwendet werden, wenn in Schritt n-1 ein benachbarter Wasserort verwendet wurde. Wenn Sie N(i, j) als benachbarte Wasserquadrate (i, j) definieren, entspricht dies y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1).
  • Wir müssen sicherstellen, dass die Variablen l_bc nur gesetzt werden, wenn b und c verknüpft sind. Wenn wir I(c) als die Orte definieren, die an Insel c grenzen, kann dies mit l_bc <= sum_{(i, j) in I(c), n} y_ijbcn erreicht werden.
  • Wir müssen sicherstellen, dass alle Inseln entweder direkt oder indirekt miteinander verbunden sind. Dies kann auf folgende Weise erreicht werden: Für jede nicht leere richtige Teilmenge S von Inseln muss mindestens eine Insel in S mit mindestens einer Insel im Komplement von S verknüpft sein, das wir S 'nennen. In Einschränkungen können wir dies implementieren, indem wir für jede nicht leere Menge S der Größe <= K/2 (wobei K die Anzahl der Inseln ist) eine Einschränkung hinzufügen, sum_{b in S} sum_{c in S'} l_bc >= 1.

Für eine Probleminstanz mit K Inseln, W Wasserquadraten und der angegebenen maximalen Pfadlänge N ist dies ein gemischtes ganzzahliges Programmiermodell mit O(K^2WN) Variablen und O(K^2WN + 2^K) Einschränkungen. Offensichtlich wird dies unlösbar, wenn das Problem groß wird, aber es kann für die Größen, die Sie interessieren, lösbar sein. Um ein Gefühl für die Skalierbarkeit zu bekommen, implementiere ich sie mit dem Pulp-Paket in python. Beginnen wir zuerst mit der kleineren 7 x 9-Karte mit 3 Inseln am Ende der Frage:

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 ""

Die Ausführung mit dem Standardlöser aus dem Zellstoffpaket (dem CBC-Löser) dauert 1,4 Sekunden und gibt die richtige Lösung aus:

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

Betrachten Sie als Nächstes das vollständige Problem oben in der Frage, das ein 13 x 14-Raster mit 7 Inseln ist:

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-Löser erhalten oft relativ schnell gute Lösungen und verbringen dann viel Zeit damit, die Optimalität der Lösung zu beweisen. Mit demselben Solver-Code wie oben wird das Programm nicht innerhalb von 30 Minuten abgeschlossen. Sie können dem Solver jedoch eine Zeitüberschreitung zuweisen, um eine ungefähre Lösung zu erhalten:

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

Dies ergibt eine Lösung mit dem Zielwert 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 

Um die Qualität der von Ihnen erhaltenen Lösungen zu verbessern, können Sie einen kommerziellen MIP-Löser verwenden (dieser ist kostenlos, wenn Sie an einer akademischen Einrichtung sind und ansonsten wahrscheinlich nicht kostenlos). Hier ist zum Beispiel die Leistung von Gurobi 6.0.4, wiederum mit einem Zeitlimit von 2 Minuten (obwohl wir aus dem Lösungsprotokoll gelesen haben, dass der Löser innerhalb von 7 Sekunden die aktuell beste Lösung gefunden hat):

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

Dies findet tatsächlich eine Lösung von objektivem Wert 16, eine, die besser ist, als das OP von Hand finden konnte!

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

Ein Brute-Force-Ansatz im Pseudocode:

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

In C++ könnte dies geschrieben werden als

// 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);
}

Nachdem Sie einen ersten Anruf getätigt haben (ich gehe davon aus, dass Sie Ihre 2d-Maps in 1d-Arrays umwandeln, um das Kopieren zu vereinfachen), enthält bestCost die Kosten der besten Antwort und best die Kosten der besten Antwort Muster von Brücken, die es ergibt. Dies ist jedoch extrem langsam.

Optimierungen:

  • Wenn Sie ein "Brückenlimit" verwenden und den Algorithmus zum Erhöhen der maximalen Anzahl von Brücken ausführen, können Sie minimale Antworten finden, ohne den gesamten Baum zu untersuchen. Eine 1-Brücken-Antwort zu finden, falls vorhanden, wäre O(nm) anstelle von O (2 ^ nm) - eine drastische Verbesserung.
  • Sie können das Suchen vermeiden (indem Sie die Rekursion stoppen; dies wird auch als "Bereinigen" bezeichnet), wenn Sie bestCost überschritten haben, da es keinen Sinn macht, danach zu suchen. Wenn es nicht besser wird, graben Sie nicht weiter.
  • Das obige Beschneiden funktioniert besser, wenn Sie sich "gute" Kandidaten ansehen, bevor Sie sich "schlechte" ansehen (so wie es ist, werden alle Zellen in der Reihenfolge von links nach rechts von oben nach unten betrachtet). Eine gute Heuristik wäre, Zellen, die sich in der Nähe mehrerer nicht verbundener Komponenten befinden, als höher priorisiert zu betrachten als Zellen, die dies nicht tun. Sobald Sie jedoch Heuristiken hinzufügen, ähnelt Ihre Suche A * (und Sie benötigen auch eine Art Prioritätswarteschlange).
  • Doppelte Brücken und Brücken ins Nirgendwo sind zu vermeiden. Jede Brücke, die das Inselnetzwerk nicht trennt, wenn sie entfernt wird, ist redundant.

Ein allgemeiner Suchalgorithmus wie A * ermöglicht eine viel schnellere Suche, obwohl das Finden einer besseren Heuristik keine einfache Aufgabe ist. Für einen problemspezifischeren Ansatz ist die Verwendung vorhandener Ergebnisse an Steiner-Bäumen , wie von @Gassa vorgeschlagen, der richtige Weg. Beachten Sie jedoch, dass das Problem des Baus von Steinerbäumen auf orthogonalen Gittern NP-vollständig ist, wie in diesem Artikel von Garey und Johnson beschrieben.

Wenn "gut genug" ausreicht, kann ein genetischer Algorithmus wahrscheinlich schnell akzeptable Lösungen finden, sofern Sie einige Schlüsselheuristiken für die bevorzugte Brückenplatzierung hinzufügen.

4
tucuxi

Dieses Problem ist eine Variante des Steinerbaums namens knotengewichteter Steinerbaum, der eine bestimmte Klasse von Graphen spezialisiert hat. Kompakt ausgedrückt, wird der knotengewichtete Steiner-Baum in einem knotengewichteten ungerichteten Graphen, in dem einige Knoten Terminals sind, nach dem billigsten Knotensatz durchsucht, einschließlich aller Terminals, die einen verbundenen Teilgraphen auslösen. Leider kann ich in einigen flüchtigen Suchen keine Löser finden.

Um ein ganzzahliges Programm zu formulieren, erstellen Sie für jeden Nicht-Terminal-Knoten eine 0-1-Variable. Für alle Teilmengen von Nicht-Terminal-Knoten, deren Entfernung aus dem Startdiagramm zwei Terminals trennt, muss die Summe der Variablen in der Teilmenge gleich sein Mindestens 1. Dies führt zu viel zu vielen Einschränkungen. Sie müssen sie daher mit einem effizienten Algorithmus für die Knotenkonnektivität (im Grunde genommen maximaler Fluss) träge erzwingen, um eine Einschränkung zu erkennen, gegen die maximal verstoßen wurde. Entschuldigen Sie den Mangel an Details, aber die Implementierung wird schwierig, selbst wenn Sie bereits mit der Programmierung von Ganzzahlen vertraut sind.

3
David Eisenstat