Как найти кратчайший путь между 100 движущимися мишенями? (Включено демо-демо.)

Фон

На этом рисунке показана проблема: square_grid_with_arrows_giving_directions

Я могу контролировать красный круг. Цели - это синие треугольники. Черные стрелки указывают направление движения мишеней.

Я хочу собрать все цели с минимальным количеством шагов.

В каждом повороте я должен двигаться на 1 шаг либо влево/вправо/вверх или вниз.

Каждый поворот цели также перемещается на 1 шаг в соответствии с указаниями, показанными на доске.

Demo

Я разместил демонстрационную версию проблемы здесь, в приложении Google appengine.

Мне было бы очень интересно, если кто-то может победить целевой счет, так как это покажет, что мой текущий алгоритм субоптимален. (Сообщение об поздравлении должно быть напечатано, если вы справитесь с этим!)

Проблема

Мой текущий алгоритм очень сильно масштабируется с количеством целей. Время увеличивается экспоненциально, а для 16 - это уже несколько секунд.

Я хотел бы вычислить ответ для размеров досок 32 * 32 и 100 движущихся целей.

Вопрос

Что такое эффективный алгоритм (идеально в Javascript) для вычисления минимального количества шагов для сбора всех целей?

Что я пробовал

Мой текущий подход основан на memoisation, но он очень медленный, и я не знаю, будет ли он всегда генерировать лучшее решение.

Я решаю подзадачу "какое минимальное количество шагов для сбора заданного набора целей и заканчивается на определенной цели?".

Подзадача рекурсивно решается путем изучения каждого выбора для предыдущей цели, которую вы посетили. Я полагаю, что всегда оптимально собирать предыдущее подмножество целей как можно быстрее, а затем быстро перемещаться от положения, в котором вы попали до текущей цели (хотя я не знаю, является ли это допустимым предположением).

Это приводит к вычислению n * 2 ^ 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;
}

То, что я рассмотрел

Некоторые параметры, о которых я подумал, следующие:

  • Кэширование промежуточных результатов. Расчет расстояния повторяет много симуляции, и промежуточные результаты могут быть кэшированы.
    Однако я не думаю, что это остановило бы его экспоненциальную сложность.

  • Алгоритм поиска A *, хотя мне непонятно, какая подходящая допустимая эвристика будет и насколько эффективной будет это на практике.

  • Исследование хороших алгоритмов для проблемы коммивояжера и их применение к этой проблеме.

  • Попытка доказать, что проблема NP-жесткая и, следовательно, неразумно искать оптимальный ответ для нее.

Ответ 1

Вы искали литературу? Я нашел эти статьи, которые, кажется, анализируют вашу проблему:

ОБНОВЛЕНИЕ 1:

Эти две статьи, похоже, сосредоточены на линейном движении для евклидовой метрики.

Ответ 2

Жадный метод

Один из подходов, предложенный в комментариях, - это сначала перейти к ближайшей цели.

Я выставил версию демо, которая включает стоимость, рассчитанную с помощью этого жадного метода здесь.

Код:

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

Для 10 целей он примерно в два раза превышает оптимальное расстояние, но иногда намного больше (например, * 4) и иногда даже достигает оптимального.

Этот подход очень эффективен, поэтому я могу позволить себе некоторые циклы для улучшения ответа.

Далее я рассматриваю использование методов колонии ant, чтобы проверить, могут ли они эффективно исследовать пространство решения.

Ant метод колонии

Ant метод колонии, кажется, отлично работает для этой проблемы. Ссылка в этом ответе теперь сравнивает результаты при использовании как жадного, так и ant метода колонии.

Идея заключается в том, что муравьи выбирают свой маршрут вероятностно на основе текущего уровня феромона. После каждых 10 испытаний мы помещаем дополнительный феромон вдоль кратчайшего пути, который они обнаружили.

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

Результаты

Этот метод колонии ant, использующий 100 повторов 10 муравьев, все еще очень быстр (37 мс для 16 целей по сравнению с 3700 мс для исчерпывающего поиска) и кажется очень точным.

В приведенной ниже таблице показаны результаты 10 испытаний с использованием 16 целей:

   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

Метод ant кажется значительно лучше, чем жадный и часто очень близкий к оптимальному.

Ответ 3

