回溯数独

前面有个文章介绍了回溯算法的一般流程和模板,并套用来解决了全排列问题,其实这个模板可以套用来解决很多问题,比如本文要介绍的数独。


数独(sudoku)想来大家都不会陌生,下面是一个号称非常难的数独,我们看看用回溯算法解决它需要多少时间。



和全排列一样,使用回溯时首先要设计一个状态类,对于数独而言,这个状态就是这个9×9的格子盘,另外,对于每个格子,我们也抽象出来一个Grid类,具体做啥用,下面会提到。

class Grid
{
public:
	Grid(){}
	int val; //当为0时,grid为空格,非0时,val为已填的数字
	int nRemainCount; // 还有几个数可以填

	//记录该grid不能再填的数字
	map<int, int> valMap;

	bool Conflict(int val)
	void IncCount(int _val)
	void DecCount(int _val);
};

class SUDOKUState
{
public:
	SUDOKUState(int a[TEMPLATE_SIZE][TEMPLATE_SIZE]);
	bool CanPlace(int val);
	void RemoveNumber(int val)
	void PlaceNumber(int val);
	bool IsFinal();

	Grid m_grids[TEMPLATE_SIZE][TEMPLATE_SIZE];
	std::stack<pair<int,int> > posTrace;     //记录放置的位置记录
	int m_curX;//当前要放置数字的空格位置
        int m_curY;
        int m_nRemained; //还有多少个要放置
        bool IsDead();
        void DecideNextPlace()};

回溯模板还是和全排列差不多

void Solve(SUDOKUState& state)
{
	if (bSolved)
	{
		return;
	}
	//cout << gCount++ << endl;
	if (state.IsFinal())
	{
		state.PrintBoard();
		bSolved = true;
		return;
	}
	if (state.IsDead())
	{
		return;
	}

	for (int i = 1; i < 10; i++)
	{
		if (!state.CanPlace(i))
		{
			continue;
		}
		state.PlaceNumber(i);
		Solve(state);
		state.RemoveNumber(i);
	}
}

我们先来看一下数独的状态如何扩展。


初始状态时,已经填了17个格子,那么还有m_nRemained = 81 - 17 = 64个格子没填,m_nRemained这个变量为0时说明状态节点已经是终节点,也即找到了一个数独的解,这里我们不需要把所有解都输出来,所以到找到一个解时,可以设定一个全局参数bSolved为true, 其他节点再扩展时直接返回。


扩展节点时,我们犯愁了,数独未填的格子中我们究竟选哪个填呢?嗯,最简单的做法是随机选一个空格,然后看看这个空格可以填哪些数,比如第二行第七列的空格就只能填4或者9,只能扩展两个节点,而第一行第一列的空格可以填3,4,5,6,8,9六个数。


于是问题就来了,如果我们随机选择填的空格,倘若该空格的候选数字比较多,那么待扩展的节点也会比较多,搜索空间会大很多。这种情况下能不能找到解呢?答案是可以的,但是也许要跑好几天,我一开始试了一下随机选择要填的空格,结果递归调用了1000多万次都没见半点要结束的样子。


这时需要引入一个启发式的方法,即下一步要选择哪个空格填数,按照数独玩家的经验,当然是填候选填数最少的那个空格,比如第二行第7列那个,只有2个备选数字4和7, 状态中的函数DecideNextPlace即是这一贪心方法的实现:

void DecideNextPlace()
{
	if (m_nRemained == 0)
	{
		return;
	}
	int minv = 10000;
	for (int r = 0; r < TEMPLATE_SIZE; r++)
	{
		for (int c = 0; c < TEMPLATE_SIZE; c++)
		{
			if (m_grids[r][c].val == 0 && m_grids[r][c].nRemainCount < minv)
			{
				minv = m_grids[r][c].nRemainCount;
				m_curX = c; 
				m_curY = r;
			}
		}
	}
	//有一个空格子已经没有数可以选择了
	if (minv == 0)
	{
		m_curX = -1;
		m_curY = -1;
	}
}

这个方法比较简单,遍历所有空格,看看哪个空格还能用的数字最少,就选哪个空格,m_curX和m_curY分别记录了选中空格的行列,接下来放数字就放在这个格子里头了。

为了快速地获取每个格子的备选数字,我在抽象出来的Grid类中维护了一个map, 用来统计该格子g的同行同列以及同section(3×3的那个子区域)的每个数字的出现次数,显然

如果某个数字的出现次数大于1,那么这个数字就不能在g中出现了,反之可以出现。于是CanPlace可以调用当前要放的空格g的Conflict方法:

bool Conflict(int val)
{
	return valMap.find(val) != valMap.end();
}


此时一切都很顺利,但麻烦来了,每个格子的map的维护可别忘了,当空格g中填入数字a时,不仅g的map需要更改,它的同行同列同sec的所有格子也受影响,于是SUDUKUState的PlaceNumber实现比较麻烦,如下:

