首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >宽度优先搜索效率

宽度优先搜索效率
EN

Stack Overflow用户
提问于 2014-08-10 21:49:54
回答 3查看 823关注 0票数 1

在无界/无限国际象棋棋盘上,计算从x1、y1到x2、y2所需的最少跳数,最有效的方法是什么?假设从x1,y1,我们总是可以生成一组合法的动作。

这听起来是为BFS量身定做的,我已经成功地实现了一个。但是如果x2,y2是任意大的话,它的空间和时间复杂性似乎是非常可怕的。

我一直在研究其他各种算法,如A*、双向搜索、迭代深化DFS等,但到目前为止,我还不知道哪种方法会产生最优(和最完整)的解决方案。有没有什么我缺少的洞察力?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2014-08-11 12:45:47

为了扩展评论中的讨论,像宽度优先搜索(BFS)这样的不知情搜索将找到最优解(最短路径)。然而,它只考虑到到目前为止节点g(n)的成本,并且它的成本随着从源到目标的距离呈指数增长。为了驯服搜索的成本,同时仍然确保搜索找到最优的解决方案,您需要通过启发式的h(n)将一些信息添加到搜索算法中。

您的情况非常适合A*搜索,其中启发式是从节点到目标(x2,y2)距离的度量。你可以用欧几里得距离“就像乌鸦飞”,但是当你在考虑骑士的时候,曼哈顿的距离可能更合适。无论您选择什么度量,它必须小于(或等于)从节点到目标的实际距离,以便搜索到最优解(在这种情况下,启发式被称为“可接受”)。请注意,您需要将每个距离除以一个常数,以使其低估移动:曼哈顿距离除以3,欧几里得距离除以sqrt(5) (sqrt(5)是2乘1平方的对角线长度)。

当您运行该算法时,您将估计来自我们已经到达的任何节点的f(n)的总距离,即到目前为止的距离加上启发式距离。即f(n) = g(n) + h(n)g(n)是从(x1,y1)到节点n的距离,h(n)是从节点n(x2,y2)的估计启发式距离。给定您需要的节点,您总是选择f(n)最低的节点f(n)。我喜欢你的说法:

维护一个由g(n) + h(n)命令签出的节点优先级队列。

如果启发式是可接受的,则该算法找到最优解,因为次优路径永远不可能位于优先级队列的前面:最优路径的任何片段总距离总是较低的(其中,总距离是产生距离加启发式距离)。

我们在这里选择的距离度量是单调的(即,随着路径的延长而不是向上或向下增加)。在这种情况下,可以证明它是有效的。如往常一样,请参阅维基百科或网络上的其他来源以获得更多详细信息。科罗拉多州立大学网页特别好,并且对最优性和效率进行了很好的讨论。

举一个从(-200,-100)到(0,0)的例子,这相当于(0,0)到(200,100)的例子,在我的实现中,我们看到的曼哈顿启发式如下

该实现进行了过多的搜索,因为使用启发式的h =曼哈顿距离,跨1向上2的步骤似乎与跨2向上1的最佳步骤一样好,即f()值没有区分两者。然而,该算法仍能找到100次移动的最优解。它需要2118个步骤,这仍然比广度优先搜索要好得多,后者像墨迹一样扩散开来(我估计它可能需要20000到30000步)。

如果你选择h =欧几里得距离,它是怎么做的?

这好多了!它只需要104步,而且它做得很好,因为它结合了我们的直觉,你需要向大致正确的方向前进。但是在我们祝贺自己之前,让我们尝试另一个例子,从(-200,0)到(0,0)。这两种启发式方法都可以找到长度为100的最佳路径。欧几里得启发式需要12171步来寻找最优路径,如下所示。

而曼哈顿的启发式则要走16077步

抛开曼哈顿的启发式更糟糕的事实相比,我认为真正的问题是有多条最优路径。这并不奇怪:最优路径的重新排序仍然是最优的。通过按照@Sneftel的答案以数学形式重新处理这个问题,这一事实就会自动得到考虑。

总之,与BFS相比,具有容许启发式的A*产生的最优解比BFS更有效,但很可能存在更有效的解。A*是一种很好的默认算法,在这种情况下,您可以很容易地找到一个距离启发式算法,虽然在这种情况下,它并不是最好的解决方案,但是通过实现它可以了解很多关于这个问题的知识。

请按您的要求在C++中编写下面的代码。

代码语言:javascript
运行
复制
#include <memory>
using std::shared_ptr;
#include <vector>
using std::vector;
#include <queue>
using std::priority_queue;
#include <map>
using std::map;
using std::pair;
#include <math.h>

#include <iostream>
using std::cout;
#include <fstream>
using std::ofstream;

