【问题标题】:Fewest number of turns heuristic最少轮数启发式
【发布时间】:2011-01-07 07:36:26
【问题描述】:

有没有办法确保除了广度优先搜索之外的任何东西都能满足最少的轮数启发式?也许更多解释会有所帮助。

我有一个随机图,很像这样:

0 1 1 1 2
3 4 5 6 7
9 a 5 b c
9 d e f f
9 9 g h i

从左上角开始,我需要知道到达右下角所需的最少步数。每组连接的颜色被假定为单个节点,因此例如在这个随机图中,顶行的三个 1 都被视为单个节点,并且每个相邻(非对角线)连接的节点都是可能的下一个状态。所以从一开始,可能的下一个状态是第一行中的 1 或第二行中的 3。

目前我使用双向搜索,但树大小的爆炸性增长很快。在我的一生中,我无法调整问题,以便我可以安全地为节点分配权重并让它们确保最少的状态更改数量以达到目标,而不会变成广度优先搜索。将此视为城市地图,启发式将是达到目标的最少转弯次数。

此搜索的结果是最少的转数非常重要,因为该值是更复杂问题的启发式的一部分。

【问题讨论】:

  • 底部的 9 是否被视为一组 4?还是一组 3 个和一组 1 个(或 2 个)?还是2人一组、1人一组、1人一组?
  • 我不明白“(非水平)”是什么意思。 4 与起始 3 水平相邻。您的意思是“不是对角线”吗?
  • 所有 4 个 9 都被视为一个组。是的,我的意思不是对角线,现在修复它。
  • 当我能理解这件事在现实生活中的映射时,我会觉得更好。这些是街道吗?建筑物?其他?
  • 我会将映射更改为所有相同值的条目都是同一节点的一部分。将它们视为街道是一个很好的比较。

标签: algorithm search artificial-intelligence heuristics


【解决方案1】:

这看起来和这个投影机http://projecteuler.net/index.php?section=problems&id=81有同样的问题

解的复杂度是 O(n) n-> 节点数

你需要的是记忆。

在每个步骤中,您最多可以从 2 个方向获得。所以选择更便宜的解决方案。

有点像(如果在边界上,只需添加取 0 的代码)

for i in row:
    for j in column:
         matrix[i][j]=min([matrix[i-1][j],matrix[i][j-1]])+matrix[i][j]

现在,如果您只是向左或向下移动,您就有了最便宜的解决方案

解在矩阵[MAX_i][MAX_j]中

如果你也可以向左和向上,比BigO高得多(我可以找出最优解)

【讨论】:

【解决方案2】:

这听起来像是 Dijkstra 的算法。最困难的部分在于正确设置图表(跟踪哪个节点获取哪个子节点),但是如果您可以为此投入一些 CPU 周期,那么之后就可以了。

为什么不想要广度优先搜索?

这里.. 我很无聊 :-) 这是用 Ruby 编写的,但可以帮助您入门。请注意,它未经测试。

class Node
  attr_accessor :parents, :children, :value
  def initialize args={}
    @parents = args[:parents] || []
    @children = args[:children] || []
    @value = args[:value]
  end

  def add_parents *args
    args.flatten.each do |node|
      @parents << node
      node.add_children self unless node.children.include? self
    end
  end

  def add_children *args
    args.flatten.each do |node|
      @children << node
      node.add_parents self unless node.parents.include? self
    end
  end
end

class Graph
  attr_accessor :graph, :root
  def initialize args={}
    @graph = args[:graph]
    @root = Node.new
    prepare_graph
    @root = @graph[0][0]
  end

  private

  def prepare_graph