void PlaceNumber(int val)
{
	m_grids[m_curY][m_curX].val = val;
	m_grids[m_curY][m_curX].IncCount(val);
	for (int r = 0;r < TEMPLATE_SIZE;r++)
	{
                if (r != m_curY)
		{
			m_grids[r][m_curX].IncCount(val);
		}
	}
	for (int c = 0 ; c < TEMPLATE_SIZE; c++)
	{
		if (c != m_curX)
		{
			m_grids[m_curY][c].IncCount(val);
		}
	}

	for (int r = (m_curY / 3) * 3; r < (m_curY / 3) * 3 + 3; r ++)
	{
		for (int c = (m_curX / 3) * 3; c < (m_curX / 3) * 3 + 3; c++)
		{
			if (r == m_curY || c == m_curX)
			{
				continue;
			}
			m_grids[r][c].IncCount(val);
		}
	}
	m_nRemained --;
	posTrace.push(make_pair<int,int>(m_curX, m_curY));
	DecideNextPlace();
}

这里受影响的格子都调用自身的IncCount方法,表示val这个数的出现又加1了。

void IncCount(int _val)
{
	valMap[_val]++;
	nRemainCount = 9 - valMap.size();
}


同样,当节点扩展完后,需要恢复原来的状态,此时需要执行反动作,即把之前放的空格中的数字拿掉,此时有个问题出现了,之气的空格是哪个呢?由于每次PlaceNumber后,都会调用DecideNextPlace,所以m_curX和m_curY已经不再是原来的选中空格位置,为了解决这个问题,我在状态类中加了一个栈posTrace,用来维护已经放过了的位置信息,那么栈顶即为上一步放数的格子。有了此信息后,我们只要再调用有关格子的DecCount方法,并将posTrace出栈,更新当前空格位置,就可以恢复到原来的状态,因此RemoveNumber方法与PlaceNumber完全是反着来:

void RemoveNumber(int val)
{
	assert(!posTrace.empty());
	pair<int, int> prePos = posTrace.top();

	m_curX = prePos.first;
	m_curY = prePos.second;
	m_grids[m_curY][m_curX].val = 0;
	m_grids[m_curY][m_curX].DecCount(val);
	for (int r = 0;r < TEMPLATE_SIZE;r++)
	{
		if (r != m_curY)
		{
			m_grids[r][m_curX].DecCount(val);
		}
	}
	for (int c = 0 ; c < TEMPLATE_SIZE; c++)
	{
		if (c != m_curX)
		{
			m_grids[m_curY][c].DecCount(val);
		}
	}

	for (int r = (m_curY / 3) * 3; r < (m_curY / 3) * 3 + 3; r ++)
	{
		for (int c = (m_curX / 3) * 3; c < (m_curX / 3) * 3 + 3; c++)
		{
			if (r == m_curY || c == m_curX)
			{
				continue;
			}
			m_grids[r][c].DecCount(val);
		}
	}
	posTrace.pop();
	m_nRemained ++;
}


最后需要说明的是,SUDOKUState中有个IsDead方法

bool IsDead()
{
	return m_curY == -1 && m_curY == -1;
}
当我们在调用DecideNextStep时,如果发现已经没有空格可以填数了,此时我们对m_curX和m_curY设了个特殊值,以此来表示当前节点是一个死节点,可以提前返回了,这其实是剪枝技术的应用。


这个程序非常快,一共调用了10374次Solve, vs2005 release下只花了52ms



完整代码如下:

//#include "stdafx.h"
#include <map>
#include <stack>
#include <cassert>
//#include "..\Utility\GFClock.h"
using namespace std;
const int TEMPLATE_SIZE = 9;
const int SUB_SIZE = 3;
bool bSolved = false;
int gCount = 0;
class Grid
{
public:
	Grid(){}
	int val;
	int nRemainCount; // 还有几个数可以填

	//记录该grid不能再填的数字
	map<int, int> valMap;
	bool Conflict(int val)
	{
		return valMap.find(val) != valMap.end();
	}

	//自身或其他地方填了数字影响了当前格子可以选择的数字集合
	void IncCount(int _val)
	{
		valMap[_val]++;
		nRemainCount = 9 - valMap.size();
	}
	void DecCount(int _val)
	{
		valMap[_val]--;
		if (valMap[_val] == 0)
		{
			valMap.erase(_val);
		}
		nRemainCount = 9 - valMap.size();
	}
};


int board[TEMPLATE_SIZE][TEMPLATE_SIZE] = {
	{0, 0, 0, 0, 0, 0, 0, 1, 2},
	{0, 0, 0, 0, 3, 5, 0, 0, 0},
	{0, 0, 0, 6, 0, 0, 0, 7, 0},
	{7, 0, 0, 0, 0, 0, 3, 0, 0},
	{0, 0, 0, 4, 0, 0, 8, 0, 0},
	{1, 0, 0, 0, 0, 0, 0, 0, 0},
	{0, 0, 0, 1, 2, 0, 0, 0, 0},
	{0, 8, 0, 0, 0, 0, 0, 4, 0},
	{0, 5, 0, 0, 0, 0, 6, 0, 0}
};
class SUDOKUState
{
public:

	bool CanPlace(int val)
	{
		return !m_grids[m_curY][m_curX].Conflict(val);
	}
	bool IsFinal()
	{
		return m_nRemained == 0;
	}

