启发式转弯次数最少

时间:2010-01-20 14:57:47

标签: algorithm search artificial-intelligence heuristics

无论如何确保除了广度优先搜索之外的任何东西都能满足最少的转弯启发式数量吗?也许更多的解释会有所帮助。

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

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 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分: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.

答案 2 :(得分: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要高得多(我可以找出最佳解决方案)

答案 3 :(得分:1)

为了使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 - &gt; C - &gt; B,但是使用空间信息可能会比D具有更低的启发式成本。

根据您的具体情况,您可能能够使用实际上不是最短路径的解决方案,只要您能尽快得到答案。这里有一个很好的博客文章:Christer Ericson(关于PS3的战神3的编程)关于这个主题:http://realtimecollisiondetection.net/blog/?p=56

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

希望这有用,请告诉我是否有任何意义。

答案 4 :(得分:1)

这种广度优先搜索的未实现的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;
                }
            }
        }
    }
}

答案 5 :(得分:1)

本文的Dijkstra算法版本略快一些,它降低了常数项。仍然是O(n),因为你真的要看每个节点。

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

答案 6 :(得分:1)

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

自从Djikstra出局以来。我会推荐一个简单的DP,它具有在最佳时间运行而不需要构建图形的好处。

D[a][b]x=ay=b的最小距离,仅使用x<=ay<=b的节点。

由于您无法对角移动,因此在计算D[a-1][b]

时只需查看D[a][b-1]D[a][b]

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

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 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.

答案 7 :(得分:0)

  

无论如何要确保除了广度优先搜索之外的任何东西都能满足最少的转弯启发次数吗?

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

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

答案 8 :(得分:0)

这是我使用简单BFS的实现。 Dijkstra也可以工作(用stl::priority_queue替代按stl::queue降低成本排序的stl::set,但是会严重过度。

这里需要注意的是,我们实际上是在一个图表上搜索,该图表的节点与给定数组中的单元格不完全对应。为了得到那个图,我使用了一个简单的基于DFS的洪水填充(你也可以使用BFS,但DFS对我来说略短)。它的作用是找到所有连接和相同的字符组件并将它们分配给相同的颜色/节点。因此,在洪水填充之后,我们可以通过查看颜色[row] [col]的值来找出底层图中每个单元属于哪个节点。然后我只是迭代细胞并找出相邻细胞不具有相同颜色的所有细胞(即在不同的节点中)。因此,这些是我们图表的边缘。当我遍历单元格以消除重复边缘时,我保持#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"; } 个边缘。之后,从边缘列表构建邻接列表是一件简单的事情,我们已经为bfs做好了准备。

代码(用C ++编写):

#define

这只是一个快速的黑客,可以进行很多改进。但我认为它在运行时复杂性方面非常接近最优,并且应该足够快地运行数千个大小的板(不要忘记更改SIZE的{​​{1}})。另外,我只测试了你提供的一个案例。因此,正如Knuth所说,“谨防上述代码中的错误;我只是证明它是正确的,没有尝试过。” :)