【问题标题】:How many moves to reach a destination? Efficient flood filling多少步才能到达目的地?高效注水
【发布时间】:2011-12-28 13:02:01
【问题描述】:

我想计算单元格与目标单元格的距离,使用四向移动的次数到达某物。因此,紧邻目的地的四个单元格的距离为 1,它们各自的四个基本方向上的单元格的距离为 2,以此类推。最大距离可能在 16 或 20 左右,并且有些单元格被障碍物占据;距离可以绕着它们流动,但不能穿过它们。

我想将输出存储到一个二维数组中,并且我希望能够非常快速地为更大的迷宫地图上的任何目的地计算这个“距离图”。

我通过洪水填充的变体成功地做到了这一点,我将相邻未填充单元的增量距离放在优先级队列中(使用 C++ STL)。

我对功能很满意,现在想专注于优化代码,因为它对性能非常敏感。

可能有哪些巧妙而快速的方法?

【问题讨论】:

  • 没有代码很难知道可以优化什么...
  • 您可以使用简单的queue 而不是priority queue,因为您按照与起始单元格的距离递增的顺序遍历单元格。

标签: algorithm fill


【解决方案1】:

我认为你做的一切都是正确的。如果编码正确,则需要 O(n) 时间和 O(n) 内存来计算洪水填充,其中 n 是单元数,并且可以证明不可能做得更好(在一般情况下)。填充完成后,您只需返回任何目的地的距离 O(1),很容易看出它也可以做得更好。

所以如果你想优化性能,你只能专注于代码本地优化。这不会影响渐近,但可以显着提高您的实际执行时间。但是如果没有真正看到源代码,很难给你任何代码优化建议。

因此,如果您真的想查看优化代码,请参阅以下(纯 C):

包括

int* BFS()
{
    int N, M; // Assume we have NxM grid.
    int X, Y; // Start position. X, Y are unit based.
    int i, j;
    int movex[4] = {0, 0, 1, -1}; // Move on x dimension.
    int movey[4] = {1, -1, 0, 0}; // Move on y dimension.

    // TO DO: Read N, M, X, Y

    // To reduce redundant functions calls and memory reallocation 
    // allocate all needed memory once and use a simple arrays.
    int* map = (int*)malloc((N + 2) * (M + 2)); 
    int leadDim = M + 2;
    // Our map. We use one dimension array. map[x][y] = map[leadDim * x + y];
    // If (x,y) is occupied then map[leadDim*x + y] = -1;
    // If (x,y) is not visited map[leadDim*x + y] = -2;

    int* queue = (int*)malloc(N*M);
    int first = 0, last =1; 

    // Fill the boarders to simplify the code and reduce conditions
    for (i = 0; i < N+2; ++i)
    {
        map[i * leadDim + 0] = -1;
        map[i * leadDim + M + 1] = -1;
    }

    for (j = 0; j < M+2; ++j)
    {
        map[j] = -1;
        map[(N + 1) * leadDim + j] = -1;
    }

    // TO DO: Read the map.

    queue[first] = X * leadDim + Y;
    map[X * leadDim + Y] = 0;

    // Very simple optimized process loop.
    while (first < last) 
    {
        int current = queue[first];
        int step = map[current];

        for (i = 0; i < 4; ++i)
        {
            int temp = current + movex[i] * leadDim + movey[i];
            if (map[temp] == -2) // only one condition in internal loop.
            {
                map[temp] = step + 1;
                queue[last++] = temp;
            }
        }

        ++first;
    }

    free(queue);

    return map;
}

代码可能看起来很棘手。当然,它看起来不像 OOP(我实际上认为 OOP 粉丝会讨厌它),但如果你想要一些非常快的东西,那就是你需要的。

