Наименьшее количество витков эвристического - PullRequest
4 голосов
/ 20 января 2010

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

У меня есть случайный график, похожий на этот:

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 ]

3 голосов
/ 20 января 2010

Вы сами сказали, что каждая группа чисел представляет один узел, и каждый узел связан с соседними узлами.Тогда это простая проблема по кратчайшему пути , и вы можете использовать (например) алгоритм Дейкстры , где каждое ребро имеет вес 1 (на 1 оборот).

2 голосов
/ 20 января 2010

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

Здесь .. Мне было скучно :-) Это в 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.
1 голос
/ 21 января 2010

РЕДАКТИРОВАТЬ: ПРЕДЫДУЩАЯ ВЕРСИЯ БЫЛА НЕПРАВИЛЬНОЙ И БЫЛА УСТРАНЕНА

Так как Джикстра отсутствует. Я порекомендую простой DP, который будет работать в оптимальное время и не заставит вас строить график.

D[a][b] - это минимальное расстояние до x=a и y=b с использованием только тех узлов, где x<=a и y<=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), и это одна из лучших функций, на которую вы можете надеяться, и она имеет намного более низкий постоянный коэффициент, чем Джикстра .

Вы легко сможете справиться с N до нескольких тысяч в секунду.

1 голос
/ 21 января 2010

Эта статья имеет немного более быструю версию алгоритма Дийсктра, которая уменьшает постоянный член. Тем не менее, O (n), так как вам действительно придется смотреть на каждый узел.

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

1 голос
/ 21 января 2010

Эта ненастроенная реализация C поиска в ширину может прожевать сетку 100 на 100 менее чем за 1 мс. Вы, вероятно, можете сделать лучше.

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;
                }
            }
        }
    }
}
1 голос
/ 21 января 2010

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

К сожалению, в вашем случае, если вы не можете сделать некоторые упрощающие предположения о размере / форме узлов, я не уверен, что вы многое можете сделать. Например, рассмотрим переход от А к В в этом случае:

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.

В зависимости от ваших обстоятельств вы, возможно, сможете жить с решением, которое на самом деле не является кратчайшим путем, если вы сможете получить ответ раньше. Здесь есть хороший пост от Кристера Эриксона (программист God of War 3 на PS3) на тему: http://realtimecollisiondetection.net/blog/?p=56

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

Надеюсь, что это полезно, дайте мне знать, если что-то из этого не имеет смысла.

1 голос
/ 20 января 2010

Это похоже на ту же проблему, что и у этого проектора 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 гораздо выше (я могу найти оптимальное решение)

0 голосов
/ 21 января 2010

Это моя реализация с использованием простой BFS. Dijkstra также будет работать (вместо stl::priority_queue, который сортируется по убыванию стоимости для stl::queue), но будет серьезно излишним.

Здесь следует обратить внимание на то, что мы на самом деле ищем график, узлы которого не совсем соответствуют ячейкам в данном массиве. Чтобы добраться до этого графика, я использовал простую заливку на основе DFS (вы также можете использовать BFS, но DFS для меня немного короче). Для этого нужно найти все подключенные и одинаковые символьные компоненты и назначить их одному цвету / узлу. Таким образом, после заливки мы можем узнать, какому узлу принадлежит каждая ячейка в базовом графе, посмотрев на значение color [row] [col]. Затем я просто перебираю ячейки и нахожу все ячейки, в которых смежные ячейки не имеют одинаковый цвет (то есть находятся в разных узлах). Это, следовательно, ребра нашего графа. Я поддерживаю stl::set ребер, поскольку я перебираю ячейки, чтобы исключить дублирование ребер. После этого достаточно просто создать список смежности из списка ребер, и мы готовы к bfs.

Код (на С ++):

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

Это просто быстрый взлом, можно внести множество улучшений. Но я думаю, что он довольно близок к оптимальному с точки зрения сложности времени выполнения и должен работать достаточно быстро для плат размером в несколько тысяч (не забудьте изменить #define из SIZE). Кроме того, я проверил это только в одном случае, который вы предоставили. Итак, как сказал Кнут, «Остерегайтесь ошибок в приведенном выше коде; я только доказал, что это правильно, а не пробовал». :.)

0 голосов
/ 20 января 2010

Есть ли способ гарантировать, что эвристика наименьшего числа ходов будет удовлетворена чем-либо, кроме первого поиска в ширину?

A быстрее способ или более простой способ? :)

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...