拼图游戏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 versionint 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