Проблема может быть представлена ​​в терминах проблемы обобщенного перемещаемого коммивояжера, а затем преобразована в обычную проблему коммивояжера. Это хорошо изученная проблема. Возможно, что наиболее эффективные решения проблемы OP не являются более эффективными, чем решения для TSP, но отнюдь не являются определенными (я, вероятно, не могу воспользоваться некоторыми аспектами структуры проблем OP, что позволило бы более быстрое решение, например, его циклический характер). В любом случае, это хорошая отправная точка.

От C. Полдень и J.Bean, эффективная трансформация общей задачи продавца:

Обобщенная проблема коммивояжера (GTSP) - полезная модель проблем, связанных с решениями выбора и последовательности. Асимметричная версия задачи определена на ориентированном графе с узлами N, соединяющими дуги A и вектором соответствующих расходов дуги c. Узлы предварительно сгруппированы в m взаимоисключающих и исчерпывающих узлов. Соединительные дуги определяются только между узлами, принадлежащими к различным наборам, то есть нет внутрисекундных дуг. Каждая заданная дуга имеет соответствующую неотрицательную стоимость. GTSP можно сформулировать как проблему нахождения минимального стоимостного m-дугового цикла, который включает в себя ровно один node из каждого набора узлов.

Для задачи OP:

  • Каждый член N является определенным местоположением рыбы в определенное время. Представьте это как (x, y, t), где (x, y) - координата сетки, а t - время, в которое рыба будет находиться по этой координате. Для самой левой рыбы в примере OP первые несколько из них (на основе 1): (3, 9, 1), (4, 9, 2), (5, 9, 3), когда рыба движется вправо.
  • Для любого члена N пусть fish(n_i) возвращает идентификатор рыбы, представленный node. Для любых двух членов из N мы можем вычислить manhattan(n_i, n_j) для расстояния между двумя узлами на манхэттене и time(n_i, n_j) для смещения времени между узлами.
  • Число непересекающихся подмножеств m равно числу рыб. Непересекающееся подмножество S_i будет состоять только из узлов, для которых fish(n) == i.
  • Если для двух узлов i и j fish(n_i) != fish(n_j) существует дуга между i и j.
  • Стоимость между node я и node j равна time(n_i, n_j) или undefined, если time(n_i, n_j) < distance(n_i, n_j) (т.е. местоположение не может быть достигнуто до того, как рыба попадет туда, возможно, потому, что она отстает во времени). Дуги этого последнего типа могут быть удалены.
  • Необходимо добавить дополнительный node, представляющий местоположение игрока с дугами и стоимостью для всех других узлов.

Решение этой проблемы приведет к однократному посещению каждого подмножества node (т.е. каждая рыба будет получена один раз) для пути с минимальной стоимостью (т.е. минимального времени для всех рыб, которые будут получены).

Далее в документе описывается, как приведенная формулировка может быть преобразована в традиционную проблему коммивояжера, а затем решается или приближается к существующим методам. Я не читал детали, но другая статья, которая делает это так, как она заявляет, что она эффективна, этот.

Есть очевидные проблемы со сложностью. В частности, пространство node бесконечно! Это можно облегчить, только создавая узлы до определенного временного горизонта. Если t - количество временных меток для генерации узлов, а f - количество рыбы, тогда размер пространства node будет t * f. A node в момент времени j будет иметь не более (f - 1) * (t - j) исходящих дуг (поскольку он не может двигаться назад во времени или в свое собственное подмножество). Общее число дуг будет порядка t^2 * f^2 дуг. Возможно, структура дуги может быть подобрана, чтобы воспользоваться тем, что пути рыб в конечном счете цикличны. Рыба будет повторять свою конфигурацию один раз каждый самый низкий общий знаменатель их длин циклов, поэтому, возможно, этот факт можно использовать.

Я не знаю достаточно о TSP, чтобы сказать, возможно ли это или нет, и я не думаю, что это означает, что проблема, поставленная обязательно NP-hard... но это один из подходов к поиску оптимального или ограниченное решение.

Ответ 4

Я думаю, что еще один подход:

Цитата wikipedia:

В математике диаграмма Вороного - это способ разделения пространства на ряд областей. Набор точек (называемых семенами, сайтами или генераторами) указан заранее, и для каждого семени будет соответствующая область, состоящая из всех точек, расположенных ближе к этому семени, чем к любому другому.

Итак, вы выбираете цель, следуете ее пути для некоторых шагов и устанавливаете там начальную точку. Сделайте это со всеми другими целями, а также получите диаграмму ворони. В зависимости от того, в какой области вы находитесь, вы переходите к его семенной точке. Виола, у тебя есть первая рыба. Теперь повторите этот шаг, пока вы не придумали все.