# We will iterate through the graph, and only check the values above and to the
# left of the current cell.

    @graph.each_with_index do |row, i|
      row.each_with_index do |cell, j|
        cell = Node.new :value => cell #in-place modification!
        # Check above
        unless i.zero?
          above = @graph[i-1][j]
          if above.value == cell.value
            # Here it is safe to do this: the new node has no children, no parents.
            cell = above
          else
            cell.add_parents above
            above.add_children cell # Redundant given the code for both of those
            # methods, but implementations may differ.
          end
        end
        # Check to the left!
        unless j.zero?
          left = @graph[i][j-1]
          if left.value == cell.value
            # Well, potentially it's the same as the one above the current cell,
            # so we can't just set one equal to the other: have to merge them.
            left.add_parents cell.parents
            left.add_children cell.children
            cell = left
          else
            cell.add_parents left
            left.add_children cell
          end
        end
      end
    end
  end
end
     #j = 0, 1, 2, 3, 4
graph = [
         [3, 4, 4, 4, 2], # i = 0
         [8, 3, 1, 0, 8], # i = 1
         [9, 0, 1, 2, 4], # i = 2
         [9, 8, 0, 3, 3], # i = 3
         [9, 9, 7, 2, 5]] # i = 4

maze = Graph.new :graph => graph

# Now, going from maze.root on, we have a weighted graph, should it matter.
# If it doesn't matter, you can just count the number of steps.
# Dijkstra's algorithm is really simple to find in the wild.

【讨论】:

  • 我不想要广度优先搜索,因为当你将它提升到一个边足够大的正方形时,它永远不会完成。该算法将在一个放大到每边 15 条边长的示例正方形上运行。平均最小圈数运行约22圈,实际问题的分支因子每级约7。
  • 您是否特别需要最短路径才能执行其他操作?因为如果这是您唯一的问题,一个权重相等的加权双向图并找到最短路径,那么您没有很多选择。如果您的搜索树太大,您可能需要先修剪您的搜索树。这里:rubylearning.com/blog/2009/12/27/rpcfn-mazes-5 最后一个是找到最短路径的实现,你有很多代码可以看;)
  • @NiskLarsen:一个 15*15 的正方形最多可以有 225 个节点。 BFS 是 O(V+E),分支因子的东西在你的图中不会起作用,因为不是所有的边都会导致新的节点。
  • "每边最多有 15 条边长" 等一下。这应该在眨眼之间运行。您的代码有问题。
  • 我同意杰森的观点。向我们展示您的代码!把它放在 github 上或者放在一个馅饼里什么的。您在 Python 和 C 中对以下算法进行了一些很好的实现。您应该已经准备好了。
【解决方案3】:

你自己说过每组数字代表一个节点,每个节点都连接到相邻的节点。那么这是一个简单的shortest-path 问题,您可以使用(例如)Dijkstra's algorithm,每条边的权重为 1(转一圈)。

【讨论】:

  • Dijkstra 的每条边具有相同的值是广度优先搜索。我正在寻找一种不是广度优先搜索的方法。
  • @Nick:这是 O(n) - 您是否希望在不访问所有(大多数)节点的情况下以某种方式找到答案?
  • 如果边长为 N,那么您希望得到的最佳算法是 O(N^2),并且使用边权重为 1 的 Djikstra 算法,使用堆栈可以得到.是的,它基本上是一个呼吸优先的搜索,但你不会比它更快,因为它只访问每个节点一次,而 DFS 多次遍历每个节点。
  • @JPvdMerwe:边长为1;你是说它是O(1 ^ 2)吗?它实际上是 O(n)(n = 节点数),因为每个节点只需要被访问一次(一旦访问过一次,就将其标记为已访问并存储到它的最短路径)
  • 你是对的。我递归地构建它并扩展队列中的每条可能路径。因此,我一直认为它是指数级的。
【解决方案4】:

有没有办法确保除了广度优先搜索之外的任何东西都能满足最少的轮数启发式?

更快的方式,还是更简单的方式? :)

您可以从两端交替进行广度优先搜索,直到两个区域在中间相遇。如果图表有很多扇出,这会快得多,比如城市地图,但最坏的情况是一样的。这真的取决于图表。

