Qual è il costo minimo per collegare tutte le isole?


84

C'è una griglia di dimensioni N x M . Alcune celle sono isole contrassegnate da "0" e le altre sono acqua . Ogni cella d'acqua ha un numero su di essa che indica il costo di un ponte realizzato su quella cella. Devi trovare il costo minimo per il quale tutte le isole possono essere collegate. Una cella è collegata a un'altra cella se condivide un bordo o un vertice.

Quale algoritmo può essere utilizzato per risolvere questo problema? Cosa può essere utilizzato come approccio di forza bruta se i valori di N, M sono molto piccoli, diciamo NxM <= 100?

Esempio : Nell'immagine data, le celle verdi indicano le isole, le celle blu indicano l'acqua e le celle blu chiaro indicano le celle su cui deve essere realizzato un ponte. Quindi per l'immagine seguente, la risposta sarà 17 .

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

Inizialmente ho pensato di contrassegnare tutte le isole come nodi e collegare ogni coppia di isole da un ponte più corto. Quindi il problema potrebbe essere ridotto a Minimum spanning tree, ma in questo approccio ho perso il caso in cui i bordi si sovrappongono. Ad esempio , nell'immagine seguente, la distanza più breve tra due isole qualsiasi è 7 (contrassegnata in giallo), quindi utilizzando Minimum Spanning Trees la risposta sarebbe 14 , ma la risposta dovrebbe essere 11 (contrassegnata in azzurro).

immagine2


L'approccio risolutivo che hai descritto nelle tue domande sembra essere corretto. Potresti approfondire cosa intendi per "ho perso il caso in cui i bordi si sovrappongono"?
Asad Saeeduddin

@ Asad: ho aggiunto un'immagine per spiegare il problema nell'approccio MST.
Atul Vaibhav

"collega ogni due isole con un ponte più corto": come puoi vedere, è chiaramente un approccio sbagliato.
Karoly Horvath

1
Potresti condividere il codice che stai attualmente utilizzando? Ciò renderebbe un po 'più facile trovare una risposta e ci mostrerebbe anche esattamente qual è il tuo approccio attuale.
Asad Saeeduddin

7
Questa è una variante del problema dell'albero di Steiner . Segui il link a Wikipedia per alcuni approfondimenti. In breve, la soluzione esatta forse non può essere trovata in tempo polinomiale, ma uno spanning tree minimo è un'approssimazione non così male.
Gassa

Risposte:


67

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_ijper ogni posizione d'acqua (i, j). Inoltre, y_ijbc1deve essere uguale a 0 se (i, j) non confina con l'isola b. Infine, per n> 1, y_ijbcnpuò 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 