struct Point
{
    short x;
    short y;
    Point(short _x, short _y) { x = _x; y = _y; }
    bool IsOrigin() { return x == 0 && y == 0; }
    bool operator<(const Point& p) const {
        return pair<short, short>(x, y) < pair<short, short>(p.x, p.y);
    }
};

class Path
{
    Point m_end;
    shared_ptr<Path> m_prev;
    int m_length; // cached

public:
    Path(const Point& start)
        : m_end(start)
        { m_length = 0; }

    Path(const Point& start, shared_ptr<Path> prev)
        : m_end(start)
        , m_prev(prev)
    { m_length = m_prev->m_length +1; }

    Point GetEnd() const { return m_end; }
    int GetLength() const { return m_length; }

    vector<Point> GetPoints() const
    {
        vector<Point> points;
        for (const Path* curr = this; curr; curr = curr->m_prev.get()) {
            points.push_back(curr->m_end);
        }
        return points;
    }

    double g() const { return m_length; }
    //double h() const { return (abs(m_end.x) + abs(m_end.y)) / 3.0; } // Manhattan
    double h() const { return sqrt((m_end.x*m_end.x + m_end.y*m_end.y)/5); } // Euclidian
    double f() const { return g() + h(); }
};

bool operator<(const shared_ptr<Path>& p1, const shared_ptr<Path>& p2)
{
    return 1/p1->f() < 1/p2->f(); // priority_queue has biggest at end of queue
}

int main()
{
    const Point source(-200, 0);
    const Point target(0, 0);

    priority_queue<shared_ptr<Path>> q;
    q.push(shared_ptr<Path>(new Path(source)));

    map<Point, short> endPath2PathLength;
    endPath2PathLength.insert(map<Point, short>::value_type(source, 0));

    int pointsExpanded = 0;
    shared_ptr<Path> path;
    while (!(path = q.top())->GetEnd().IsOrigin())
    {
        q.pop();
        const short newLength = path->GetLength() + 1;
        for (short dx = -2; dx <= 2; ++dx){
            for (short dy = -2; dy <= 2; ++dy){
                if (abs(dx) + abs(dy) == 3){
                    const Point newEnd(path->GetEnd().x + dx, path->GetEnd().y + dy);
                    auto existingEndPath = endPath2PathLength.find(newEnd);
                    if (existingEndPath == endPath2PathLength.end() ||
                        existingEndPath->second > newLength) {
                        q.push(shared_ptr<Path>(new Path(newEnd, path)));
                        endPath2PathLength[newEnd] = newLength;
                    }
                }
            }
        }
        pointsExpanded++;
    }

    cout<< "Path length " << path->GetLength()
        << " (points expanded = " << pointsExpanded << ")\n";

    ofstream fout("Points.csv");
    for (auto i : endPath2PathLength) {
        fout << i.first.x << "," << i.first.y << "," << i.second << "\n";
    }

    vector<Point> points = path->GetPoints();
    ofstream fout2("OptimalPoints.csv");
    for (auto i : points) {
        fout2 << i.x << "," << i.y << "\n";
    }

    return 0;
}

注意,这不是很好的测试,所以很可能会有but,但我希望总体思路是明确的。

票数 1
EN

Stack Overflow用户

发布于 2014-08-11 04:10:15

我还没有一个完整的证据,但我相信,如果x1,y1和x2,y2在两个方向上都很遥远,那么任何最优的解决方案都会有许多直接向x2和直接向y2移动的移动(2种可能的L型移动,向这个方向移动)。例如,如果当前位置x接近x2,但当前位置y远离y2,则在向y2移动两个方格的两个移动之间交替进行。类似地,如果y接近y2,那么x和x2是很远的。然后,只要到x2的垂直距离和水平距离都小于一些相当小的阈值(可能是5或10),那么就必须用BFS或其他方法来解决问题,才能得到最优解,并且您得到的解应该是最优的。当我有证据时,我会更新我的答案,但我几乎肯定这是真的。如果是这样的话,这意味着无论x1、y1和x2之间有多远,基本上只要解决水平和垂直距离为5或10的问题,就可以快速完成。

票数 2
EN

Stack Overflow用户

发布于 2014-08-11 13:04:43

如果合法移动集独立于当前空间,那么作为整数线性规划(ILP)问题,这似乎是理想的。您基本上可以解决每种类型移动的数量,这样移动的总数就会最小化。例如,对于被限制只能向上移动和向右移动的骑士(因此每一次移动都是x+=1, y+=2x+=2, y+=1 ),您可以将a1+a2最小化,但前提是2*a1+a2 == x2-x1, a1+2*a2 == y2-y1, a1 >= 0, a2 >= 0。虽然ILPs通常是NP-完全的,但我希望一个标准爬山算法能够相当有效地解决它。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/25233353

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档