¿Cómo puedo encontrar el camino más corto entre 100 objetivos móviles? (Demo en vivo incluida).

89

Antecedentes

Esta imagen ilustra el problema: cuadrícula_cuadrada_con_flechas_direcciones_acción

Puedo controlar el círculo rojo. Los objetivos son los triángulos azules. Las flechas negras indican la dirección en la que se moverán los objetivos.

Quiero recopilar todos los objetivos en el número mínimo de pasos.

Cada turno debo moverme 1 paso hacia la izquierda / derecha / arriba o abajo.

Cada turno, los objetivos también se moverán 1 paso de acuerdo con las instrucciones que se muestran en el tablero.

Manifestación

He puesto una demostración reproducible del problema aquí en Google appengine .

Me interesaría mucho si alguien puede superar la puntuación objetivo, ya que esto mostraría que mi algoritmo actual no es óptimo. (¡Debería imprimirse un mensaje de felicitación si logra esto!)

Problema

Mi algoritmo actual escala realmente mal con el número de objetivos. El tiempo aumenta exponencialmente y para 16 peces ya son varios segundos.

Me gustaría calcular la respuesta para tamaños de placa de 32 * 32 y con 100 objetivos móviles.

Pregunta

¿Cuál es un algoritmo eficiente (idealmente en Javascript) para calcular el número mínimo de pasos para recopilar todos los objetivos?

Lo que he probado

Mi enfoque actual se basa en la memorización, pero es muy lento y no sé si siempre generará la mejor solución.

Resuelvo el subproblema de "¿cuál es el número mínimo de pasos para recopilar un conjunto dado de objetivos y terminar en un objetivo en particular?".

El subproblema se resuelve de forma recursiva examinando cada opción para el objetivo anterior que ha visitado. Supongo que siempre es óptimo recopilar el subconjunto anterior de objetivos lo más rápido posible y luego pasar de la posición en la que terminó al objetivo actual lo más rápido posible (aunque no sé si esta es una suposición válida).

Esto da como resultado que se computen n * 2 ^ n estados que crecen muy rápidamente.

El código actual se muestra a continuación:

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

Lo que he considerado

Algunas opciones sobre las que me he preguntado son:

  1. Almacenamiento en caché de resultados intermedios. El cálculo de la distancia repite mucha simulación y los resultados intermedios pueden almacenarse en caché.
    Sin embargo, no creo que esto impida que tenga una complejidad exponencial.

  2. Un algoritmo de búsqueda A *, aunque no me queda claro cuál sería una heurística admisible apropiada y qué tan efectivo sería en la práctica.

  3. Investigar buenos algoritmos para el problema del viajante y ver si se aplican a este problema.

  4. Tratar de demostrar que el problema es NP-difícil y, por lo tanto, no es razonable buscar una respuesta óptima.

Peter de Rivaz
fuente
1
Yo iría por el n. ° 4 y, posteriormente, por el n. ° 3: con placas lo suficientemente grandes, imita bastante bien al TSP.
John Dvorak
2
Hasta donde yo sé, TSP es NP-hard con la métrica euclidiana y también con la métrica de Manhattan (cuadrícula cuadrada).
John Dvorak
1
Si lo hace mediante una simple búsqueda de árbol, sí, será exponencial. Sin embargo, si puede encontrar una heurística decente en cada paso, puede que no sea realmente óptima, pero podría ser muy buena. Una posible heurística sería, al observar el conjunto actual de peces, ¿cuál podría alcanzarse más rápidamente? Una heurística secundaria podría ser, ¿a qué 2 peces podría llegar más rápido?
Mike Dunlavey
2
@MikeDunlavey que correspondería al codicioso algoritmo TSP, y funciona muy bien en la práctica. Ir por el pez más cercano parece una buena idea
John Dvorak
1
+1 para una de las mejores preguntas que he visto últimamente, tanto por contenido como por estructura.
surfitscrollit

Respuestas:

24

¿Ha buscado la literatura? Encontré estos papeles que parecen analizar su problema:

ACTUALIZACIÓN 1:

