¿Cuál es el costo mínimo para conectar todas las islas?

84

Hay una rejilla de tamaño N x M . Algunas celdas son islas indicadas por '0' y las otras son agua . Cada celda de agua tiene un número que indica el costo de un puente hecho en esa celda. Tienes que encontrar el costo mínimo por el que se puedan conectar todas las islas. Una celda está conectada a otra celda si comparte un borde o un vértice.

¿Qué algoritmo se puede utilizar para resolver este problema? ¿Qué se puede utilizar como enfoque de fuerza bruta si los valores de N, M son muy pequeños, digamos NxM <= 100?

Ejemplo : en la imagen dada, las celdas verdes indican islas, las celdas azules indican agua y las celdas azul claro indican las celdas en las que se debe hacer un puente. Por lo tanto, para la siguiente imagen, la respuesta será 17 .

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

Inicialmente pensé en marcar todas las islas como nodos y conectar cada par de islas por un puente más corto. Entonces el problema podría reducirse al árbol de expansión mínimo, pero en este enfoque me perdí el caso donde los bordes se superponen. Por ejemplo , en la siguiente imagen, la distancia más corta entre dos islas cualesquiera es 7 (marcada en amarillo), por lo que al usar Árboles de expansión mínimos la respuesta sería 14 , pero la respuesta debería ser 11 (marcada en azul claro).

imagen2

Atul Vaibhav
fuente
El enfoque de solución que ha descrito en sus preguntas parece ser correcto. ¿Podría explicar lo que quiere decir con "Me perdí el caso donde los bordes se superponen"?
Asad Saeeduddin
@Asad: He agregado una imagen para explicar el problema en el enfoque MST.
Atul Vaibhav
"conecta cada dos islas por un puente más corto" - como puede ver, es claramente un mal enfoque.
Karoly Horvath
1
¿Podría compartir el código que está usando actualmente? Esto facilitaría un poco la búsqueda de una respuesta y también nos mostraría exactamente cuál es su enfoque actual.
Asad Saeeduddin
7
Esta es una variante del problema del árbol de Steiner . Siga el enlace a Wikipedia para obtener algunas ideas. En resumen, la solución exacta tal vez no se pueda encontrar en el tiempo polinomial, pero un árbol de expansión mínimo es una aproximación no tan mala.
Gassa

Respuestas:

67

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_ijpor lo que para cada ubicación de agua (i, j). Además, y_ijbc1debe ser igual a 0 si (i, j) no limita con la isla b. Finalmente, para n> 1, y_ijbcnsolo 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 
josliber
fuente
En lugar de la formulación y_ijbcn, probaría una formulación basada en el flujo (variable para cada tupla que consta de un par de islas y una adyacencia cuadrada; restricciones de conservación, con exceso de 1 en el sumidero y -1 en la fuente; flujo de entrada total limitado en un cuadrado por si se compró).
David Eisenstat
1
@DavidEisenstat, gracias por la sugerencia; acabo de probarlo y, desafortunadamente, se resolvió mucho más lentamente para estos casos de problemas.
josliber
8
Esto es exactamente lo que estaba buscando cuando comencé la recompensa. Me sorprende cómo un problema tan trivial de describir puede dificultar tanto a los solucionadores de MIP. Me preguntaba si lo siguiente es cierto: una ruta que une dos islas es la ruta más corta con la restricción adicional de que tiene que pasar por alguna celda (i, j). Por ejemplo, las islas superior izquierda y media en la solución de Gurobi están vinculadas con un SP que está obligado a pasar a través de la celda (6, 5). No estoy seguro de si esto es cierto, pero lo investigaré en algún momento. ¡Gracias por la respuesta!
Ioannis
@Ioannis pregunta interesante: no estoy seguro de si tu conjetura es cierta, pero me parece bastante plausible. Podría pensar en la celda (i, j) como donde los puentes de estas islas deben ir para conectarse más a otras islas, y luego, sujeto a alcanzar ese punto de coordinación, solo querrá construir los puentes más baratos posibles para conectar la isla. par.
josliber
5

Un enfoque de fuerza bruta, en pseudocódigo:

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

En C ++, esto podría escribirse como

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

Después de hacer una primera llamada (supongo que está transformando sus mapas 2d en matrices 1d para facilitar la copia), bestCostcontendrá el costo de la mejor respuesta y bestcontendrá el patrón de puentes que lo produce. Sin embargo, esto es extremadamente lento.

Optimizaciones:

  • Al utilizar un "límite de puentes" y ejecutar el algoritmo para aumentar el número máximo de puentes, puede encontrar respuestas mínimas sin explorar todo el árbol. Encontrar una respuesta de 1 puente, si existiera, sería O (nm) en lugar de O (2 ^ nm), una mejora drástica.
  • Puede evitar la búsqueda (deteniendo la recursividad; esto también se llama "poda") una vez que haya superado bestCost, porque no tiene sentido seguir buscando después. Si no puede mejorar, no sigas investigando.
  • La poda anterior funciona mejor si observa a los candidatos "buenos" antes de mirar a los "malos" (tal como está, las celdas se examinan todas en orden de izquierda a derecha, de arriba a abajo). Una buena heurística sería considerar las celdas que están cerca de varios componentes no conectados como de mayor prioridad que las celdas que no lo están. Sin embargo, una vez que agrega heurísticas, su búsqueda comienza a parecerse a A * (y también necesita algún tipo de cola de prioridad).
  • Deben evitarse los puentes duplicados y los puentes a ninguna parte. Cualquier puente que no desconecte la red de la isla si se quita es redundante.

