Come posso trovare il percorso più breve tra 100 bersagli mobili? (Demo live inclusa.)


89

sfondo

Questa immagine illustra il problema: square_grid_with_arrows_giving_directions

Posso controllare il cerchio rosso. Gli obiettivi sono i triangoli blu. Le frecce nere indicano la direzione in cui si muoveranno i bersagli.

Voglio raccogliere tutti i target nel numero minimo di passaggi.

Ogni turno devo muovermi di 1 passo a sinistra / destra / su o giù.

Ogni turno i bersagli si muoveranno anche di 1 passo secondo le direzioni mostrate sul tabellone.

Demo

Ho messo su una demo giocabile del problema qui su appengine di Google .

Sarei molto interessato se qualcuno potesse battere il punteggio target in quanto ciò dimostrerebbe che il mio algoritmo attuale non è ottimale. (Se lo gestisci, dovrebbe essere stampato un messaggio di congratulazioni!)

Problema

Il mio attuale algoritmo scala davvero male con il numero di bersagli. Il tempo aumenta in modo esponenziale e per 16 pesci è già di diversi secondi.

Vorrei calcolare la risposta per schede di dimensioni 32 * 32 e con 100 target mobili.

Domanda

Qual è un algoritmo efficiente (idealmente in Javascript) per calcolare il numero minimo di passaggi per raccogliere tutti i target?

Quello che ho provato

Il mio attuale approccio si basa sulla memorizzazione ma è molto lento e non so se genererà sempre la soluzione migliore.

Risolvo il sottoproblema di "qual è il numero minimo di passaggi per raccogliere un determinato insieme di obiettivi e finire in un determinato obiettivo?".

Il sottoproblema viene risolto ricorsivamente esaminando ogni scelta per il target precedente da visitare. Presumo che sia sempre ottimale raccogliere il precedente sottoinsieme di bersagli il più rapidamente possibile e poi spostarsi dalla posizione in cui sei finito al bersaglio corrente il più rapidamente possibile (anche se non so se questo sia un presupposto valido).

Ciò si traduce in n * 2 ^ n stati da calcolare che crescono molto rapidamente.

Il codice corrente è mostrato di seguito:

var DX=[1,0,-1,0];
var DY=[0,1,0,-1]; 

// Return the location of the given fish at time t
function getPt(fish,t) {
  var i;
  var x=pts[fish][0];
  var y=pts[fish][1];
  for(i=0;i<t;i++) {
    var b=board[x][y];
    x+=DX[b];
    y+=DY[b];
  }
  return [x,y];
}

// Return the number of steps to track down the given fish
// Work by iterating and selecting first time when Manhattan distance matches time
function fastest_route(peng,dest) {
  var myx=peng[0];
  var myy=peng[1];
  var x=dest[0];
  var y=dest[1];
  var t=0;
  while ((Math.abs(x-myx)+Math.abs(y-myy))!=t) {
    var b=board[x][y];
    x+=DX[b];
    y+=DY[b];
    t+=1;
  }
  return t;
}

// Try to compute the shortest path to reach each fish and a certain subset of the others
// key is current fish followed by N bits of bitmask
// value is shortest time
function computeTarget(start_x,start_y) {
  cache={};
  // Compute the shortest steps to have visited all fish in bitmask
  // and with the last visit being to the fish with index equal to last
  function go(bitmask,last) {
    var i;
    var best=100000000;
    var key=(last<<num_fish)+bitmask;
    if (key in cache) {
      return cache[key];
    }
    // Consider all previous positions
    bitmask -= 1<<last;
    if (bitmask==0) {
      best = fastest_route([start_x,start_y],pts[last]);
    } else {
      for(i=0;i<pts.length;i++) {
        var bit = 1<<i;
        if (bitmask&bit) {
          var s = go(bitmask,i);   // least cost if our previous fish was i
          s+=fastest_route(getPt(i,s),getPt(last,s));
          if (s<best) best=s;
        }
      }
    }
    cache[key]=best;
    return best;
  }
  var t = 100000000;
  for(var i=0;i<pts.length;i++) {
    t = Math.min(t,go((1<<pts.length)-1,i));
  }
  return t;
}

Quello che ho considerato