Los dos artículos anteriores parecen concentrarse en el movimiento lineal de la métrica euclidiana.

uldall
fuente
Gracias, no había visto esos papeles, pero parecen muy relevantes. Veré si puedo adaptar el algoritmo genético para que funcione en mi caso y compararlo con los resultados del enfoque de fuerza bruta.
Peter de Rivaz
13

Método codicioso

Un enfoque sugerido en los comentarios es ir primero al objetivo más cercano.

He puesto una versión de la demostración que incluye el costo calculado a través de este método codicioso aquí .

El codigo es:

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

Para 10 objetivos, es aproximadamente el doble de la distancia óptima, pero a veces es mucho más (por ejemplo, * 4) y, en ocasiones, incluso alcanza el óptimo.

Este enfoque es muy eficiente, por lo que puedo permitirme algunos ciclos para mejorar la respuesta.

A continuación, estoy considerando usar métodos de colonias de hormigas para ver si pueden explorar el espacio de la solución de manera efectiva.

Método de colonia de hormigas

Un método de colonia de hormigas parece funcionar notablemente bien para este problema. El enlace en esta respuesta ahora compara los resultados cuando se usa el método de colonia de hormigas y codiciosos.

La idea es que las hormigas elijan su ruta de manera probabilística en función del nivel actual de feromonas. Después de cada 10 pruebas, depositamos feromona adicional a lo largo del camino más corto que encontraron.

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

Resultados

Este método de colonias de hormigas que utiliza 100 repeticiones de 10 hormigas sigue siendo muy rápido (37 ms para 16 objetivos en comparación con 3700 ms para la búsqueda exhaustiva) y parece muy preciso.

La siguiente tabla muestra los resultados de 10 ensayos con 16 objetivos:

   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

El método de las hormigas parece significativamente mejor que el codicioso y, a menudo, muy cerca del óptimo.

Peter de Rivaz
fuente
Agradable. Es posible que todavía no obtenga resultados óptimos de la búsqueda exhaustiva (¡o posiblemente nunca debido a su intratabilidad!), Pero sería interesante ver cómo la colonia de hormigas escala con el tamaño del tablero (32x32) con el mismo número de objetivos.
timxyz
8

El problema puede representarse en términos del problema del vendedor ambulante generalizado y luego convertirse en un problema del vendedor ambulante convencional. Este es un problema bien estudiado. Es posible que las soluciones más eficientes al problema del OP no sean más eficientes que las soluciones al TSP, pero de ninguna manera seguro (probablemente no estoy aprovechando algunos aspectos de la estructura del problema del OP que permitirían una solución más rápida , como su naturaleza cíclica). De cualquier manera, es un buen punto de partida.

De C. Noon & J. Bean, Una transformación eficiente del problema generalizado del viajante de comercio :

El problema del vendedor ambulante generalizado (GTSP) es un modelo útil para problemas que involucran decisiones de selección y secuencia. La versión asimétrica del problema se define en un gráfico dirigido con nodos N, conectando los arcos A y un vector de los correspondientes costos de arco c. Los nodos están agrupados previamente en m conjuntos de nodos exhaustivos y mutuamente excluyentes. Los arcos de conexión se definen solo entre nodos que pertenecen a diferentes conjuntos, es decir, no hay arcos dentro del conjunto. Cada arco definido tiene un costo no negativo correspondiente. El GTSP se puede plantear como el problema de encontrar un ciclo de arco m de costo mínimo que incluya exactamente un nodo de cada conjunto de nodos .