【讨论】:

  • 优先队列不是 O(1)?我希望描述行进或扫掠算法
  • 有很多优先级队列数据结构,专为不同的需求而设计。最简单的称为二叉堆,对于添加、删除、优先级增加、优先级减少操作具有 O(log n) 渐近性。一些新方法(例如斐波那契和松弛堆)将某些操作(在某些条件下)的渐近性降低到 O(1),但它们非常复杂并且具有显着的常数。您需要的简单 FIFO 队列已经在 O(1) 中进行了推送/弹出。而这个任务的性质保证了基于FIFO的解决方案的有效性。仍然没有什么可以使用算法理论进行优化的。
  • 有趣的话题,但他们调查的任务比你的要复杂。对于您的简单案例,您可以使用更简单(有效)的解决方案。
【解决方案2】:

从递归实现开始:(未经测试的代码)

 int visit( int xy, int dist) {
    int ret =1;
    if (array[xy] <= dist) return 0;
    array[xy] = dist;
    if (dist == maxdist) return ret;
    ret += visit ( RIGHT(xy) , dist+1);
    ... 
    same for left, up, down
    ...
    return ret;
    }

您需要处理初始化和边缘情况。而且您必须决定是要二维阵列还是一维阵列。

下一步可能是使用待办事项列表并删除递归,第三步可能是添加一些位掩码。

【讨论】:

  • BFS 在该任务上优于 DFS
  • 这是一个糟糕解决方案的好例子。起初它是递归的,而没有递归则有一个清晰快速的解决方案。过多的递归调用会降低性能。其次,它会遭受大矩阵的堆栈溢出。
  • 递归并不总是很慢。此外,使用递归来证明程序的属性通常要容易得多。
  • 恕我直言,第一步应该总是很简单。正如我所说:下一步应该是使用待办事项列表/队列/堆/任何东西。
  • “递归并不总是很慢。”取决于您将使用什么来比较。同一事物的良好直接实现总是比递归更好。但当然并不总是更容易理解和更快地编写代码。而实际上这段代码还不够清楚。如果您真的是指 DFS,那么它对这项任务完全没用。事实上,这需要指数级的时间。
【解决方案3】:

这是BFS 的常见任务。复杂度为 O(cellsCount)

我的 c++ 实现:

vector<vector<int> > GetDistance(int x, int y, vector<vector<int> > cells)
{
    const int INF = 0x7FFFFF;
    vector<vector<int> > distance(cells.size());
    for(int i = 0; i < distance.size(); i++)
        distance[i].assign(cells[i].size(), INF);
    queue<pair<int, int> > q;

    q.push(make_pair(x, y));
    distance[x][y] = 0;

    while(!q.empty())
    {
        pair<int, int> curPoint = q.front();
        q.pop();
        int curDistance = distance[curPoint.first][curPoint.second];
        for(int i = -1; i <= 1; i++)
            for(int j = -1; j <= 1; j++)
            {
                if( (i + j) % 2 == 0 ) continue;
                pair<int, int> nextPoint(curPoint.first + i, curPoint.second + j);
                if(nextPoint.first >= 0 && nextPoint.first < cells.size()
                   && nextPoint.second >= 0 && nextPoint.second < cells[nextPoint.first].size()
                   && cells[nextPoint.first][nextPoint.second] != BARRIER
                   && distance[nextPoint.first][nextPoint.second] > curDistance + 1)
                   {
                       distance[nextPoint.first][nextPoint.second] = curDistance + 1;
                       q.push(nextPoint);
                   }                    
            }
    }
    return distance;
}

【讨论】:

    【解决方案4】:

    1970 年代的 8 位计算机使用具有相同算法复杂度的优化来做到这一点,但在典型情况下在实际硬件上要快得多。

    从最初的正方形开始,左右扫描,直到找到“墙”。现在你有一个“跨度”,它是一平方高,N 平方宽。将跨度标记为“已填充”,在这种情况下,每个正方形都标有到初始正方形的距离。

    对于当前跨度上方和下方的每个方格,如果它不是“墙”或已填充,则选择它作为跨度的新原点。

    重复直到找不到新的跨度。

    由于水平行倾向于连续存储在内存中,因此与没有水平搜索偏差的算法相比,该算法对缓存的冲击要小得多。

    此外,由于在最常见的情况下,从堆栈中推送和弹出的项目要少得多(跨越而不是单个块),因此维护堆栈所花费的时间更少。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-27
      相关资源
      最近更新 更多