1. A星算法解决修道士与野人问题

A星算法解决修道士与野人问题


1. 运行环境

  • CPU:I5-10400
  • 内存:16GB
  • 系统:Win10 64位专业版,20H2
  • IDE:Vistual Studio 2019专业版

2. 问题描述

假设有 n 个修道士和 n 个野人准备渡河,但只有一条能容纳 c 人的小船,为了防止野人侵犯修道士,要求无论在何处,修道士的个数不得少于野人的人数(除非修道士个数为0)。如果两种人都会划船,试设计一个算法,确定他们能否渡过河去,若能,则给出一个完整的渡河方案。

3. 算法简介

3.1 A算法的基本原理分析

在或图的一般搜索算法中,如果在搜索过程的利用估价函数f(n)=g(n)+h(n)对open表中的节点进行排序,则该搜索算法为A算法。

  • g(n):从初始节点到n的实际代价。因为n为当前节点,搜索已达到n点,所以g(n)可计算出。
  • h(n):启发函数,从n到目标节点的最佳路径的估计代价。因为尚未找到解路径,所以h(n)仅仅是估计值。

对A算法中的g(n)和h(n)做出限制:

  • g(n) >= g(n)(g*(n)为S0到n的最小费用)
  • h(n) <= h*(n)(h*(n)为n到Sg的实际最小费用)

则算法被称为A*算法。

3.2 评估函数

评估函数:F = G + H

  • F:搜索的总代价。
  • G:开始状态到当前状态的代价。
  • H:当前状态到结束状态的预估代价。

A星算法具有启发策略,在于其可以通过预估H值,降低走弯路的可能性,更容易找到一条更短的路径。其他不具有启发策略的算法,没有做预估处理,只是穷举所有可通过路径,然后从中挑选出一条最短的路径。

3.3 算法流程

  1. 将初始节点S0放入Open表中。
  2. 建立Close表,初始值为空。
  3. 如果Open表为空,则问题无解,退出。
  4. 从Open表中取出第一个节点n,并将n放入Close表中。
  5. 考察节点n是否是目标节点。如果是,则得到了问题的解,解的路径通过n到S0的指针获得,退出。
  6. 如果节点n不可拓展,则转到第2步。
  7. 扩展节点n,将子节点放入Open表中,并为每个子节点分配指向父节点的指针。
  8. 将Open表中的节点按照评估函数F值从小到大的顺序重新进行排序。
  9. 转向第3步。

4. 解决思路

4.1 状态空间表示方式

在这个问题中,需要考虑:

  1. 两岸的传修道士人数和野人人数。
  2. 船在左岸还是在右岸。

已知传教士和野人数:N(两者默认相同),船的最大容量:K。定义M:左岸传教士人数。C:左岸野人人数。B:左岸船个数。可用一个三元组来表示左岸状态,即S=(M, C, B)。将所有扩展的节点和原始节点存放在同一列表中。初始状态为(N, N, 1),目标状态为(0, 0, 0)。问题的求解转换为在状态空间中,找到一条从状态(N, N, 1)到状态(0, 0, 0)的最优路径。

4.2 评估函数

评估函数建立:f = d + h = d + M + C - 2*B。

  • M:左岸修道士的人数。
  • C:左岸野人的人数。
  • B:取0或1。0表示船在右岸,1表示船在左岸。
  • d:表示状态搜索深度,每次从当前状态搜索到下一状态时,下一状态在当前状态的深度基础上加1,初始状态的深度为0。

4.3 节点拓展

通过减少和增加修道士或也认得数量来拓展节点。当船在左岸时,向右岸移动,需要减少修道士和野人的数量;当船在右岸时,向左岸移动,需要增加修道士和野人的数量。

4.4 状态判断

  1. 左岸修道士人数等于总数或者为零:C>=0, C<=N。
  2. 左岸修道士人数在(0, N)之间时:C >=0 , M >= C , M <= N , C <= N , N-M >= N-C。
  3. 其他状态都不合法。

5. 关键代码

5.1 状态设计

定义结构体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();
};

5.2 A*算法主要设计

起始状态为(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);
}

5.3 链表设计

为了便于存放生成的状态信息和已访问的状态信息,在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;
	}
}

5.4 输出设计

利用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;
}

5.5 多线程设计

分配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;
}

6. 运行结果与分析

C++20个结点单线程
在这里插入图片描述
C++20结点多线程
在这里插入图片描述
C++1000个结点单线程
在这里插入图片描述

C++1000个结点多线程
在这里插入图片描述
在多线程的程序中,分配了4个线程。从上图中不难发现,当搜索结点比较少时,多线程相较于单线程的运行优势体现的并不明显。这一方面是由于数据量比较小,运行时间都比较短;另一方面由于线程的创建和销毁都需要一定的时间,此外在多线程程序中,对共享数据需要互斥访问,对数据上锁和解锁的操作也耗费了一定的时间。当数据量比较大时,多线程的速度优势就体现出来了。

  • 0
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值