Alcune opzioni che mi sono chiesto sono:

  1. Memorizzazione nella cache dei risultati intermedi. Il calcolo della distanza ripete molte simulazioni ei risultati intermedi potrebbero essere memorizzati nella cache.
    Tuttavia, non credo che ciò impedirebbe che abbia una complessità esponenziale.

  2. Un algoritmo di ricerca A * sebbene non sia chiaro quale sarebbe un'euristica ammissibile appropriata e quanto sarebbe efficace nella pratica.

  3. Investigare buoni algoritmi per il problema del venditore ambulante e vedere se si applicano a questo problema.

  4. Cercare di dimostrare che il problema è NP-difficile e quindi irragionevole cercare una risposta ottimale.


1
Vorrei scegliere il n. 4 e successivamente il n. 3: con tavole abbastanza grandi, imita abbastanza bene il TSP.
John Dvorak

2
Per quanto ne so, TSP è NP-hard con la metrica euclidea e con la metrica manhattan (griglia quadrata).
John Dvorak

1
Se lo fai con una semplice ricerca ad albero, sì, sarà esponenziale. Tuttavia, se riesci a trovare un'euristica decente in ogni passaggio, potrebbe non essere veramente ottimale, ma potrebbe essere molto buona. Una possibile euristica sarebbe, guardando l'attuale serie di pesci, quale potrebbe essere raggiunta più rapidamente? Un'euristica secondaria potrebbe essere, quali 2 pesci potrei raggiungere più rapidamente?
Mike Dunlavey

2
@MikeDunlavey che corrisponderebbe all'algoritmo greedy TSP e funziona molto bene nella pratica. Scegliere il pesce più vicino sembra una buona idea
John Dvorak

1
+1 per una delle migliori domande che ho visto ultimamente, sia per il contenuto che per la struttura.
surfitscrollit

Risposte:


24

Hai cercato nella letteratura? Ho trovato questi documenti che sembrano analizzare il tuo problema:

AGGIORNAMENTO 1:

I due articoli precedenti sembrano concentrarsi sul movimento lineare per la metrica euclidea.


Grazie - non avevo visto quei documenti ma sembrano molto rilevanti. Vedrò se posso adattare l'algoritmo genetico per funzionare nel mio caso e confrontarlo con i risultati dell'approccio della forza bruta.
Peter de Rivaz

13

Metodo goloso

Un approccio suggerito nei commenti è quello di andare prima all'obiettivo più vicino.

Ho messo su una versione della demo che include il costo calcolato tramite questo metodo avido qui .

Il codice è:

function greedyMethod(start_x,start_y) {
  var still_to_visit = (1<<pts.length)-1;
  var pt=[start_x,start_y];
  var s=0;
  while (still_to_visit) {
    var besti=-1;
    var bestc=0;
    for(i=0;i<pts.length;i++) {
      var bit = 1<<i;
      if (still_to_visit&bit) {
        c = fastest_route(pt,getPt(i,s));
        if (besti<0 || c<bestc) {
          besti = i;
          bestc = c;
        }
      }
    }
    s+=c;
    still_to_visit -= 1<<besti;
    pt=getPt(besti,s);
  }
  return s;
}

Per 10 bersagli è circa il doppio della distanza ottimale, ma a volte molto di più (es. * 4) e occasionalmente raggiunge anche quella ottimale.

Questo approccio è molto efficiente, quindi posso permettermi alcuni cicli per migliorare la risposta.

Successivamente sto considerando di utilizzare metodi di colonia di formiche per vedere se possono esplorare lo spazio della soluzione in modo efficace.

Metodo della colonia di formiche

Un metodo di colonia di formiche sembra funzionare molto bene per questo problema. Il collegamento in questa risposta ora confronta i risultati quando si utilizza sia il metodo avido che quello delle formiche.

L'idea è che le formiche scelgano il loro percorso probabilisticamente in base al livello attuale di feromone. Dopo ogni 10 prove, depositiamo feromoni aggiuntivi lungo il percorso più breve che hanno trovato.

