【问题标题】:Most memory efficient algorithm for finding a path on a grid在网格上查找路径的内存效率最高的算法
【发布时间】:2016-11-28 00:20:34
【问题描述】:

可用于查找从一个方格到另一个方格的路径的最节省内存的算法是什么?网格可能有无法跨越的障碍。成为最短路径不是必需的,但肯定是一种奖励。该算法将用 C 编码(C++ 可用,但我避免使用它以减少内存使用)并在只有 2048 字节 SRAM 的 ATmega328 芯片上运行。 CPU 效率并不是最重要的。

编辑: 网格是 16 x 32 方格,每个方格用一位表示。因此,总内存使用量为 64 字节。网格存储为无符号字符的二维数组,所有 2048 个字节都可用。输出将是一个整数数组,引用应该采用的正方形。

如果正方形中有障碍物,则正方形数组的值为 1 而不是 0。这些方块应该像墙一样对待。

【问题讨论】:

  • 网格数据是静态的吗?如果是这样,您可以预先计算一个查找表,这样您就不必在设备上执行路径查找算法。如果可用,您可以将查找表存储在 ROM 上的闪存中,以便它不使用 ram。
  • @nbering 数据不是静态的,因为它是在运行时计算的,但是一旦收集到数据,就不太可能发生变化
  • 我推荐Dijkstra's_algorithm
  • @WeatherVane 优先队列不会导致更大的内存使用吗?
  • @RandomUser SO 通常不喜欢没有任何证据证明您已经尝试过的问题,而不仅仅是要求现成的解决方案,并补充说您没有解释任何约束(并且仍然错过,例如,障碍的形式)。所以这可以被认为是“不清楚你在问什么”和/或“太宽泛”。哦,还有 - 如果你明确不想使用 C++ - 不要标记它?!?

标签: c algorithm performance memory


【解决方案1】:

如果您只想找到目标,但不关心记住所走的路径,那么随机搜索几乎是最佳的记忆方式。它不需要记住有关先前状态的任何内容,因此内存使用是恒定的。 (另一方面,时间复杂度是无限的,这不是很好,但不排除在您的要求之外)

如果您确实需要记住所采用的路径,那么您不能使用完整的算法低于线性空间复杂度 - 即如果路径存在,则始终找到路径。广度优先搜索和深度优先搜索都具有线性空间复杂度,因此它们将与最优完全算法渐近地属于同一类。

由于内存非常有限,您可能更喜欢使用内存有界算法,该算法为您提供恒定的内存使用上限,但不能保证找到可能存在的路径。我推荐 Simplified Memory Bounded A*。

【讨论】:

  • 在内存使用和时间复杂度方面,随机搜索与完全不使用地图的算法(如 Trémaux 算法)相比如何?
  • @RandomUser 我不知道,我没有分析它的复杂性。时间复杂性不是无限的,这是肯定的。
  • 考虑到问题的规模,随机搜索将需要很长时间,尤其是在没有路径的情况下。
【解决方案2】:

对于可能适合 2048 字节的算法,这是一个未完成的想法,我在尝试找到非递归洪水填充变体时提出了这个想法。

第一步是创建一个额外的 32×16 的 8 位值数组;这使用 512 个字节。然后,您水平遍历网格,并对相邻可到达方块的运行进行编号,如下图所示:

对于 32×16 网格,最大运行次数为 256(例如,使用棋盘格图案或垂直条纹),因此此编号适合 8 位值。

第二步是垂直遍历网格,并将相邻的运行分组:

检查垂直线1后:
{0A,11,1A}
{2E}
{44,50,5C}
{72}
{87,8F,98}

检查垂直线2后:
{0A,11,1A,00,24}
{2E}
{44,50,5C,37,69}
{72}
{87,8F,98,7C}

检查垂直线2后:
{0A,11,1A,00,24,12,2F}
{2E}
{44,50,5C,37,69,51,73}
{72}
{87,8F,98,7C,90}

... 以此类推,如果它们由相邻的运行链接,则合并组。如果最后,起始和目标方块的数量在同一组中,则表示存在路径。

现在,如果您将组存储为简单列表,就像上面的示例一样,这并不能真正为您提供路径;它只是告诉您从起点和目标方格​​可以到达哪些方格,但路径可能不需要穿过所有这些方格。

如果您将组存储在您知道哪些运行相互连接的数据结构中,那么它会成为较小空间中的“通过图的最短路径”问题。我不确定哪种数据结构最适合剩余的 1536 个字节。

(欢迎任何人尝试并进一步推动这个想法。)


此方法可用于在运行另一种算法之前简化网格。首先,运行的分组标识了网格中不可到达的部分;这些可以标记为原始网格中的墙壁或它的副本。其次,它确定了死胡同;仅连接到另一条运行(并且不包含起始或目标方格)的运行是不必要的弯路,也可以这样标记。 (这应该重复:删除一个单连接的运行可能会显示另一个运行是单连接的。)