Para el problema del OP:

  • Cada miembro de Nes la ubicación de un pez en particular en un momento determinado. Represente esto como (x, y, t), donde (x, y)es una coordenada de la cuadrícula y tes el momento en que el pez estará en esta coordenada. Para el pez más a la izquierda en el ejemplo del OP, los primeros (basados ​​en 1) son: (3, 9, 1), (4, 9, 2), (5, 9, 3)cuando el pez se mueve hacia la derecha.
  • Para cualquier miembro de N, fish(n_i)devuelva el ID del pez representado por el nodo. Para dos miembros cualesquiera de N podemos calcular manhattan(n_i, n_j)la distancia de Manhattan entre los dos nodos, y time(n_i, n_j) el desplazamiento de tiempo entre los nodos.
  • El número de subconjuntos disjuntos m es igual al número de peces. El subconjunto disjunto S_iconstará solo de los nodos para los que fish(n) == i.
  • Si para dos nodos iy j fish(n_i) != fish(n_j)luego hay un arco entre iyj .
  • El costo entre el nodo i y el nodo j es time(n_i, n_j), o indefinido si time(n_i, n_j) < distance(n_i, n_j)(es decir, no se puede llegar a la ubicación antes de que llegue el pez, quizás porque está al revés en el tiempo). Los arcos de este último tipo pueden eliminarse.
  • Será necesario agregar un nodo adicional que represente la ubicación del jugador con arcos y costos para todos los demás nodos.

La solución de este problema resultaría en una única visita a cada subconjunto de nodos (es decir, cada pez se obtiene una vez) para una ruta con un costo mínimo (es decir, un tiempo mínimo para obtener todos los peces).

El artículo continúa describiendo cómo la formulación anterior puede transformarse en un problema tradicional de vendedor ambulante y posteriormente resolverse o aproximarse con técnicas existentes. No he leído los detalles, pero otro documento que hace esto de una manera que proclama ser eficiente es este .

Hay problemas obvios con la complejidad. En particular, ¡el espacio nodal es infinito! Esto se puede aliviar generando solo nodos hasta un cierto horizonte de tiempo. Si tes el número de pasos de tiempo para generar nodos y fes el número de peces, entonces el tamaño del espacio del nodo será t * f. Un nodo en un momento jtendrá como máximo (f - 1) * (t - j)arcos salientes (ya que no puede retroceder en el tiempo ni a su propio subconjunto). El número total de arcos será del orden det^2 * f^2 arcos. Es probable que la estructura del arco se pueda arreglar, para aprovechar el hecho de que las rutas de los peces son eventualmente cíclicas. Los peces repetirán su configuración una vez cada mínimo común denominador de la duración de sus ciclos, por lo que tal vez se pueda utilizar este hecho.

No sé lo suficiente sobre el TSP para decir si esto es factible o no, y no creo que signifique que el problema publicado sea necesariamente NP-hard ... pero es un enfoque para encontrar una solución óptima o limitada .

timxyz
fuente
Gracias, esto es nuevo para mí y muy interesante. Creo que debería poder usar esta transformación en combinación con el algoritmo de Christofides para encontrar de manera eficiente una solución dentro de un factor de aproximación de 3/2 del óptimo. Si consigo que funcione, agregaré las rutas producidas a la página de demostración.
Peter de Rivaz
Ah, creo que hay un problema con mi plan porque, aunque mi problema original es un gráfico completo que satisface una desigualdad apropiada en la métrica, la transformación descrita da como resultado un gráfico incompleto y, por lo tanto, el algoritmo de Christofides ya no se aplica. Gracias de todos modos por la interesante perspectiva.
Peter de Rivaz
Sí, olvidé mencionar que la desigualdad del triángulo ya no se cumple. Sin embargo, es un buen punto de partida para soluciones heurísticas y aproximaciones más generales.
timxyz
1

Creo que otro enfoque sería:

Cita wikipedia:

En matemáticas, un diagrama de Voronoi es una forma de dividir el espacio en varias regiones. Un conjunto de puntos (llamados semillas, sitios o generadores) se especifica de antemano y para cada semilla habrá una región correspondiente que consta de todos los puntos más cercanos a esa semilla que a cualquier otra.

Entonces, elige un objetivo, sigue su camino para algunos pasos y establece un punto de semilla allí. Haga esto también con todos los demás objetivos y obtendrá un diagrama de voroni. Dependiendo de la zona en la que te encuentres, te mueves al punto de partida de la misma. Viola, tienes el primer pescado. Ahora repita este paso hasta que los compre todos.

Jürgen Stürmer
fuente