Para abordar este problema, usaría un marco de programación de enteros y definiría tres conjuntos de variables de decisión:
- x_ij : Una variable indicadora binaria de si construimos un puente en la ubicación del agua (i, j).
- y_ijbcn : Un indicador binario de si la ubicación del agua (i, j) es la n ^ ésima ubicación que une la isla b con la isla c.
- l_bc : Una variable indicadora binaria para saber si las islas byc están directamente vinculadas (también conocido como se puede caminar solo en los cuadrados de los puentes de bac).
Para los costos de construcción de puentes c_ij , el valor objetivo a minimizar es sum_ij c_ij * x_ij
. Necesitamos agregar las siguientes restricciones al modelo:
- Necesitamos asegurarnos de que las variables y_ijbcn sean válidas. Siempre podemos llegar a un cuadrado de agua si construimos un puente allí,
y_ijbcn <= x_ij
por lo que para cada ubicación de agua (i, j). Además, y_ijbc1
debe ser igual a 0 si (i, j) no limita con la isla b. Finalmente, para n> 1, y_ijbcn
solo se puede usar si se usó una ubicación de agua vecina en el paso n-1. Definiendo N(i, j)
como los cuadrados de agua vecinos (i, j), esto es equivalente a y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1)
.
- Necesitamos asegurarnos de que las variables l_bc solo se establezcan si byc están vinculados. Si definimos
I(c)
las ubicaciones que bordean la isla c, esto se puede lograr con l_bc <= sum_{(i, j) in I(c), n} y_ijbcn
.
- Necesitamos asegurarnos de que todas las islas estén conectadas, ya sea directa o indirectamente. Esto se puede lograr de la siguiente manera: para cada subconjunto S propio no vacío de islas, se requiere que al menos una isla en S esté vinculada a al menos una isla en el complemento de S, que llamaremos S '. En limitaciones, podemos implementar esto añadiendo una restricción para cada conjunto no vacío S de tamaño <= K / 2 (donde K es el número de islas),
sum_{b in S} sum_{c in S'} l_bc >= 1
.
Para una instancia de problema con K islas, W cuadrados de agua y una longitud de ruta máxima especificada N, este es un modelo de programación de enteros mixtos con O(K^2WN)
variables y O(K^2WN + 2^K)
restricciones. Obviamente, esto se volverá intratable a medida que el tamaño del problema aumente, pero puede resolverse para los tamaños que le interesan. Para tener una idea de la escalabilidad, la implementaré en Python usando el paquete pulp. Primero comencemos con el mapa más pequeño de 7 x 9 con 3 islas al final de la pregunta:
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 ""
Esto tarda 1,4 segundos en ejecutarse utilizando el solucionador predeterminado del paquete de pulpa (el solucionador CBC) y genera la solución correcta:
I I - - - - - I I
- - B - - - B - -
- - - B - B - - -
- - - - B - - - -
- - - - B - - - -
- - - - B - - - -
- - - I I I - - -
A continuación, considere el problema completo en la parte superior de la pregunta, que es una cuadrícula de 13 x 14 con 7 islas:
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
Los solucionadores de MIP a menudo obtienen buenas soluciones con relativa rapidez y luego pasan mucho tiempo tratando de demostrar la optimización de la solución. Usando el mismo código de resolución que el anterior, el programa no se completa en 30 minutos. Sin embargo, puede proporcionar un tiempo de espera al solucionador para obtener una solución aproximada:
mod.solve(pulp.solvers.PULP_CBC_CMD(maxSeconds=120))
Esto produce una solución con valor objetivo 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
Para mejorar la calidad de las soluciones que obtiene, puede utilizar un solucionador de MIP comercial (esto es gratis si se encuentra en una institución académica y probablemente no lo sea de otra manera). Por ejemplo, aquí está el rendimiento de Gurobi 6.0.4, nuevamente con un límite de tiempo de 2 minutos (aunque en el registro de la solución leemos que el solucionador encontró la mejor solución actual en 7 segundos):
mod.solve(pulp.solvers.GUROBI(timeLimit=120))
¡Esto realmente encuentra una solución de valor objetivo 16, una mejor que la que el OP pudo encontrar 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