通过删除无法访问和单独链接的运行来简化网格

再次运行算法,但使用垂直运行和水平分组,可以消除额外的死角。


下面的 JavaScript sn-p 是算法第一部分的简单代码示例:使用图像中的示例网格,它对运行进行编号,将它们分配到组,必要时合并组,然后检查是否start 和 target square 在同一组中,即是否有路径。

分组方法可能不是最有效的,尤其是在合并组时,但它使用最大 256 字节(运行次数 × 8 位值)的固定大小数组,这在有限内存中可能是最好的情况。

function gridPath(grid, x1, y1, x2, y2) {
    var runs = [], rcount = 0;
    for (var i = 0; i < 16; i++) {           // number runs
        var start = true; runs[i] = [];
        for (var j = 0; j < 32; ++j) {
            if (grid[i][j] == 0) {           // found empty cell
                if (start) ++rcount;         // start of new run
                runs[i][j] = rcount - 1;
                start = false;
            }
            else start = true;               // found blocked cell
        }
    }
    var groups = [], gcount = 0;
    for (var i = 0; i < rcount; i++) groups[i] = 0xFF;

    for (var j = 0; j < 32; ++j) {           // assign runs to groups
        var g = [];
        for (var i = 0; i < 16; ++i) {
            if (grid[i][j] == 0) g.push(runs[i][j]);
            if ((grid[i][j] == 1 || i == 15) && g.length > 0) {
                insertGroup(g);
                g = [];
            }
        }
    }     
    return groups[runs[y1][x1]] == groups[runs[y2][x2]];

    function insertGroup(g) {
        var matches = [];
        for (var i = 0; i < g.length; i++) { // check if runs are already in group
            if (groups[g[i]] != 0xFF && matches.indexOf(groups[g[i]]) < 0) {
                matches.push(groups[g[i]]);
            }
        }
        if (matches.length == 0) matches.push(gcount++); // start new group
        for (var i = 0; i < g.length; i++) { // add runs to group
            groups[g[i]] = matches[0];
        }
        if (matches.length > 1) {            // merge groups
            for (var i = 0; i < rcount; i++) {
                if (matches.indexOf(groups[i]) > 0) groups[i] = matches[0];
            }
        }
    }
}

var grid = [[1,0,1,0,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0],
            [0,0,0,1,0,0,0,1,0,0,0,0,0,1,1,1,1,1,1,1,0,0,1,0,0,0,0,1,0,0,1,0],
            [0,1,0,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,1,0,0,0,1,0,0],
            [0,0,1,0,1,0,1,0,1,0,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0,1,1,0,1,0,0,1],
            [1,0,0,1,0,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1,0,0,1,0,1,0,0,1,0,0,1,0],
            [0,1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1,1,0,0,1,0,1,1,0,0,0,0,0,1,0,1],
            [1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,1,1,1,1,0,1,0],
            [0,0,0,1,0,0,0,1,0,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0,0,0],
            [0,1,0,0,0,1,0,0,0,1,1,0,1,0,0,1,0,0,1,0,1,0,1,0,1,0,1,0,0,0,1,0],
            [0,0,1,0,1,0,1,0,1,0,1,0,0,1,0,0,0,1,0,0,1,0,1,0,1,0,0,1,0,1,0,0],
            [1,0,0,1,0,0,0,1,0,0,0,1,0,0,1,1,1,0,0,1,0,0,1,0,0,0,1,0,1,0,0,1],
            [0,1,0,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,1,0,0,1,0,1,0,0,0,1,0,0,1,0],
            [1,0,1,0,1,0,1,0,1,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0,1,0,1,0,1,0,0,1],
            [0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,1,0,0,0,1,0,0],
            [0,1,0,0,0,1,0,0,0,1,0,0,1,1,1,1,1,1,1,0,0,1,0,0,1,0,0,1,0,0,1,0],
            [0,0,1,0,1,0,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0]];
document.write(gridPath(grid, 0, 15, 15, 7));

【讨论】:

  • 这是一个非常有趣的想法,然而,它似乎比动态运行的算法更适合作为预处理算法,特别是考虑到 ATmega328 的处理能力。
  • @RandomUser 我必须承认我什至不知道 ATmega328 是什么 :-)(谷歌告诉我它在一些 Arduino 模型中)
【解决方案3】:

我研究过使用 Dijkstra(如 Weather Vane 所建议的那样),这将要求存储每个网格单元到起点的距离和前一个单元的方向。

不幸的是,32x16 网格上的路径距离可能大于 255;我找到的最长路径距离为 319(见下图,左)。这意味着距离不适合 8 位,并且距离矩阵的大小为 1024 字节。

左:最长路径(距离=319)。右图:最大等距单元数(距离 16 处有 72 个单元)