【讨论】:

  • 正如我在帖子中提到的,我已经使用了双向搜索方法。老实说,我不知道有什么比广度优先搜索更容易的事情,但我正在寻找有机会运行得更快的东西。
  • 啊!我想知道“双向搜索”是什么意思,但假设您的意思是水平和垂直。
  • 我想凭直觉很容易找到合适的路径。很难在不检查大部分细胞的情况下证明没有更快的路径。
【解决方案5】:

为了让 A* 始终找到最短路径,您的启发式算法需要始终低估实际成本(启发式算法是“可接受的”)。简单的启发式方法(例如在网格上使用欧几里德或曼哈顿距离)效果很好,因为它们的计算速度很快,并且保证小于或等于实际成本。

不幸的是,就您而言,除非您可以对节点的大小/形状做出一些简化假设,否则我不确定您能做多少。例如,考虑在这种情况下从 A 到 B:

B 1 2 3 A
C 4 5 6 D
C 7 8 9 C
C e f g C
C C C C C

最短路径是 A -> D -> C -> B,但使用空间信息可能会给 3 比 D 更低的启发式成本。

根据您的具体情况,只要您能尽快得到答案,您就可以接受实际上并不是最短路径的解决方案。 Christer Ericson(PS3 上的《战神 3》程序员)在此发表了一篇不错的博文:http://realtimecollisiondetection.net/blog/?p=56

这是我对不可接受的启发式的想法:从该点开始,水平移动直到达到目标,然后垂直移动直到达到目标,然后计算您所做的状态更改次数。您也可以计算其他测试路径(例如,垂直然后水平),并选择最小值作为最终启发式。如果您的节点大小大致相等且形状规则(与我的示例不同),这可能会做得很好。你做的测试路径越多,你得到的越准确,但它会越慢。

希望对您有所帮助,如果有任何不妥之处,请告诉我。

