如何找到100个移动目标之间的最短路径?(包括实时演示。)

90

背景

这张图片说明了问题: square_grid_with_arrows_giving_directions

我可以控制红色圆圈。目标是蓝色三角形。黑色箭头表示目标将移动的方向。

我想用最少的步骤收集所有目标。

每次转弯,我必须向左/右/上或下移动1步。

每次转弯,目标也会根据板子上显示的方向移动1步。

演示

我在Google appengine上放置了一个可玩的问题演示(链接)

如果有人能够超越目标分数,我会非常感兴趣,因为这将表明我的当前算法不够优秀。(如果您成功了,将打印祝贺信息!)

问题

我的当前算法随着目标数量的增加而变得非常糟糕。时间呈指数增长,对于16条鱼,已经需要几秒钟的时间。

我想计算出32*32的棋盘和100个移动目标的答案。

问题

有什么有效的算法(最好是JavaScript),可以计算收集所有目标所需的最小步数?

我尝试过的方法

我的当前方法基于记忆化,但速度非常慢,而且我不知道它是否总能生成最佳解。

我解决了“收集给定一组目标并以特定目标为结束点所需的最小步数”的子问题。

通过检查先前访问的目标的每个选择来递归地解决子问题。我假设尽可能快地收集先前的子集目标,然后尽可能快地从您停留的位置移动到当前目标(尽管我不知道这是否是一个有效的假设)。

这导致需要计算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;
}

我考虑过的事情

我考虑过的一些选项包括:

  1. 缓存中间结果。距离计算重复了很多模拟,可以缓存中间结果。
    但是,我认为这并不能阻止它具有指数复杂度。

  2. 使用A*搜索算法,尽管我不清楚一个适当的可接受启发式将是什么以及在实践中它会有多有效。

  3. 调查旅行商问题的好算法,并看看它们是否适用于此问题。

  4. 试图证明该问题是NP困难的,因此寻求其最优解是不合理的。


1
我会选择#4,然后是#3:对于足够大的板子,它可以很好地模拟TSP。 - John Dvorak
2
据我所知,TSP问题在欧几里得度量和曼哈顿度量(方形网格)下都是NP难问题。 - John Dvorak
2
@MikeDunlavey 这将对应于贪心TSP算法,在实践中它表现得非常好。选择最近的鱼似乎是个好主意。 - John Dvorak
1
这是我最近看到的最好的问题之一,无论是内容还是结构都非常出色。 - shauneba
1
尽快收集前面的目标子集,然后尽快从你停留的位置移动到当前目标。这样做是正确的。拖延捕获永远不会带来优势。毕竟,您始终可以“提前”捕获一个目标,然后沿着它本来要走的路径前进(这相当于拖延捕获)。 - Tom Sirgedas
显示剩余16条评论
4个回答

25

谢谢 - 我之前没有看过这些论文,但它们看起来非常相关。我会尝试将遗传算法适应到我的情况中,并将其与暴力方法的结果进行比较。 - Peter de Rivaz

13

贪心算法

评论中建议的一种方法是首先去最近的目标。

我已经发布了一个包括通过这种贪心算法计算的成本的演示版本,在这里

代码如下:

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),偶尔甚至达到最优。

这种方法非常有效,所以我可以花费一些时间来改善答案。

接下来,我考虑使用蚁群算法来看看它们是否能有效地探索解决方案空间。

蚁群算法

蚁群算法似乎非常适用于这个问题。该答案中的链接现在比较了使用贪心和蚁群算法时的结果。

想法是蚂蚁根据当前信息素水平概率选择它们的路线。 在每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;
}

结果

使用100次10只蚂蚁的此蚂蚁群算法仍然非常快(与穷举搜索相比,16个目标37ms对3700ms),而且似乎非常准确。

下表显示了使用16个目标进行10次试验的结果:

   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

蚂蚁算法似乎比贪心算法显着优秀,而且通常非常接近最优解。


不错。你可能还没有从详尽搜索中获得最佳结果(或者由于其难以处理性质,可能永远无法获得!),但看到蚂蚁群算法如何随着棋盘大小(32x32)和相同数量的目标而扩展是很有趣的。 - timxyz

