Um dieses Problem anzugehen, würde ich ein ganzzahliges Programmierframework verwenden und drei Sätze von Entscheidungsvariablen definieren:
- x_ij : Eine binäre Indikatorvariable dafür, ob wir am Wasserstandort 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 für die direkte Verknüpfung der Inseln b und c (auch bekannt als Sie können nur auf Brückenquadraten von b nach c gehen).
Für die Brückenbaukosten c_ij ist der zu minimierende Zielwertsum_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 einen Wasserplatz erreichen, wenn wir dort eine Brücke bauen, also
y_ijbcn <= x_ij
für jeden Wasserstandort (i, j). Ferner y_ijbc1
muss 0 gleich 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. Definieren N(i, j)
das Wasser Quadrate zu benachbarten (i, j), dann ist dies äquivalent zuy_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)
die Orte definieren , die an Insel c grenzen, kann dies mit erreicht werdenl_bc <= sum_{(i, j) in I(c), n} y_ijbcn
.
- Wir müssen sicherstellen, dass alle Inseln 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 verbunden sein, die wir S 'nennen. In Einschränkungen können wir dies implementieren, indem wir eine Einschränkung für jede nicht leere Menge S der Größe <= K / 2 hinzufügen (wobei K die Anzahl der Inseln ist)
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 die Problemgröße 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, werde ich es mit dem Pulp-Paket in Python implementieren. Beginnen wir zunächst 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, nämlich ein 13 x 14-Raster mit 7 Inseln:
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. Unter Verwendung des gleichen Solver-Codes wie oben wird das Programm nicht innerhalb von 30 Minuten abgeschlossen. Sie können dem Solver jedoch eine Zeitüberschreitung gewähren, 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 Lösungen zu verbessern, die Sie erhalten, können Sie einen kommerziellen MIP-Solver verwenden (dies 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, ebenfalls mit einem Zeitlimit von 2 Minuten (obwohl wir aus dem Lösungsprotokoll gelesen haben, dass der Löser innerhalb von 7 Sekunden die derzeit beste Lösung gefunden hat):
mod.solve(pulp.solvers.GUROBI(timeLimit=120))
Dies findet tatsächlich eine Lösung mit dem objektiven 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