Per affrontare questo problema, utilizzerei un framework di programmazione intera e definirei tre set di variabili decisionali:
- x_ij : una variabile indicatore binaria per stabilire se costruiamo un ponte in corrispondenza dell'acqua (i, j).
- y_ijbcn : un indicatore binario per stabilire se la posizione dell'acqua (i, j) è l'n ^ esima posizione che collega l'isola b all'isola c.
- l_bc : una variabile binaria indicante se le isole bec sono collegate direttamente (ovvero puoi camminare solo sui quadrati dei ponti da b a c).
Per i costi di costruzione di ponti c_ij , il valore obiettivo da minimizzare è sum_ij c_ij * x_ij
. Dobbiamo aggiungere i seguenti vincoli al modello:
- Dobbiamo assicurarci che le variabili y_ijbcn siano valide. Possiamo sempre raggiungere un quadrato d'acqua solo se costruiamo un ponte lì, quindi
y_ijbcn <= x_ij
per ogni posizione d'acqua (i, j). Inoltre, y_ijbc1
deve essere uguale a 0 se (i, j) non confina con l'isola b. Infine, per n> 1, y_ijbcn
può essere utilizzato solo se nel passaggio n-1 è stata utilizzata una posizione dell'acqua vicina. Definendo N(i, j)
i quadrati d'acqua vicini (i, j), questo è equivalente a y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1)
.
- Dobbiamo assicurarci che le variabili l_bc siano impostate solo se bec sono collegati. Se definiamo
I(c)
come le località confinanti con l'isola c, ciò può essere ottenuto l_bc <= sum_{(i, j) in I(c), n} y_ijbcn
.
- Dobbiamo garantire che tutte le isole siano collegate, direttamente o indirettamente. Ciò può essere ottenuto nel modo seguente: per ogni sottoinsieme proprio non vuoto S di isole, richiedi che almeno un'isola in S sia collegata ad almeno un'isola nel complemento di S, che chiameremo S '. In vincoli, possiamo implementare aggiungendo un vincolo per ogni insieme non vuoto S di dimensioni <= K / 2 (dove K è il numero di isole)
sum_{b in S} sum_{c in S'} l_bc >= 1
.
Per un'istanza del problema con K isole, W quadrati d'acqua e lunghezza del percorso massima specificata N, questo è un modello di programmazione a interi misti con O(K^2WN)
variabili e O(K^2WN + 2^K)
vincoli. Ovviamente questo diventerà intrattabile man mano che la dimensione del problema diventa grande, ma potrebbe essere risolvibile per le dimensioni che ti interessano. Per avere un'idea della scalabilità, lo implementerò in Python usando il pacchetto pulp. Iniziamo prima con la mappa 7 x 9 più piccola con 3 isole in fondo alla domanda:
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 ""
Questo richiede 1,4 secondi per essere eseguito utilizzando il risolutore predefinito dal pacchetto pulp (il risolutore CBC) e restituisce la soluzione corretta:
I I - - - - - I I
- - B - - - B - -
- - - B - B - - -
- - - - B - - - -
- - - - B - - - -
- - - - B - - - -
- - - I I I - - -
Quindi, considera il problema completo all'inizio della domanda, che è una griglia 13 x 14 con 7 isole:
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
I risolutori MIP spesso ottengono buone soluzioni in tempi relativamente brevi e quindi impiegano molto tempo a cercare di dimostrare l'ottimalità della soluzione. Utilizzando lo stesso codice del risolutore di cui sopra, il programma non viene completato entro 30 minuti. Tuttavia, puoi fornire un timeout al risolutore per ottenere una soluzione approssimativa:
mod.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=120))
Questo produce una soluzione con valore obiettivo 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
Per migliorare la qualità delle soluzioni ottenute, potresti utilizzare un risolutore MIP commerciale (è gratuito se ti trovi in un istituto accademico e probabilmente non è gratuito altrimenti). Ad esempio, ecco le prestazioni di Gurobi 6.0.4, sempre con un limite di tempo di 2 minuti (anche se dal registro della soluzione si legge che il risolutore ha trovato la soluzione migliore corrente entro 7 secondi):
mod.solve(pulp.solvers.GUROBI(timeLimit=120))
Questo trova effettivamente una soluzione di valore obiettivo 16, una migliore di quella che l'OP è stato in grado di trovare a mano!
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