8
问题可以用广义旅行商问题的术语来表示,然后转换为传统的旅行商问题。这是一个经过深入研究的问题。可能最有效的解决方案与TSP的解决方案一样有效,但并不确定(我可能未能利用OP问题结构的某些方面以实现更快的解决方案,例如其循环性质)。无论如何,这是一个很好的起点。
来自C. Noon和J.Bean的《广义旅行推销员问题的高效转化》:
广义旅行商问题(GTSP)是涉及选择和顺序决策的问题的有用模型。问题的非对称版本在具有节点N的有向图上定义,连接弧A和相应弧成本c的向量。节点被预先分成m个互斥且彼此穷尽的节点集。仅定义介于属于不同节点集的节点之间的连接弧,即不存在内部集合弧。每个定义的弧都有相应的非负成本。GTSP可以陈述为找到包括每个节点集中恰好一个节点的最小成本m-弧循环的问题。
对于OP的问题:
  • 每个 N 的成员都是特定鱼在特定时间的位置。将其表示为 (x,y,t),其中(x,y)是网格坐标,t是鱼到达该坐标的时间。对于 OP 示例中最左边的鱼,这些(基于1)是:鱼向右移动时,(3,9,1),(4,9,2),(5,9,3)
  • 对于 N 中的任何成员,让 fish(n_i) 返回节点表示的鱼的ID。对于 N 中的任意两个成员,我们可以计算出这两个节点之间的曼哈顿距离 manhattan(n_i, n_j) 和时间偏移 time(n_i, n_j)
  • 不相交子集数 m 等于鱼的数量。不相交子集 S_i 只包含满足 fish(n) == i 的节点。
  • 如果对于两个节点 ij,有 fish(n_i) != fish(n_j),则它们之间存在一条弧。
  • 节点 i 和节点 j 之间的代价是 time(n_i, n_j),如果 time(n_i, n_j) < distance(n_i, n_j)(即鱼到达之前无法到达该位置,例如因为时间倒流),则代价未定义。可以删除此类弧。
  • 需要添加一个额外的节点,表示具有弧和成本到所有其他节点的玩家的位置。

解决此问题将导致对每个节点子集进行一次访问(即每条鱼仅被捕获一次),以获取最小成本路径(即使所有鱼都被捕获所需的最短时间)。

该论文接着描述了如何将上述公式转化为传统的旅行商问题,并随后使用现有技术进行求解或近似解。我没有细读其细节,但另一篇声称以高效方式解决此问题的论文是这篇

存在复杂性问题,尤其是节点空间是无限的!通过仅生成到某个时间范围内的节点可以缓解这种情况。如果 t 是要为其生成节点的时间步数,而 f 是鱼的数量,则节点空间的大小将为 t * f。时间为 j 的节点将具有最多 (f - 1) * (t - j) 条出弧(因为它不能向后移动或移动到自己的子集)。弧的总数将按照 t^2 * f^2 的数量级增长。弧结构可能需要整理,以利用鱼路径最终是循环的事实。鱼将在其周期长度的最小公倍数处重复其配置,因此可能可以利用这一点。

我对 TSP 不够了解,无法确定这是否可行,并且我认为这并不意味着所发布的问题必然是 NP 困难的...但这是寻找最优或有界解决方案的一种方法。


谢谢,这对我来说是新的和非常有趣的。我认为我应该能够将这种转换与Christofides算法结合使用,以有效地找到一个近似于最优解3/2的解决方案。如果我成功了,我会将生成的路线添加到演示页面中。 - Peter de Rivaz
啊,我觉得我的计划有问题,因为虽然我的原始问题是一个满足度量上适当不等式的完全图,但所描述的转换结果是一个不完整的图,因此克里斯托菲德算法不再适用。无论如何,感谢你提供的有趣视角。 - Peter de Rivaz
是的,我忘了提到三角不等式不再成立。然而,它仍然是启发式解决方案和更一般的近似的良好起点。 - timxyz

1
我认为另一种方法是: 引用维基百科: 在数学中,Voronoi图是一种将空间分成多个区域的方法。预先指定一组点(称为种子、站点或生成器),对于每个种子,都会有一个相应的区域,其中包含所有比任何其他点更接近该种子的点。 因此,您选择一个目标,跟随它的路径进行一些步骤,并在那里设置种子点。也要对所有其他目标执行此操作,就可以得到Voroni图。根据您所在的区域,移动到其种子点。哇,你得到了第一条鱼。现在重复此步骤,直到捕获它们全部。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接