web-dev-qa-db-fra.com

Quel est le coût minimum pour connecter toutes les îles?

Il y a une grille de taille N x M. Certaines cellules sont des îles notées '0' et les autres sont de l'eau . Chaque cellule d'eau a un numéro indiquant le coût d'un pont construit sur cette cellule. Vous devez trouver le coût minimum pour lequel toutes les îles peuvent être connectées. Une cellule est connectée à une autre cellule si elle partage une arête ou un sommet.

Quel algorithme peut être utilisé pour résoudre ce problème?
Edit: Que peut-on utiliser comme approche par force brute si les valeurs de N, M sont très petites, disons NxM <= 100?

Exemple : Dans l'image donnée, les cellules vertes indiquent les îles, les cellules bleues indiquent l'eau et les cellules bleu clair indiquent les cellules sur lesquelles un pont doit être fait. Ainsi pour l'image suivante, la réponse sera 17.

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

Au départ, j'ai pensé à marquer toutes les îles comme des nœuds et à connecter chaque paire d'îles par un pont le plus court. Ensuite, le problème pourrait être réduit à un arbre couvrant minimum, mais dans cette approche, j'ai raté le cas où les bords se chevauchent. Par exemple , dans l'image suivante, la distance la plus courte entre deux îles est 7 (marquée en jaune), donc par en utilisant des arbres couvrant minimum, la réponse serait 14 , mais la réponse devrait être 11 (marquée en bleu clair).

image2

81
Atul Vaibhav

Pour aborder ce problème, j'utiliserais un cadre de programmation entier et définirais trois ensembles de variables de décision:

  • x_ij: Une variable indicateur binaire pour savoir si nous construisons un pont à l'emplacement de l'eau (i, j).
  • y_ijbcn: Un indicateur binaire pour savoir si l'emplacement de l'eau (i, j) est le n ^ ième emplacement reliant l'île b à l'île c.
  • l_bc: Une variable indicatrice binaire pour savoir si les îles b et c sont directement liées (aka vous ne pouvez marcher que sur des carrés de pont de b à c).