	bool IsDead()
	{
		return m_curY == -1 && m_curY == -1;
	}
	//将第y行,第x列的数字挪去
	void RemoveNumber(int val)
	{
		assert(!posTrace.empty());
		pair<int, int> prePos = posTrace.top();

		m_curX = prePos.first;
		m_curY = prePos.second;
		m_grids[m_curY][m_curX].val = 0;
		m_grids[m_curY][m_curX].DecCount(val);
		for (int r = 0;r < TEMPLATE_SIZE;r++)
		{
			if (r != m_curY)
			{
				m_grids[r][m_curX].DecCount(val);
			}
		}
		for (int c = 0 ; c < TEMPLATE_SIZE; c++)
		{
			if (c != m_curX)
			{
				m_grids[m_curY][c].DecCount(val);
			}
		}

		for (int r = (m_curY / 3) * 3; r < (m_curY / 3) * 3 + 3; r ++)
		{
			for (int c = (m_curX / 3) * 3; c < (m_curX / 3) * 3 + 3; c++)
			{
				if (r == m_curY || c == m_curX)
				{
					continue;
				}
				m_grids[r][c].DecCount(val);
			}
		}
		posTrace.pop();
		m_nRemained ++;
	}
	void PlaceNumber(int val)
	{
		m_grids[m_curY][m_curX].val = val;
		m_grids[m_curY][m_curX].IncCount(val);
		for (int r = 0;r < TEMPLATE_SIZE;r++)
		{
			if (r != m_curY)
			{
				m_grids[r][m_curX].IncCount(val);
			}
		}
		for (int c = 0 ; c < TEMPLATE_SIZE; c++)
		{
			if (c != m_curX)
			{
				m_grids[m_curY][c].IncCount(val);
			}
		}

		for (int r = (m_curY / 3) * 3; r < (m_curY / 3) * 3 + 3; r ++)
		{
			for (int c = (m_curX / 3) * 3; c < (m_curX / 3) * 3 + 3; c++)
			{
				if (r == m_curY || c == m_curX)
				{
					continue;
				}
				m_grids[r][c].IncCount(val);
			}
		}
		m_nRemained --;
		posTrace.push(make_pair<int,int>(m_curX, m_curY));
		DecideNextPlace();
	}

	SUDOKUState(int a[TEMPLATE_SIZE][TEMPLATE_SIZE])
	{
		m_nRemained = TEMPLATE_SIZE * TEMPLATE_SIZE;
		for (int i = 0; i <TEMPLATE_SIZE;i++)
		{
			for (int j = 0; j< TEMPLATE_SIZE;j++)
			{
				m_grids[i][j].nRemainCount = TEMPLATE_SIZE;
				m_grids[i][j].val = a[i][j];
				if (a[i][j] != 0)
				{
					//在第i行第j列放了数字a[i][j]
					m_curX = j;
					m_curY = i;
					PlaceNumber(a[i][j]);
				}
			}
		}
		DecideNextPlace();
	}
	//计算下一步放数字的格子,贪心
	void DecideNextPlace()
	{
		if (m_nRemained == 0)
		{
			return;
		}
		int minv = 10000;
		for (int r = 0; r < TEMPLATE_SIZE; r++)
		{
			for (int c = 0; c < TEMPLATE_SIZE; c++)
			{
				if (m_grids[r][c].val == 0 && m_grids[r][c].nRemainCount < minv)
				{
					minv = m_grids[r][c].nRemainCount;
					m_curX = c; 
					m_curY = r;
				}
			}
		}

		//有一个空格子已经没有数可以选择了
		if (minv == 0)
		{
			m_curX = -1;
			m_curY = -1;
		}
	}

	void PrintBoard()
	{
		for (int i = 0; i <TEMPLATE_SIZE;i++)
		{
			for (int j = 0; j< TEMPLATE_SIZE;j++)
			{
				cout << m_grids[i][j].val << " ";
			}
			cout << endl;
		}
	}
	
	Grid m_grids[TEMPLATE_SIZE][TEMPLATE_SIZE];
	std::stack<pair<int,int> > posTrace;     //记录放置的位置记录
	int m_nRemained;	//还有多少个要放置
	
	//当前需要放置的位置
	int m_curX;
	int m_curY;	
};

void Solve(SUDOKUState& state)
{
	if (bSolved)
	{
		return;
	}
	//cout << gCount++ << endl;
	gCount++;
	if (state.IsFinal())
	{
		state.PrintBoard();
		bSolved = true;
		return;
	}
	if (state.IsDead())
	{
		return;
	}

	for (int i = 1; i < 10; i++)
	{
		if (!state.CanPlace(i))
		{
			continue;
		}
		state.PlaceNumber(i);
		Solve(state);
		state.RemoveNumber(i);
	}
}
int _tmain(int argc, _TCHAR* argv[])
{
	SUDOKUState state(board);	
	state.PrintBoard();
	cout << endl;
	//GFClock gfClock;
	Solve(state);
	//cout << gfClock.Elapsed() << " ms" << endl;
	cout << gCount << " times called" << endl;
	return 0;
}




  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值