假设有 n 个修道士和 n 个野人准备渡河,但只有一条能容纳 c 人的小船,为了防止野人侵犯修道士,要求无论在何处,修道士的个数不得少于野人的人数(除非修道士个数为0)。如果两种人都会划船,试设计一个算法,确定他们能否渡过河去,若能,则给出一个完整的渡河方案。
在或图的一般搜索算法中,如果在搜索过程的利用估价函数f(n)=g(n)+h(n)对open表中的节点进行排序,则该搜索算法为A算法。
对A算法中的g(n)和h(n)做出限制:
则算法被称为A*算法。
评估函数:F = G + H
A星算法具有启发策略,在于其可以通过预估H值,降低走弯路的可能性,更容易找到一条更短的路径。其他不具有启发策略的算法,没有做预估处理,只是穷举所有可通过路径,然后从中挑选出一条最短的路径。
在这个问题中,需要考虑:
已知传教士和野人数:N(两者默认相同),船的最大容量:K。定义M:左岸传教士人数。C:左岸野人人数。B:左岸船个数。可用一个三元组来表示左岸状态,即S=(M, C, B)。将所有扩展的节点和原始节点存放在同一列表中。初始状态为(N, N, 1),目标状态为(0, 0, 0)。问题的求解转换为在状态空间中,找到一条从状态(N, N, 1)到状态(0, 0, 0)的最优路径。
评估函数建立:f = d + h = d + M + C - 2*B。
通过减少和增加修道士或也认得数量来拓展节点。当船在左岸时,向右岸移动,需要减少修道士和野人的数量;当船在右岸时,向左岸移动,需要增加修道士和野人的数量。
定义结构体Node用来存储状态信息,包括左岸修道士和野人的人数,船在左岸还是右岸,当前状态的深度,当前状态到预估状态的预估代价,搜索总代价,指向下一结点的指针和指向父状态的指针。具体定义如下:
struct Node
{
int m_M; // 左岸的修道士人数
int m_C; // 左岸的野人人数
int m_B; // 1:船在左岸;0:船在右岸
int m_d; // 从开始状态到当前状态的代价,这里表示:结点深度
int m_h; // 当前状态到结束状态的预估代价,h = M + C -2*B;
int m_f; // 搜索总代价,f = d + h = d + M + C - 2*B;
Node* next; // 下一节点
Node* father; // 当前状态的父状态
public:
Node();
Node(int, int, int, int, Node*);
~Node();
};
起始状态为(N, N, 1), 每次从Open表中取出第一个结点,寻找可能的下一状态,并将当前结点加入Close表中。如果到达目标状态(0, 0, 0)时,搜索结束,输出生成结点数和搜索结点数。否则寻找当前结点的所有可能的下一状态。当整个Open表为空时,搜索结束。
while (m_Open->next != nullptr)
{
Node* current = m_Open->next;
m_Open->next = current->next;
AddLinkList(m_Close, current);
if (current->m_M == 0 && current->m_C == 0 && current->m_B == 0)
{
std::cout << "搜索成功!生成节点:" << m_creatPoint << ",搜索节点:" << m_searchPoint << std::endl;
m_endPoint = current;
return true;
}
GetNext(current);
}
为了便于存放生成的状态信息和已访问的状态信息,在MC类中设计了两个单链表,m_Open和m_Close。分别用于存放所有合法且没有访问的状态和所有已访问过的状态。
Node* m_Open;
Node* m_Close;
为了能够方便寻找Open表中搜索代价最小的状态,通过头插法,设计了一带头的升序单链表。在插入新结点时,如果到了链表尾部的,则直接在当前结点后面插入;否则与当前节点的下一结点比较,如果待插入结点的搜索代价小于当前结点下一结点的搜索代价,则将该结点插入当前结点后面,否则指链表的指针向下一结点移动。
void MC::AddLinkList(Node* LinkList, Node* node)
{
Node* p = LinkList;
while (p)
{
if (p->next == nullptr)
{
p->next = node;
node->next = nullptr;
return;
}
else if (node->m_f < p->next->m_f)
{
node->next = p->next;
p->next = node;
return;
}
else p = p->next;
}
}
利用Node结点指向父结点进行递归倒序输出。
void MC::Output(Node* node)
{
if (node == nullptr) return;
if (node->father != nullptr) Output(node->father);
std::cout << "(" << node->m_M << ", " << node->m_C << ", " << node->m_B << ")" << std::endl;
}
分配4个线程进行搜索,当某个线程搜索到结果时,while循环中的判断条件为真,搜索便完成。
bool MultiSearch()
{
m_Open->next = new Node(m_M, m_C, 1, 0, nullptr);
m_Open->next->next = nullptr;
int N = 0;
const int ThSize = 4;
std::thread th[ThSize];
for (int i = 0; i < ThSize; ++i) th[i] = std::thread(MultiGetNext);
for (int i = 0; i < ThSize; ++i) th[i].detach();
while (true) if (m_endPoint != nullptr) return true;
}
C++20个结点单线程
C++20结点多线程
C++1000个结点单线程
C++1000个结点多线程
在多线程的程序中,分配了4个线程。从上图中不难发现,当搜索结点比较少时,多线程相较于单线程的运行优势体现的并不明显。这一方面是由于数据量比较小,运行时间都比较短;另一方面由于线程的创建和销毁都需要一定的时间,此外在多线程程序中,对共享数据需要互斥访问,对数据上锁和解锁的操作也耗费了一定的时间。当数据量比较大时,多线程的速度优势就体现出来了。