Pour les coûts de construction de ponts c_ij, la valeur objective à minimiser est sum_ij c_ij * x_ij. Nous devons ajouter les contraintes suivantes au modèle:

  • Nous devons nous assurer que les variables y_ijbcn sont valides. Nous ne pouvons toujours atteindre un carré d'eau que si nous y construisons un pont, donc y_ijbcn <= x_ij Pour chaque point d'eau (i, j). De plus, y_ijbc1 Doit être égal à 0 si (i, j) ne borde pas l'île b. Enfin, pour n> 1, y_ijbcn Ne peut être utilisé que si un point d'eau voisin a été utilisé à l'étape n-1. Définir N(i, j) comme étant les carrés d'eau voisins (i, j), cela équivaut à y_ijbcn <= sum_{(l, m) in N(i, j)} y_lmbc(n-1).
  • Nous devons nous assurer que les variables l_bc ne sont définies que si b et c sont liés. Si nous définissons I(c) comme étant les emplacements bordant l'île c, cela peut être accompli avec l_bc <= sum_{(i, j) in I(c), n} y_ijbcn.
  • Nous devons nous assurer que toutes les îles sont reliées, directement ou indirectement. Cela peut être accompli de la manière suivante: pour chaque sous-ensemble propre non vide S d'îles, exiger qu'au moins une île dans S soit liée à au moins une île dans le complément de S, que nous appellerons S '. Dans les contraintes, nous pouvons implémenter cela en ajoutant une contrainte pour chaque ensemble S non vide de taille <= K/2 (où K est le nombre d'îlots), sum_{b in S} sum_{c in S'} l_bc >= 1.

Pour une instance de problème avec K îles, W carrés d'eau et la longueur de chemin maximale spécifiée N, il s'agit d'un modèle de programmation à nombres entiers mixtes avec des variables O(K^2WN) et O(K^2WN + 2^K). Évidemment, cela deviendra insoluble à mesure que la taille du problème devient importante, mais cela peut être résolu pour les tailles qui vous intéressent. Pour avoir une idée de l'évolutivité, je vais l'implémenter dans python en utilisant le paquet pulp. Commençons d'abord par la petite carte 7 x 9 avec 3 îles au bas de la question:

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

Cela prend 1,4 seconde pour s'exécuter à l'aide du solveur par défaut du package pulp (le solveur CBC) et génère la bonne solution:

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

Ensuite, considérez le problème complet en haut de la question, qui est une grille 13 x 14 avec 7 îles:

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

Les solveurs MIP obtiennent souvent de bonnes solutions relativement rapidement et passent ensuite énormément de temps à essayer de prouver l'optimalité de la solution. En utilisant le même code de solveur que ci-dessus, le programme ne se termine pas dans les 30 minutes. Cependant, vous pouvez fournir un délai d'expiration au solveur pour obtenir une solution approximative:

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

Cela donne une solution avec une valeur d'objectif 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 

Pour améliorer la qualité des solutions que vous obtenez, vous pouvez utiliser un solveur MIP commercial (c'est gratuit si vous êtes dans un établissement universitaire et probablement pas gratuit autrement). Par exemple, voici les performances de Gurobi 6.0.4, encore une fois avec un délai de 2 minutes (bien que d'après le journal de la solution, nous lisons que le solveur a trouvé la meilleure solution actuelle en 7 secondes):

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

Cela trouve en fait une solution de valeur objective 16, meilleure que l'OP n'a pu trouver à la main!

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

Une approche par force brute, en pseudo-code:

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++, cela pourrait être écrit comme

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

Après avoir effectué un premier appel (je suppose que vous transformez vos cartes 2d en tableaux 1d pour faciliter la copie), bestCost contiendra le coût de la meilleure réponse et best contiendra le modèle de ponts qui le donne. Ceci est cependant extrêmement lent.

Optimisations:

  • En utilisant une "limite de pont" et en exécutant l'algorithme pour augmenter le nombre maximal de ponts, vous pouvez trouver des réponses minimales sans explorer tout l'arborescence. Trouver une réponse à 1 pont, si elle existait, serait O(nm) au lieu de O (2 ^ nm) - une amélioration drastique.
  • Vous pouvez éviter de chercher (en arrêtant la récursivité; cela est aussi appelé "élagage") une fois que vous avez dépassé bestCost, car cela n'a aucun sens de continuer à chercher après. Si ça ne peut pas aller mieux, ne continuez pas à creuser.
  • L'élagage ci-dessus fonctionne mieux si vous examinez les "bons" candidats avant d'en examiner les "mauvais" (comme c'est le cas, les cellules sont toutes examinées de gauche à droite, de haut en bas). Une bonne heuristique serait de considérer les cellules qui sont proches de plusieurs composants non connectés comme prioritaires par rapport aux cellules qui ne le sont pas. Cependant, une fois que vous ajoutez des heuristiques, votre recherche commence à ressembler à A * (et vous avez également besoin d'une sorte de file d'attente prioritaire).
  • Les ponts en double et les ponts vers nulle part doivent être évités. Tout pont qui ne déconnecte pas le réseau d'îles s'il est supprimé est redondant.

Un algorithme de recherche général tel que A * permet une recherche beaucoup plus rapide, bien que trouver une meilleure heuristique ne soit pas une tâche simple. Pour une approche plus spécifique au problème, utiliser les résultats existants sur arbres de Steiner , comme suggéré par @Gassa, est la voie à suivre. Notez, cependant, que le problème de la construction d'arbres de Steiner sur des grilles orthogonales est NP-complet, selon cela article de Garey et Johnson .

Si "assez bon" suffit, un algorithme génétique peut probablement trouver rapidement des solutions acceptables, à condition d'ajouter quelques heuristiques clés quant au placement préféré du pont.

4
tucuxi

Ce problème est une variante de l'arbre de Steiner appelée arbre de Steiner pondéré par les nœuds, spécialisée dans une certaine classe de graphiques. De manière compacte, l'arborescence Steiner pondérée par les nœuds permet, compte tenu d'un graphique non orienté pondéré par les nœuds où certains nœuds sont des terminaux, de trouver l'ensemble de nœuds le moins cher, y compris tous les terminaux qui induisent un sous-graphique connecté. Malheureusement, je n'arrive pas à trouver de solveurs dans certaines recherches superficielles.

Pour formuler un programme entier, créez une variable 0-1 pour chaque nœud non terminal, puis pour tous les sous-ensembles de nœuds non terminaux dont la suppression du graphe de départ déconnecte deux terminaux, exigez que la somme des variables du sous-ensemble soit à au moins 1. Cela induit beaucoup trop de contraintes, vous devrez donc les appliquer paresseusement, en utilisant un algorithme efficace pour la connectivité des nœuds (débit maximal, essentiellement) pour détecter une contrainte violée au maximum. Désolé pour le manque de détails, mais cela va être difficile à implémenter même si vous êtes déjà familier avec la programmation entière.

3
David Eisenstat