Invece della formulazione y_ijbcn, proverei una formulazione basata sul flusso (variabile per ogni tupla costituita da una coppia di isole e un'adiacenza quadrata; vincoli di conservazione, con eccesso di 1 al sink e -1 alla sorgente; flusso totale vincolato in una piazza in base al fatto che sia stato acquistato).
David Eisenstat

1
@DavidEisenstat grazie per il suggerimento - L'ho appena provato e sfortunatamente ha risolto molto più lentamente per questi casi di problemi.
josliber

8
Questo è esattamente quello che stavo cercando quando ho iniziato la taglia. Mi stupisce come un problema così banale da descrivere possa dare un momento così difficile ai risolutori MIP. Mi chiedevo se quanto segue è vero: un percorso che collega due isole è un percorso più breve con il vincolo aggiuntivo che deve passare attraverso alcune celle (i, j). Ad esempio, le isole in alto a sinistra e al centro nella soluzione di Gurobi sono collegate con un SP che è vincolato a passare attraverso la cella (6, 5). Non sono sicuro che sia vero, ma ad un certo punto ci penserò. Grazie per la risposta!
Ioannis

@Ioann è una domanda interessante - Non sono sicuro che la tua congettura sia vera ma mi sembra abbastanza plausibile. Potresti pensare alla cella (i, j) come il punto in cui i ponti da queste isole devono andare per connettersi ulteriormente ad altre isole, e quindi, a condizione di raggiungere quel punto di coordinamento, vorresti solo costruire i ponti più economici possibili per collegare l'isola paio.
josliber

5

Un approccio a forza bruta, in pseudo-codice:

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

In C ++, questo potrebbe essere scritto come

Dopo aver effettuato una prima chiamata (presumo che tu stia trasformando le tue mappe 2d in array 1d per facilità di copia), bestCostconterrà il costo della migliore risposta e bestconterrà il modello di ponti che lo produce. Questo è, tuttavia, estremamente lento.

Ottimizzazioni:

  • Utilizzando un "limite di bridge" ed eseguendo l'algoritmo per aumentare il numero massimo di bridge, è possibile trovare risposte minime senza esplorare l'intero albero. Trovare una risposta a 1 ponte, se esistesse, sarebbe O (nm) invece di O (2 ^ nm) - un drastico miglioramento.
  • Puoi evitare la ricerca (interrompendo la ricorsione; questo è anche chiamato "potatura") una volta che hai superato bestCost, perché non ha senso continuare a guardare dopo. Se non può migliorare, non continuare a scavare.
  • La potatura di cui sopra funziona meglio se si guardano i candidati "buoni" prima di quelli "cattivi" (così com'è, le celle sono tutte guardate in ordine da sinistra a destra, dall'alto verso il basso). Una buona euristica potrebbe essere quella di considerare le celle che sono vicine a diversi componenti non collegati come prioritarie rispetto alle celle che non lo sono. Tuttavia, una volta aggiunta l'euristica, la tua ricerca inizia ad assomigliare ad A * (e hai anche bisogno di una sorta di coda di priorità).
  • I ponti duplicati e quelli che conducono al nulla devono essere evitati. Qualsiasi bridge che non disconnette la rete dell'isola se rimosso è ridondante.

Un algoritmo di ricerca generale come A * consente una ricerca molto più veloce, sebbene trovare una migliore euristica non sia un compito semplice. Per un approccio più specifico al problema, utilizzare i risultati esistenti sugli alberi di Steiner , come suggerito da @Gassa, è la strada da percorrere. Si noti, tuttavia, che il problema della costruzione di alberi di Steiner su griglie ortogonali è NP-Complete, secondo questo articolo di Garey e Johnson .

Se "abbastanza buono" è sufficiente, un algoritmo genetico può probabilmente trovare rapidamente soluzioni accettabili, a condizione che si aggiungano alcune euristiche chiave per il posizionamento preferito del ponte.


"prova tutte le 2 ^ (n * m) combinazioni" uh, 2^(13*14) ~ 6.1299822e+54iterazioni. Se supponiamo che tu possa fare un milione di iterazioni al secondo, ci vorrebbero solo ... ~ 194380460000000000000000000000000000000000` anni. Queste ottimizzazioni sono molto necessarie.
Mooing Duck

OP ha chiesto "un approccio di forza bruta se i valori di N, M sono molto piccoli, diciamo NxM <= 100". Supponendo, diciamo, che 20 bridge siano sufficienti e che l'unica ottimizzazione che usi sia quella che limita il bridge sopra, la soluzione ottimale si troverà in O (2 ^ 20), che è ben nel raggio del tuo ipotetico computer.
tucuxi

La maggior parte degli algoritmi di backtracking sono orribilmente inefficienti finché non aggiungi potatura, approfondimento iterativo e così via. Questo non vuol dire che siano inutili. Ad esempio, i motori scacchistici battono regolarmente i grandi maestri con questi algoritmi (garantito - usano ogni trucco del libro per potare in modo aggressivo)
tucuxi

3

Questo problema è una variante dell'albero di Steiner chiamato albero di Steiner pesato sui nodi , specializzato in una certa classe di grafici. In modo compatto, l'albero di Steiner ponderato per nodo, dato un grafo non orientato ponderato per nodo in cui alcuni nodi sono terminali, trova l'insieme di nodi più economico, inclusi tutti i terminali che inducono un sottografo connesso. Purtroppo, non riesco a trovare alcun risolutore in alcune ricerche superficiali.

Per formulare un programma intero, creare una variabile 0-1 per ogni nodo non terminale, quindi per tutti i sottoinsiemi di nodi non terminali la cui rimozione dal grafo iniziale disconnette due terminali, richiede che la somma delle variabili nel sottoinsieme sia a almeno 1. Questo induce troppi vincoli, quindi dovrai applicarli pigramente, usando un algoritmo efficiente per la connettività dei nodi (flusso massimo, in pratica) per rilevare un vincolo violato al massimo. Ci scusiamo per la mancanza di dettagli, ma sarà una seccatura da implementare anche se hai già familiarità con la programmazione intera.


-1

Dato che questo problema si verifica in una griglia e hai parametri ben definiti, affronterei il problema con l'eliminazione sistematica dello spazio problematico creando uno spanning tree minimo. In tal modo, per me ha senso se affronti questo problema con l'algoritmo di Prim.

Sfortunatamente, ora ti imbatti nel problema di astrarre la griglia per creare un insieme di nodi e bordi ... ergo il vero problema di questo post è come converto la mia griglia nxm in {V} e {E}?

Questo processo di conversione è, a colpo d'occhio, probabilmente NP-Difficile a causa dell'enorme numero di combinazioni possibili (supponiamo che tutti i costi delle vie navigabili siano identici). Per gestire le istanze in cui i percorsi si sovrappongono, dovresti considerare di creare un'isola virtuale.

Al termine, esegui l'algoritmo di Prim e dovresti arrivare alla soluzione ottimale.

Non credo che la programmazione dinamica possa essere eseguita efficacemente qui perché non esiste un principio osservabile di ottimalità. Se troviamo il costo minimo tra due isole, ciò non significa necessariamente che possiamo trovare il costo minimo tra quelle due e la terza isola, o un altro sottoinsieme di isole che sarà (secondo la mia definizione per trovare l'MST tramite Prim) collegato.

Se desideri che il codice (pseudo o altro) converta la tua griglia in un insieme di {V} e {E}, inviami un messaggio privato e cercherò di unire insieme un'implementazione.


Tutti i costi dell'acqua non sono identici (vedi esempi). Poiché Prim non ha la nozione di creare quei "nodi virtuali", dovresti considerare un algoritmo che lo faccia: alberi di Steiner (dove i tuoi nodi virtuali sono chiamati "punti di Steiner").
tucuxi

@tucuxi: Affermare che tutti i costi delle vie d'acqua potrebbero essere identici è necessario per l'analisi del caso peggiore perché questa è la condizione che gonfia lo spazio di ricerca al suo massimo potenziale. Ecco perché ne ho parlato. Per quanto riguarda Prim, presumo che il programmatore incaricato di implementare Prim per questo problema riconosca che Prim non crea nodi virtuali e lo gestisce a livello di implementazione. Non ho ancora visto alberi di Steiner (ancora undergrad) quindi grazie per il nuovo materiale da imparare!
karnesJ.R
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.