但是,在所有距离都等于 1 的方形网格中,您可以将 Dijkstra 简化为不使用距离矩阵的广度优先搜索;如果您使用先进先出队列,则按照与起始单元的距离顺序访问单元,因此您无法找到到已访问单元的更短路径。

fifo 队列将包含一定距离的每个单元格,然后逐渐过渡到距离 + 1,依此类推。队列的最大大小取决于可以有多少等距单元;我发现的最大值是 72(见上图,右图),在从前一个距离过渡期间,这需要一个可以容纳 76 个单元格或 152 个字节的坐标的队列。

算法返回的路径是一个数组,最多保存 320 个单元格的坐标,因此它的最大大小为 640 字节。在构造这个数组之前,队列可以被丢弃,所以只有方向网格和路径同时在内存中。

下面是简化算法的代码示例,只有一个方向矩阵和一个fifo队列;它可能在很多方面都可以改进,但它证明了这个想法。 findPath() 函数使用最少 664 到最多 1152 字节的已分配内存(取决于路径长度)加上大约 20 字节用于附加变量。

这可以进一步减少,例如通过将方向矩阵存储为 4 位半字节,将其大小从 512 字节减少到 256 字节(但需要更多计算),或者通过将路径作为上/右/下/左方向序列而不是单元坐标返回,这将每步只需要 2 位,将其最大大小从 640 字节减少到 80 字节。

#include <stdlib.h>                                         //  gcc -std=c99

short int findPath(char grid[][32], char x1, char y1, char x2, char y2, char **path) {
    char (*dir)[16][32] = calloc(512, 1);                   // allocate direction matrix: 512 bytes (zeros)
    (*dir)[y2][x2] = 5;                                     // mark starting cell as visited (search backwards)
    char *queue = malloc(152);                              // allocate fifo queue: 152 bytes
    queue[0] = x2; queue[1] = y2;                           // put starting cell in queue (search backwards)
    unsigned char qRead = 0, qWrite = 2;                    // queue pointers
    char qCurSize = 1, qNextSize = 0;                       // queue size per distance
    short int distance = 0;                                 // distance to current cell
    char dx[4] = {0, 1, 0, -1};                             // up, right, down, left
    while (qRead != qWrite && !(*dir)[y1][x1]) {            // until queue empty (fail) or target reached
        char x = queue[qRead++], y = queue[qRead++];        // take oldest cell from queue
        qRead %= 152;                                       // wrap-around queue pointer
        for (char i = 0; i < 4; i++) {                      // check 4 neighbouring cells
            char nx = x + dx[i], ny = y + dx[3 - i];        // coordinates of neighbouring cell
            if (nx >= 0 && nx < 32 && ny >= 0 && ny < 16    // coordinates not off-grid
            && !grid[ny][nx] && !(*dir)[ny][nx]) {          // traversable unvisited cell
                (*dir)[ny][nx] = i + 1;                     // store direction 1-4
                queue[qWrite++] = nx; queue[qWrite++] = ny; // put cell in queue
                qWrite %= 152;                              // wrap-around queue pointer
                ++qNextSize;                                // increment queue size for next distance
            }
        }
        if (!--qCurSize || (*dir)[y1][x1]) {                // current distance done or target reached
            qCurSize = qNextSize;                           // switch to distance + 1
            qNextSize = 0;
            ++distance;
        }
    }
    free(queue);                                            // free up queue memory for path
    if (!(*dir)[y1][x1]) distance = -1;                     // no path found
    else {                                                  // path found
        *path = malloc(distance * 2 + 2);                   // allocate path array: 2 bytes per step
        (*path)[0] = x1; (*path)[1] = y1;                   // starting position (forward)
        for (short int i = 1; i <= distance; i++) {         // retrace steps
            char d = (*dir)[y1][x1] - 1;                    // direction of previous step 0-3
            x1 -= dx[d]; y1 -= dx[3 - d];                   // go back to previous position
            (*path)[i * 2] = x1; (*path)[i * 2 + 1] = y1;   // add cell to path
        }
    }
    free(*dir);                                             // discard direction matrix
    return distance + 1;                                    // return number of cells in path
}

int main() {
    char grid[][32] = // max queue size: 76
        {{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
         {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}};
    char x1 = 31, y1 = 0, x2 = 16, y2 = 7, *path = NULL;
    short int steps = findPath(grid, x1, y1, x2, y2, &path);
    // do stuff
    free(path);                                             // discard path array

    return 0;
}

【讨论】:

  • @AleksandurMurfitt 我看到你没有接受这个答案并接受了我的另一个答案。您发现此方法有问题吗?
猜你喜欢
  • 2017-06-21
  • 1970-01-01
  • 1970-01-01
  • 2013-03-08
  • 1970-01-01
  • 1970-01-01
  • 2016-07-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多