function antMethod(start_x,start_y) {
  // First establish a baseline based on greedy
  var L = greedyMethod(start_x,start_y);
  var n = pts.length;
  var m = 10; // number of ants
  var numrepeats = 100;
  var alpha = 0.1;
  var q = 0.9;
  var t0 = 1/(n*L);

  pheromone=new Array(n+1); // entry n used for starting position
  for(i=0;i<=n;i++) {
    pheromone[i] = new Array(n);
    for(j=0;j<n;j++)
      pheromone[i][j] = t0; 
  }

  h = new Array(n);
  overallBest=10000000;
  for(repeat=0;repeat<numrepeats;repeat++) {
    for(ant=0;ant<m;ant++) {
      route = new Array(n);
      var still_to_visit = (1<<n)-1;
      var pt=[start_x,start_y];
      var s=0;
      var last=n;
      var step=0;
      while (still_to_visit) {
        var besti=-1;
        var bestc=0;
        var totalh=0;
        for(i=0;i<pts.length;i++) {
          var bit = 1<<i;
          if (still_to_visit&bit) {
            c = pheromone[last][i]/(1+fastest_route(pt,getPt(i,s)));
            h[i] = c;
            totalh += h[i];
            if (besti<0 || c>bestc) {
              besti = i;
              bestc = c;
            }
          }
        }
        if (Math.random()>0.9) {
          thresh = totalh*Math.random();
          for(i=0;i<pts.length;i++) {
            var bit = 1<<i;
            if (still_to_visit&bit) {
              thresh -= h[i];
              if (thresh<0) {
                besti=i;
                break;
              }
            }
          }
        }
        s += fastest_route(pt,getPt(besti,s));
        still_to_visit -= 1<<besti;
        pt=getPt(besti,s);
        route[step]=besti;
        step++;
        pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*t0;
        last = besti;
      }
      if (ant==0 || s<bestantscore) {
        bestroute=route;
        bestantscore = s;
      }
    }
    last = n;
    var d = 1/(1+bestantscore);
    for(i=0;i<n;i++) {
      var besti = bestroute[i];
      pheromone[last][besti] = (1-alpha) * pheromone[last][besti] + alpha*d;
      last = besti;
    }
    overallBest = Math.min(overallBest,bestantscore);
  }
  return overallBest;
}

Risultati

Questo metodo di colonia di formiche che utilizza 100 ripetizioni di 10 formiche è ancora molto veloce (37 ms per 16 bersagli rispetto a 3700 ms per la ricerca esaustiva) e sembra molto accurato.

La tabella seguente mostra i risultati di 10 prove utilizzando 16 target:

   Greedy   Ant     Optimal
   46       29      29
   91       38      37
  103       30      30
   86       29      29
   75       26      22
  182       38      36
  120       31      28
  106       38      30
   93       30      30
  129       39      38

Il metodo della formica sembra significativamente migliore di quello avido e spesso molto vicino all'ottimale.


Bello. Potresti non avere ancora risultati ottimali dalla ricerca esaustiva (o forse mai a causa della sua intrattabilità!) Ma sarebbe interessante vedere come la colonia di formiche scala con le dimensioni della tavola (32x32) con lo stesso numero di bersagli.
timxyz

8

Il problema può essere rappresentato in termini di Problema del venditore ambulante generalizzato e quindi convertito in un problema del venditore ambulante convenzionale. Questo è un problema ben studiato. È possibile che le soluzioni più efficienti al problema dell'OP non siano più efficienti delle soluzioni al TSP, ma non è affatto certo (probabilmente non riesco a trarre vantaggio da alcuni aspetti della struttura del problema dell'OP che consentirebbero una soluzione più rapida , come la sua natura ciclica). Ad ogni modo, è un buon punto di partenza.

Da C. Noon e J. Bean, An Efficient Transformation of the Generalized Travelling Salesman Problem :

Il problema del venditore ambulante generalizzato (GTSP) è un modello utile per problemi che coinvolgono decisioni di selezione e sequenza. La versione asimmetrica del problema è definita su un grafo orientato con nodi N, che connettono gli archi A e un vettore dei corrispondenti costi dell'arco c. I nodi sono raggruppati in m insiemi di nodi che si escludono a vicenda ed esaurienti. Gli archi di connessione sono definiti solo tra nodi appartenenti a insiemi diversi, ovvero non ci sono archi intraset. Ogni arco definito ha un corrispondente costo non negativo. Il GTSP può essere definito come il problema di trovare un ciclo m-arc a costo minimo che includa esattamente un nodo da ciascun nodeset .

