A*算法求解八数码--演示程序(MFC)

首先看看程序的运行效果

A*算法求解八数码--演示程序(MFC)_第1张图片

一、程序功能:

1、单步或者连续演示用A*解决八数码或者十五数码难题的步骤,可暂停。

2、随机生成初始状态。

3、调节演示速度(加快与减慢)

4、open表与close表对应存储的为未扩展结点和已扩展的结点。

5、演示完毕,显示最优路径。

二、预备知识

1、八数码与A*算法

A*算法:A*算法是一种有序搜索算法,其特点在于对估价函数的定义上。对于一般的有序搜索,总是选择f值最小的节点作为扩展节点。因此,f是根据需要找到一条最小代价路径的观点来估算节点的,所以,可考虑每个节点n的估价函数值为两个分量:从起始节点到节点n的代价以及从节点n到达目标节点的代价。

八数码中的代价函数为f(n)=g(n) + w(n)。其中w(n)为搜索深度,目标状态对应的深度为0,由目标状态扩展的节点的状态的深度为1,接着扩展的深度为2,3……等;g(n)为从当前状态到目标状态所需的最小步数。具体代码如下:

//求解从当前状态到目标状态最少需要多少步
int CalWeight(int * a, int n)
{
	int res = 0, x, y, col = sqrt(n + 1);
	for (int i = 0; i <= n; ++i)
	{
		if (a[i] == 0)
			continue;
		for (int j = 0; j <= n; ++j)
		{
			if (EndState[j] == a[i])
			{
				x = j / col;
				y = j % col;
				break;
			}
		}
		
		res = res + abs(i / col - x) + abs(i % col - y);
	}
	return res;
}

 

八数码:在一个3*3的方棋盘上放置着1,2,3,4,5,6,7,8八个数码,每个数码占一格,且有一个空格。这些数码可以在棋盘上移动,其移动规则是:与空格相邻的数码方格可以移入空格。经有限步数或者不可能从起始状态到达目标状态。

 

2、八数码有解判断

逆序数:一个序列中,每个数字的前面比本数字大的数字的个数的总和。

代码如下:

//计算逆序数
int inver(int s[], int number)
{
	int sum = 0;
	for (int i = 0;i<= number;i++)
	{
		if (s[i] == 0) continue;
		for (int j = i - 1;j >= 0;j--)
			if (s[j]>s[i]) sum++;
	}
	return sum;
}

 

若起始状态与目标状态的逆序数的奇偶性一致,则认为两个状态可达,否则不可达。

 

证明见:八数码问题是否可解

3、标记八数码搜索过程中已访问的状态

常用的哈希函数,散列值是通过康托展开得到。但此方法仅限于数字序列小于10,不然映射数字过大。

康托展开与逆展开的代码如下:

//康托展开式及逆展开
static const int FAC[] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880 };   // 阶乘
int cantor(int *a, int n)
{
	int x = 0;
	for (int i = 0; i <= n; ++i) {
		int smaller = 0;  // 在当前位之后小于其的个数
		for (int j = i + 1; j <= n; ++j) {
			if (a[j] < a[i])
				smaller++;
		}
		x += FAC[n - i] * smaller; // 康托展开累加
	}
	return x;  // 康托展开值
}
//康托展开逆运算
void decantor(int x, int * a, int n)
{
	int idx = 0;
	vector v;  // 存放当前可选数
	for (int i = 0;i <= n;i++)
		v.push_back(i);
	for (int i = n;i >= 0;i--)
	{
		int r = x % FAC[i];
		int t = x / FAC[i];
		x = r;
		sort(v.begin(), v.end());// 从小到大排序 
		a[idx++] = v[t];    // 剩余数里第t+1个数为当前位
		v.erase(v.begin() + t);   // 移除选做当前位的数
	}
}

N数码的通用标记方法,我采用的是set去重,数字转成字符串('#'分隔)

//数字转字符串(用'#'分隔)
string NumToStr(int *a, int n)
{
	string res("");
	char ch[5];
	for (int i = 0; i <= n; ++i)
	{
		sprintf(ch, "%d", a[i]);
		res = res + ch;
		res.push_back('#');
	}
	return res;
}

//字符串转数字
void StrToNum(string str, int *a, int n)
{
	int idx = 0, i = 0, tmp = 0, num = 0;
	while (str.find('#', idx) != -1)
	{
		tmp = idx;
		idx = str.find('#', idx);
		num = 0;
		for (int j = tmp; j < idx; ++j)
		{
			num = num * 10 + (str[j] - '0');
		}
		a[i++] = num;
		idx++;
	}
}

三、程序局部功能实现

1、绘制界面,分为三部分:起始状态,移动前状态,移动后状态。代码如下:

//显示三个状态(重绘界面)
//初始状态,移动前,移动后
//运行程序时,若界面显示异常,可尝试电脑的显示设置中"更改文本、应用等项目大小"是否为100%
void CEightFigureDlg::ShowFigure(CDC * dc)
{
	//显示背景
	CDC pdc, ddc;
	pdc.CreateCompatibleDC(dc); // 创建一个临时显示设备
	ddc.CreateCompatibleDC(dc); // 创建一个加载盘子的临时显示设备

	CBitmap bmp, *obmp;
	int step;
	switch (number)
	{
	case 8:
		step = 51;
		break;
	case 15:
		step = 33;
		break;
	}
	bmp.LoadBitmap(IDB_BG1); // 加载背景图片
	obmp = pdc.SelectObject(&bmp); // 将图片显示在设备pdc上。

	
	for (int i = 0, col = sqrt(number + 1); i <= number; i++)
	{
			CBitmap dbmp, *odbmp;
			dbmp.LoadBitmap(IDB_Num0 + StartState[i]); // 加载数字图片
			odbmp = ddc.SelectObject(&dbmp); // 将盘子显示在设备ddc上。
			pdc.BitBlt(0 + (i % col)* step, 0 + (i  / col) * step, 30, 30, &ddc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上
			ddc.SelectObject(odbmp); // 显示完毕,还原设备
	}
	dc->BitBlt(10, 50, 132, 132, &pdc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上
	pdc.SelectObject(obmp); // 显示完毕,还原设备

	obmp = pdc.SelectObject(&bmp); // 将图片显示在设备pdc上。
	for (int i = 0, col = sqrt(number + 1); i <= number; i++)
	{
			CBitmap dbmp, *odbmp;
			dbmp.LoadBitmap(IDB_Num0 + NowState[i]); // 加载数字图片
			odbmp = ddc.SelectObject(&dbmp); // 将盘子显示在设备ddc上。
			pdc.BitBlt(0 + (i % col) * step, 0 + (i / col) * step, 30, 30, &ddc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上
			ddc.SelectObject(odbmp); // 显示完毕,还原设备
	}
	dc->BitBlt(340, 170, 132, 132, &pdc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上

	obmp = pdc.SelectObject(&bmp); // 将图片显示在设备pdc上。
	for (int i = 0, col = sqrt(number + 1); i <= number; i++)
	{
			CBitmap dbmp, *odbmp;
			dbmp.LoadBitmap(IDB_Num0 + NextState[i]); // 加载数字图片
			odbmp = ddc.SelectObject(&dbmp); // 将盘子显示在设备ddc上。
			pdc.BitBlt(0 + (i % col) * step, 0 + (i / col) * step, 30, 30, &ddc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上
			ddc.SelectObject(odbmp); // 显示完毕,还原设备
	}
	dc->BitBlt(560, 170, 132, 132, &pdc, 0, 0, SRCCOPY); // 将ddc拷贝到临时显示设备pdc对应位置上
	//dc->BitBlt(10, 10, 300, 300, &pdc, 0, 0, SRCCOPY); // 将pdc拷贝到程序显示设备dc上
	//pdc.SelectObject(obmp); // 显示完毕,还原设备
}

2、程序中要实现连续,单步,速度可调,可暂停。使用定时器Timer,调节速度,可调整第二个参数的大小。

具体代码如下:

OnTimer(UINT nIDEvent)

//定时器执行函数
void CEightFigureDlg::OnTimer(UINT nIDEvent)
{
	switch (nIDEvent)
	{
	case 602:
		int col = sqrt(number + 1);
		if(!que.empty())
		{
			Node now = que.top();
			que.pop();
			//decantor(now.cantorVal, NowState, number);
			StrToNum(now.str, NowState, number);
			if (CalWeight(NowState, number) == 0)
			{
				//MessageBox(_T("结束了"));
				KillTimer(602);
			}
			
			v2.push_back(now);
			//更新open表
			CListCtrl *List = (CListCtrl *)GetDlgItem(IDC_LIST2);
			int len = m_OpenListCtrl.GetItemCount();//取行数
			CString itemText;
			for (int row = 0;row < len; row++)
			{
				itemText = List->GetItemText(row, 0);
				if (_ttoi(itemText) == now.rank)
				{
					List->DeleteItem(row);
					break;
				}
			}
			//m_OpenListCtrl.DeleteItem();
			//MyDeal(now.rank);
			//更新close表
			CString str;
			int rows = m_ClosedListCtrl.GetItemCount();
			str.Format("%d", now.rank);
			m_ClosedListCtrl.InsertItem(rows, str);
			str.Format("%d", now.depth);
			m_ClosedListCtrl.SetItemText(rows, 1, str);
			str.Format("%d", now.val);
			m_ClosedListCtrl.SetItemText(rows, 2, str);
			int idx, depth, val;
			for (idx = 0; idx <= number; ++idx)
			{
				if (NowState[idx] == 0)
					break;
			}
			int a = idx / col, b = idx % col;
			string oldStr = NumToStr(NowState, number);
			for (int j = 0; j < 4; ++j)
			{
				int x = a + dir[j][0], y = b + dir[j][1];
				if (Judge(x, y))
				{
					swap(NowState[idx], NowState[x * col + y]);//交换
					/*int news = cantor(NowState, number);
					if (!mark[news])
					{
						depth = now.depth + 1;
						val = CalWeight(NowState, number);
						que.push(Node{ ++g_rank, depth, news, val });
						v1.push_back(Node{ g_rank, depth, news, val });
						mark[news] = 1;
					}*/
					string newStr = NumToStr(NowState, number);
					if (!set1.count(newStr))
					{
						depth = now.depth + 1;
						val = CalWeight(NowState, number);
						que.push(Node{ ++g_rank, depth, newStr, val });
						v1.push_back(Node{ g_rank, depth, newStr, val });
						map1[newStr] = oldStr;
						//更新Open表
						CString str;
						int rows = m_OpenListCtrl.GetItemCount();
						str.Format("%d", g_rank);
						m_OpenListCtrl.InsertItem(rows, str);
						str.Format("%d",depth);
						m_OpenListCtrl.SetItemText(rows, 1, str);
						str.Format("%d", val + depth);
						m_OpenListCtrl.SetItemText(rows, 2, str);
						set1.insert(newStr);
					}
					swap(NowState[idx], NowState[x * col + y]);//恢复交换
				}
			}
			
			//m_OpenListCtrl.DeleteAllItems();
			
			/*for (int i = 0; i < v1.size(); ++i)
			{
				CString str;
				str.Format("%d", v1[i].rank);
				m_OpenListCtrl.InsertItem(0, str);
				str.Format("%d", v1[i].depth);
				m_OpenListCtrl.SetItemText(0, 1, str);
				str.Format("%d", v1[i].val);
				m_OpenListCtrl.SetItemText(0, 2, str);
			}*/
			Node next = que.top();
			//decantor(next.cantorVal, NextState, number);
			StrToNum(next.str, NextState, number);
			if (CalWeight(NextState, number) == 0 && !flag1)
			{
				flag1 = false;
				KillTimer(602);
				string str = NumToStr(EndState, number);
				while (map1[str] != "-1")
				{
					v3.push_back(str);
					str = map1[str];
				}
				v3.push_back(str);

				//添加最优路径
				CString str1;
				str1.Format("初始状态: %s", v3.back().c_str());
				m_answer.InsertString(0, str1);
				str1.Format("目标状态: %s", v3[0].c_str());
				m_answer.InsertString(1, str1);
				for (int i = v3.size() - 1, j = 2; i > 1; --i, ++j)
				{
					str1.Format("第%d步:%s->%s", j - 1, v3[i].c_str(), v3[i - 1].c_str());
					m_answer.InsertString(j, str1);
				}
			}
			else if (flag1)
			{
				flag1 = false;
				KillTimer(602);
			}
			Invalidate(FALSE); // 重绘
			//KillTimer(602);
			/*char str[10];
			test(NowState, str, number);
			MessageBox(str);
			test(NextState, str, number);
			MessageBox(str);*/
			
		}
		break;
	}
	CDialog::OnTimer(nIDEvent);
}

加快,减慢

//加快
void CEightFigureDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	if (last > 50)
	{
		last = last - 50;
		UpdateData(FALSE);
		KillTimer(602);
		SetTimer(602, last, NULL);
	}
}

//减慢
void CEightFigureDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
	if (last < 1000)
	{
		last = last + 50;
		UpdateData(FALSE);
		KillTimer(602);
		SetTimer(602, last, NULL);
	}
}

随机生成

打乱数组,使用random_shuffle(),并且判断打乱后的状态是否可解IsSolve()

//随机生成初始状态
void CEightFigureDlg::OnBnClickedRandomButton()
{
	// TODO: 在此添加控件通知处理程序代码
	flag = false;
	random_shuffle(StartState, StartState + number + 1);
	while(!IsSolve(StartState, number))
		random_shuffle(StartState, StartState + number + 1);
	Init();//初始化
	for (int i = 0; i <= number; ++i)
	{
		NowState[i] = NextState[i] = StartState[i];
	}
	Invalidate(FALSE); // 重绘

}

四、参考工程

点击打开链接

五、初充

经博友提醒:发现N数码是否有解写的有问题

需要修改的代码为:

//判断八数码是否有解(初始状态与目标状态的逆序数奇偶性相同)
bool CEightFigureDlg::IsSolve(int *a, int number)
{
	int num1, num2;
	num1 = inver(a, number);
	num2 = inver(EndState, number);
	return (num1 % 2) == (num2 % 2);
}

需要增加Number奇偶性的判断 参考博客

N为奇数时,初始状态与指定状态逆序数奇偶性相同即有解;N为偶数时,先计算出从初始状态到指定状态,空位要移动的行数m,如果初始状态的逆序数加上m与指定状态的逆序数奇偶性相同,则有解。

代码不在此写了,可以自行修改。

 

你可能感兴趣的:(MFC)