【讨论】:

    【解决方案6】:

    这是我使用简单 BFS 的实现。 Dijkstra 也可以工作(替换stl::priority_queue,按stl::queue 的降价排序),但会严重过大。

    这里要注意的是,我们实际上是在一个图上搜索,其节点与给定数组中的单元格不完全对应。为了获得该图,我使用了一个简单的基于 DFS 的填充(您也可以使用 BFS,但 DFS 对我来说稍微短一些)。这样做是找到所有连接的和相同的字符组件,并将它们分配给相同的颜色/节点。因此,在填充之后,我们可以通过查看 colour[row][col] 的值来找出每个单元格在底层图中属于哪个节点。然后我只是遍历单元格并找出相邻单元格不具有相同颜色的所有单元格(即位于不同节点中)。因此,这些是我们图表的边缘。当我遍历单元格以消除重复的边缘时,我维护了一个stl::set 的边缘。之后,从边列表构建邻接列表就很简单了,我们就可以开始 bfs 了。

    代码(C++):

    #include <queue>
    #include <vector>
    #include <iostream>
    #include <string>
    #include <set>
    #include <cstring>
    
    using namespace std;
    #define SIZE 1001
    vector<string> board;
    
    int colour[SIZE][SIZE];
    int dr[]={0,1,0,-1};
    int dc[]={1,0,-1,0};
    
    int min(int x,int y){ return (x<y)?x:y;}
    int max(int x,int y){ return (x>y)?x:y;}
    
    void dfs(int r, int c, int col, vector<string> &b){
        if (colour[r][c]<0){
            colour[r][c]=col;
            for(int i=0;i<4;i++){
                int nr=r+dr[i],nc=c+dc[i];
                if (nr>=0 && nr<b.size() && nc>=0 && nc<b[0].size() && b[nr][nc]==b[r][c])
                    dfs(nr,nc,col,b);
            }
        }
    }
    
    int flood_fill(vector<string> &b){
        memset(colour,-1,sizeof(colour));
        int current_node=0;
        for(int i=0;i<b.size();i++){
            for(int j=0;j<b[0].size();j++){
                if (colour[i][j]<0){
                    dfs(i,j,current_node,b);
                    current_node++;
                }
            }
        }
        return current_node;
    }
    
    vector<vector<int> > build_graph(vector<string> &b){
        int total_nodes=flood_fill(b);
        set<pair<int,int> > edge_list;
        for(int r=0;r<b.size();r++){
            for(int c=0;c<b[0].size();c++){
                for(int i=0;i<4;i++){
                    int nr=r+dr[i],nc=c+dc[i];
                    if (nr>=0 && nr<b.size() && nc>=0 && nc<b[0].size() && colour[nr][nc]!=colour[r][c]){
                        int u=colour[r][c], v=colour[nr][nc];
                        if (u!=v) edge_list.insert(make_pair(min(u,v),max(u,v)));
                    }
                }
            }
        }
        vector<vector<int> > graph(total_nodes);
        for(set<pair<int,int> >::iterator edge=edge_list.begin();edge!=edge_list.end();edge++){
            int u=edge->first,v=edge->second;
            graph[u].push_back(v);
            graph[v].push_back(u);
        }
        return graph;
    }
    
    int bfs(vector<vector<int> > &G, int start, int end){
        vector<int> cost(G.size(),-1);
        queue<int> Q;
        Q.push(start);
        cost[start]=0;
        while (!Q.empty()){
            int node=Q.front();Q.pop();
            vector<int> &adj=G[node];
            for(int i=0;i<adj.size();i++){
                if (cost[adj[i]]==-1){
                    cost[adj[i]]=cost[node]+1;
                    Q.push(adj[i]);
                }
            }
        }
        return cost[end];
    }
    
    int main(){
        string line;
        int rows,cols;
        cin>>rows>>cols;
        for(int r=0;r<rows;r++){
            line="";
            char ch;
            for(int c=0;c<cols;c++){
                cin>>ch;
                line+=ch;
            }
            board.push_back(line);
        }
        vector<vector<int> > actual_graph=build_graph(board);
        cout<<bfs(actual_graph,colour[0][0],colour[rows-1][cols-1])<<"\n";
    }
    

    这只是一个快速破解,可以进行很多改进。但我认为它在运行时复杂性方面非常接近最优,并且对于几千块大小的板应该运行得足够快(不要忘记更改SIZE#define)。另外,我只用您提供的一个案例对其进行了测试。所以,正如 Knuth 所说,“当心上述代码中的错误;我只是证明它是正确的,没有尝试过。” :)。

    【讨论】:

      【解决方案7】:

      这种未调整的 C 广度优先搜索实现可以在不到 1 毫秒的时间内浏览 100×100 的网格。你或许可以做得更好。

      int shortest_path(int *grid, int w, int h) {
          int mark[w * h];  // for each square in the grid:
                            // 0 if not visited
                            // 1 if not visited and slated to be visited "now"
                            // 2 if already visited
          int todo1[4 * w * h];  // buffers for two queues, a "now" queue
          int todo2[4 * w * h];  // and a "later" queue
          int *readp;            // read position in the "now" queue
          int *writep[2] = {todo1 + 1, 0};
          int x, y, same;
      
          todo1[0] = 0;
          memset(mark, 0, sizeof(mark));
      
          for (int d = 0; ; d++) {
              readp = (d & 1) ? todo2 : todo1;      // start of "now" queue
              writep[1] = writep[0];                // end of "now" queue
              writep[0] = (d & 1) ? todo1 : todo2;  // "later" queue (empty)
      
              // Now consume the "now" queue, filling both the "now" queue
              // and the "later" queue as we go. Points in the "now" queue
              // have distance d from the starting square. Points in the
              // "later" queue have distance d+1.
              while (readp < writep[1]) {
                  int p = *readp++;
                  if (mark[p] < 2) {
                      mark[p] = 2;
                      x = p % w;
                      y = p / w;
                      if (x > 0 && !mark[p-1]) {                // go left
                          mark[p-1] = same = (grid[p-1] == grid[p]);
                          *writep[same]++ = p-1;
                      }
                      if (x + 1 < w && !mark[p+1]) {            // go right
                          mark[p+1] = same = (grid[p+1] == grid[p]);
                          if (y == h - 1 && x == w - 2)
                              return d + !same;
                          *writep[same]++ = p+1;
                      }
                      if (y > 0 && !mark[p-w]) {                // go up
                          mark[p-w] = same = (grid[p-w] == grid[p]);
                          *writep[same]++ = p-w;
                      }
                      if (y + 1 < h && !mark[p+w]) {            // go down
                          mark[p+w] = same = (grid[p+w] == grid[p]);
                          if (y == h - 2 && x == w - 1)
                              return d + !same;
                          *writep[same]++ = p+w;
                      }
                  }
              }
          }
      }
      

      【讨论】:

        【解决方案8】:

        本文有一个稍快的 Dijsktra 算法版本,它降低了常数项。不过仍然是 O(n),因为你真的要查看每个节点。

        http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.54.8746&rep=rep1&type=pdf

        【讨论】:

          【解决方案9】:

          编辑:以前的版本错误并已修复

          自从 Djikstra 出来了。我会推荐一个简单的 DP,它的好处是可以在最佳时间运行,并且不需要您构建图表。

          D[a][b] 是到x=ay=b 的最小距离,仅使用x&lt;=ay&lt;=b 所在的节点。

          由于您不能沿对角线移动,因此在计算 D[a][b] 时只需查看 D[a-1][b]D[a][b-1]

          这为您提供以下重复关系:

          D[a][b] = min(if grid[a][b] == grid[a-1][b] then D[a-1][b] else D[a-1][b] + 1, if grid[a][b] == grid[a][b-1] then D[a][b-1] else D[a][b-1] + 1)
          

          但是,在这种情况下仅执行上述操作会失败:

          0 1 2 3 4
          5 6 7 8 9
          A b d e g
          A f r t s
          A z A A A
          A A A f d
          

          因此,您需要缓存到目前为止找到的每组节点的最小值。而不是查看D[a][b],而是查看grid[a][b] 组的最小值。

          这是一些 Python 代码: 注意grid 是您作为输入提供的网格,并且假设网格是N by N

          groupmin = {}
          
          for x in xrange(0, N):
              for y in xrange(0, N):
                  groupmin[grid[x][y]] = N+1#N+1 serves as 'infinity'
          
          #init first row and column
          groupmin[grid[0][0]] = 0
          for x in xrange(1, N):
              gm = groupmin[grid[x-1][0]]
              temp = (gm) if grid[x][0] == grid[x-1][0] else (gm + 1)
              groupmin[grid[x][0]] = min(groupmin[grid[x][0]], temp); 
          
          for y in xrange(1, N):
              gm = groupmin[grid[0][y-1]]
              temp = (gm) if grid[0][y] == grid[0][y-1] else (gm + 1)
              groupmin[grid[0][y]] = min(groupmin[grid[0][y]], temp); 
          
          #do the rest of the blocks
          for x in xrange(1, N):
              for y in xrange(1, N):
                  gma = groupmin[grid[x-1][y]]
                  gmb = groupmin[grid[x][y-1]]
                  a = (gma) if grid[x][y] == grid[x-1][y] else (gma + 1)
                  b = (gmb) if grid[x][y] == grid[x][y-1] else (gma + 1)
                  temp = min(a, b)
                  groupmin[grid[x][y]] = min(groupmin[grid[x][y]], temp); 
          
          ans = groupmin[grid[N-1][N-1]]
          

          这将在O(N^2 * f(x)) 中运行,其中f(x) 是散列函数所花费的时间,通常是O(1) 时间,这是您可以期待的最佳函数之一,它的常数因子比 Djikstra 的低很多.

          您应该能够轻松地在一秒钟内处理多达几千个的 N。

          【讨论】:

            猜你喜欢
            • 2013-03-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-02-11
            • 1970-01-01
            • 1970-01-01
            • 2011-12-28
            • 1970-01-01
            相关资源
            最近更新 更多