Per il problema dell'OP:

  • Ogni membro di Nè la posizione di un particolare pesce in un determinato momento. Rappresenta questo come (x, y, t), dove (x, y)è una coordinata della griglia, ed tè l'ora in cui il pesce si troverà a questa coordinata. Per il pesce più a sinistra nell'esempio dell'OP, i primi pochi di questi (a base 1) sono: (3, 9, 1), (4, 9, 2), (5, 9, 3)quando il pesce si muove a destra.
  • Per ogni membro di N fish(n_i)restituiamo l'ID del pesce rappresentato dal nodo. Per due membri qualsiasi di N possiamo calcolare manhattan(n_i, n_j)la distanza di manhattan tra i due nodi e time(n_i, n_j) per l'offset temporale tra i nodi.
  • Il numero di sottoinsiemi disgiunti m è uguale al numero di pesci. Il sottoinsieme disgiunto S_isarà composto solo dai nodi per i quali fish(n) == i.
  • Se per due nodi ie j fish(n_i) != fish(n_j)poi c'è un arco tra ie j.
  • Il costo tra il nodo i e il nodo j è time(n_i, n_j), o indefinito se time(n_i, n_j) < distance(n_i, n_j)(cioè la posizione non può essere raggiunta prima che il pesce arrivi lì, forse perché è indietro nel tempo). Gli archi di quest'ultimo tipo possono essere rimossi.
  • Sarà necessario aggiungere un nodo aggiuntivo che rappresenta la posizione del giocatore con archi e costi a tutti gli altri nodi.

Risolvere questo problema risulterebbe quindi in una singola visita a ciascun sottoinsieme di nodi (cioè ogni pesce è ottenuto una volta) per un percorso con un costo minimo (cioè tempo minimo per ottenere tutti i pesci).

L'articolo prosegue descrivendo come la formulazione di cui sopra può essere trasformata in un tradizionale problema del venditore ambulante e successivamente risolta o approssimata con le tecniche esistenti. Non ho letto i dettagli, ma un altro documento che lo fa in un modo che dichiara di essere efficiente è questo .

Ci sono ovvi problemi con la complessità. In particolare, lo spazio dei nodi è infinito! Questo può essere alleviato generando solo nodi fino a un certo orizzonte temporale. Se tè il numero di passi temporali per cui generare nodi ed fè il numero di pesci, allora la dimensione dello spazio del nodo sarà t * f. Un nodo alla volta javrà al massimo (f - 1) * (t - j)archi in uscita (poiché non può tornare indietro nel tempo o al proprio sottoinsieme). Il numero totale di archi sarà nell'ordine degli t^2 * f^2archi. La struttura ad arco può probabilmente essere riordinata, per sfruttare il fatto che i percorsi dei pesci sono eventualmente ciclici. Il pesce ripeterà la loro configurazione una volta ogni minimo comune denominatore della loro lunghezza del ciclo, quindi forse questo fatto può essere utilizzato.

Non so abbastanza sul TSP per dire se questo sia fattibile o meno, e non penso che significhi che il problema pubblicato sia necessariamente NP-difficile ... ma è un approccio verso la ricerca di una soluzione ottimale o limitata .


Grazie, questo è nuovo per me e molto interessante. Penso che dovrei essere in grado di utilizzare questa trasformazione in combinazione con l'algoritmo di Christofides per trovare in modo efficiente una soluzione entro un fattore di approssimazione di 3/2 dell'ottimale. Se riesco a farlo funzionare aggiungerò i percorsi prodotti alla pagina demo.
Peter de Rivaz

Ah, penso che ci sia un problema con il mio piano in quanto sebbene il mio problema originale sia un grafico completo che soddisfi un'appropriata disuguaglianza sulla metrica, la trasformazione descritta risulta in un grafico incompleto e quindi l'algoritmo di Christofides non si applica più. Grazie comunque per l'interessante prospettiva.
Peter de Rivaz

Sì, ho dimenticato di dire che la disuguaglianza triangolare non è più valida. Tuttavia, è un buon punto di partenza per soluzioni euristiche e approssimazioni più generali.
timxyz

1

Penso che un altro approccio sarebbe:

Citazione wikipedia:

In matematica, un diagramma di Voronoi è un modo per dividere lo spazio in un numero di regioni. Un insieme di punti (chiamati semi, siti o generatori) è specificato in anticipo e per ogni seme ci sarà una regione corrispondente composta da tutti i punti più vicini a quel seme che a qualsiasi altro.

Quindi, scegli un obiettivo, segui il suo percorso per alcuni passaggi e imposta un punto di partenza lì. Fallo anche con tutti gli altri bersagli e otterrai un diagramma di voroni. A seconda della zona in cui ti trovi, ti sposti al seedpoint. Viola, hai preso il primo pesce. Ora ripeti questo passaggio finché non li hai colti tutti.

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.