拼图游戏N数码问题AI算法实现
人工智能,拼图复原,Astar算法,N数码问题,图搜索2016-08-08
最近写了一个5*5拼图游戏的复原算法,拼图复原问题又叫N数码问题,由于搜索空间巨大,可以用来作为测试算法效率的工具。
先放上效果图(图片大小700K,加载可能比较慢):
这个是用javascript做的一个算法的图形演示版,整个算法刚开始是用c++完成的,为了让算法的效果更生动所以用把它移植到了javascript上,这篇博客主要讲用c++的实现方法,js与c++的差别会在讲解中提到。以下是项目地址:
js版本:https://github.com/chuyangLiu/NPuzzle-AI
c++版本:https://github.com/chuyangLiu/DataStructureAndAlgorithm(这个版本库存放了许多算法与数据结构,拼图算法是其中一个,源文件是NPuzzle.h和NPuzzle.cpp)
/* N-Puzzle node definition. The value of the node must be like this: {1, 2, 3, 4, 5, 6, 7, 8, 0} (row = 3, col = 3) Value 0 indicates the empty grid in the puzzle. */ class NPuzzleNode { public: ~NPuzzleNode(); /* Initialize the node. @param val_ the node value @param row_ the row number @param col_ the column number */ NPuzzleNode(); NPuzzleNode(const std::vector<int> &val_, const int row_, const int col_); /* Get the node value. */ const std::vector<int>& getVal() const; /* Get the adjacent node at the direction. Precondition: current node can move to the direction @param direc the direction @return the adjacent node pointer (make sure to delete because the pointer is allocated dynamically) */ NPuzzleNode* getAdjNode(const Direction &direc) const; /* Move the empty grid toward the direction @param direc the direction */ void move(const Direction &direc); /* Check whether the empty grid can move toward the direction @param direc the direction @return true if can move, false otherwise */ bool canMove(const Direction &direc) const; /* Randomly rearrange the node value. (Make sure a solution exists) */ void shuffle(); /* Get the row number of the index @param i the index @return the row number (range: [0, row - 1]) */ int getRow(const int &i) const; /* Get the column number of the index @param i the index @return the column number (range: [0, col - 1]) */ int getCol(const int &i) const; /* Get the element numbers of the node value. */ int getSize() const; /* Get the string description of the node value. @return the string description */ std::string toString() const; /* Hash function for a node @return the hash value of the node */ unsigned long long hash() const; /* Check if two nodes equal. */ bool operator==(const NPuzzleNode &a) const; /* Compare two nodes by their f value. */ bool operator<(const NPuzzleNode &a) const; bool operator>(const NPuzzleNode &a) const; bool operator<=(const NPuzzleNode &a) const; bool operator>=(const NPuzzleNode &a) const; /* Getters and setters for fields used in searching algorithm. */ void setG(const int g_); void setH(const int h_); void setParent(NPuzzleNode* p); void setDirection(const Direction &d); int getG() const; int getH() const; int getF() const; NPuzzleNode* getParent() const; Direction getDirection() const; private: std::vector<int> val; int emptyPos = -1; int row = 0; int col = 0; int g = 0; // The distance from the beginning node to the current node int h = 0; // The distance from the current node to the goal node (heuristic) NPuzzleNode* parent = nullptr; // Parent node pointer Direction direc = NONE; // Direction from parent node to this node /* Initialize the node value. @param val_ the node value @throw range_error if the node value is not valid */ void init(const std::vector<int> &val_); };与节点相关的大部分操作比较简单,这里只列出头文件,源码可以从以下地址获得:
/* Comparator for binary heap */ struct cmpBinaryHeap { bool operator()(const NPuzzleNode *const &a, const NPuzzleNode *const &b) const { return *a <= *b; //return *a > *b; // STD version } }; /* Min-root binary heap declaration. */ typedef BinaryHeap<NPuzzleNode*, cmpBinaryHeap> min_heap; typedef std::priority_queue<NPuzzleNode*, std::vector<NPuzzleNode*>, cmpBinaryHeap> min_heap; // STL version为了提高运行效率,在堆中存放节点的指针,利用一个struct cmpBinaryHeap仿函数(functor)完成对两个节点的F值比较。注意标准库中的priority_queue默认是个大顶堆,所以比较策略需要使用大于才能产生一个小顶堆。
/* Comparator for hash table */ struct cmpHashTable { bool operator()(const NPuzzleNode *const &a, const NPuzzleNode *const &b) const { return *a == *b; } }; /* Hash table declaration. */ typedef HashTable<NPuzzleNode*, cmpHashTable> hash_table; typedef std::unordered_set<NPuzzleNode*,std::function<unsigned long long(const NPuzzleNode *const &)>, cmpHashTable> hash_table; // STL version
int NPuzzle::getEstimate(const NPuzzleNode *const n) const { const auto &val = n->getVal(); const auto &desVal = des.getVal(); const auto &size = n->getSize(); // Number of nodes whose next node is in a wrong position int wrongNext = 0; for (int i = 0; i < size - 1; i++) { if (val[i] + 1 != val[i + 1]) { wrongNext++; } } // Number of nodes which are in a wrong position int wrong = 0; for (int i = 0; i < size; ++i) { if (val[i] != desVal[i]) { ++wrong; } } // Sum up the distance of each element int manhatten = 0, geometric = 0; for (int i = 0; i < size; ++i) { if (val[i]) { // Escape value 0 int curR = n->getRow(i); int curC = n->getCol(i); int desR = n->getRow(val[i] - 1); int desC = n->getCol(val[i] - 1); int dR = curR > desR ? curR - desR : desR - curR; int dC = curC > desC ? curC - desC : desC - curC; manhatten += dR + dC; geometric += (int)(sqrt(dR * dR + dC * dC)); } } return 5 * (1 * wrongNext + 0 * wrong + 2 * manhatten + 1 * geometric); }该函数传入当前节点,然后返回从当前节点到目标节点的估算距离。我一共用了4个估算值,后续节点不正确个数(wrongNext)、放错位置个数(wrong)、所有拼图块到目标块的曼哈顿距离(manhatten)以及所有拼图块到目标块的几何距离(geometric),分别的权重为1、0、2、1,因为测试发现wrong值的效果不是很好,权重就暂时设为了0,前面乘上的系数5是对h值的放大。在目前这种估算值的策略下,算法平均每毫秒搜索70个点,在搜索8000个点左右找到一条长度平均为300的路径。当然,调整这些权重会随时改变算法的效果。
void NPuzzle::run() { searchedCnt = 0; openList.push(&src); while (!openList.empty()) { // Loop until the open list is empty or finding // a node that is not in the close list. NPuzzleNode *cur = nullptr; do { cur = openList.top(); openList.pop(); } while (!openList.empty() && isVisit(cur)); // If all the nodes in the open list is in the // close list, then there is no available path // between the two nodes. if (openList.empty() && isVisit(cur)) { return; } ++searchedCnt; closeList.insert(cur); //printSearchInfo(cur); if (*cur == des) { // Find goal constructPath(cur); freeResources(); return; } for (int i = 1; i <= 4; ++i) { // Traverse adj Direction d = Direction(i); if (cur->canMove(d)) { NPuzzleNode *adj = cur->getAdjNode(d); alloc.push_back(adj); // alloc用于记录所有动态分配的内存,在上面的freeResources()函数中将会一并释放这些内存 if (!isVisit(adj)) { adj->setParent(cur); adj->setDirection(d); adj->setG(cur->getG() + 1); adj->setH(getEstimate(adj)); openList.push(adj); } } } } }算法没有太大变化,基本就是广搜的模板,唯一的区别是每次从open list中拿F值最小的节点时,需要反复弹出队首节点直到该节点未被访问过(不在close list内),因为同一个节点可能入堆多次。
js版本:https://github.com/chuyangLiu/NPuzzle-AI
c++版本:https://github.com/chuyangLiu/DataStructureAndAlgorithm