Un algoritmo de búsqueda general como A * permite una búsqueda mucho más rápida, aunque encontrar una mejor heurística no es una tarea sencilla. Para un enfoque más específico del problema, usar los resultados existentes en árboles Steiner , como sugiere @Gassa, es el camino a seguir. Sin embargo, tenga en cuenta que el problema de construir árboles Steiner en cuadrículas ortogonales es NP-Complete, según este artículo de Garey y Johnson .

Si "suficientemente bueno" es suficiente, un algoritmo genético probablemente pueda encontrar soluciones aceptables rápidamente, siempre que agregue algunas heurísticas clave en cuanto a la ubicación preferida del puente.

tucuxi
fuente
"prueba las 2 ^ (n * m) combinaciones" uh, 2^(13*14) ~ 6.1299822e+54iteraciones. Si asumimos que puede hacer un millón de iteraciones por segundo, eso solo tomaría ... ~ 194380460000000000000000000000000000000000` años. Esas optimizaciones son muy necesarias.
Mooing Duck
OP se pide "un método de fuerza bruta si los valores de N, M son muy pequeñas, por ejemplo N x M <= 100". Suponiendo, digamos, 20 puentes son suficientes, y la única optimización que usa es la que limita el puente anterior, la solución óptima se encontrará en O (2 ^ 20), que está dentro del rango de su computadora hipotética.
Tucuxi
La mayoría de los algoritmos de retroceso son terriblemente ineficientes hasta que agrega poda, profundización iterativa, etc. Esto no quiere decir que sean inútiles. Por ejemplo, los motores de ajedrez derrotan rutinariamente a los grandes maestros con estos algoritmos (concedido, usan todos los trucos del libro para podar agresivamente)
tucuxi
3

Este problema es una variante del árbol de Steiner llamado árbol de Steiner ponderado por nodos , especializado en una determinada clase de gráficos. De manera compacta, el árbol Steiner ponderado por nodos, dado un gráfico no dirigido ponderado por nodos donde algunos nodos son terminales, encuentra el conjunto de nodos más barato que incluye todos los terminales que inducen un subgrafo conectado. Lamentablemente, parece que no puedo encontrar ningún solucionador en algunas búsquedas superficiales.

Para formular como un programa entero, haga una variable 0-1 para cada nodo no terminal, luego para todos los subconjuntos de nodos no terminales cuya eliminación del gráfico inicial desconecta dos terminales, requiera que la suma de las variables en el subconjunto sea en al menos 1. Esto induce demasiadas restricciones, por lo que tendrá que hacerlas cumplir de manera perezosa, utilizando un algoritmo eficiente para la conectividad del nodo (flujo máximo, básicamente) para detectar una restricción violada al máximo. Perdón por la falta de detalles, pero esto va a ser complicado de implementar incluso si ya está familiarizado con la programación de enteros.

David Eisenstat
fuente
-1

Dado que este problema tiene lugar en una cuadrícula y tiene parámetros bien definidos, abordaría el problema con la eliminación sistemática del espacio del problema creando un árbol de expansión mínimo. Al hacerlo, para mí tiene sentido si aborda este problema con el algoritmo de Prim.

Desafortunadamente, ahora se encuentra con el problema de abstraer la cuadrícula para crear un conjunto de nodos y bordes ... ergo, el problema real de esta publicación es ¿cómo puedo convertir mi cuadrícula nxm en {V} y {E}?

Este proceso de conversión es, de un vistazo, NP-Hard debido a la gran cantidad de combinaciones posibles (suponga que todos los costos de las vías navegables son idénticos). Para manejar instancias donde las rutas se superponen, debería considerar la posibilidad de crear una isla virtual.

Una vez hecho esto, ejecute el algoritmo de Prim y debería llegar a la solución óptima.

No creo que la programación dinámica se pueda ejecutar de manera efectiva aquí porque no existe un principio observable de optimización. Si encontramos el costo mínimo entre dos islas, eso no significa necesariamente que podamos encontrar el costo mínimo entre esas dos y la tercera isla, u otro subconjunto de islas que será (según mi definición, encontrar el MST a través de Prim) conectado.

Si desea que el código (pseudo o de otro tipo) convierta su cuadrícula en un conjunto de {V} y {E}, envíeme un mensaje privado y analizaré la posibilidad de unir una implementación.

karnesJ.R
fuente
Todos los costos del agua no son idénticos (ver ejemplos). Dado que Prim no tiene la noción de crear esos "nodos virtuales", debería considerar un algoritmo que sí lo haga: árboles Steiner (donde sus nodos virtuales se denominan "puntos Steiner").
Tucuxi
@tucuxi: Mencionar que todos los costos de las vías navegables podrían ser idénticos es necesario para analizar el peor de los casos porque esta es la condición que infla el espacio de búsqueda a su máximo potencial. Por eso lo mencioné. Con respecto a Prim, supongo que el programador a cargo de implementar Prim para este problema reconoce que Prim no crea nodos virtuales y lo maneja a nivel de implementación. Todavía no he visto árboles Steiner (todavía no soy licenciatura), ¡así que gracias por aprender el nuevo material!
karnesJ.R