C++求解经典数独——几毫秒破译世界最难数独

数独是一款常见的益智数字游戏,无奈我智商偏低,很多数独解不下去,只有靠程序才能挽回一点尊严。这里附带了一个2017年夏天写的C++求解数独程序,可以在毫秒级的时间破译最难数独。

经典的数独游戏由9*9的方阵表示,每个位置可以填写1-9的数字,要求:

数独是一款常见的益智数字游戏,无奈我智商偏低,很多数独解不下去,只有靠程序帮忙才能挽回一点尊严。

这里附带了一个2017年夏天写的C++求解数独程序,可以在几毫秒的时间里破译所谓的最难数独;由于是在一两个下午匆匆写完的,代码已经看不下去了,不过幸好思路还记得。


思路

可能性数组

基本的思路是递归+回溯。考虑一种最暴力的解法:递归地瞎蒙填数字,到所有数字填满后,根据数独规则检查是否合法,如合法则输出结果,如不合法则回溯。这样写出来的代码简洁易懂,不过性能就很坑爹了。其实,玩过数独的朋友都知道,在填写数字时,根据数独三条基本规则进行直接或间接地推导,有些位置的数字可以唯一地确定。例如:某空格所在行、列、组中已经出现了2-9这8个数字,那么这个空格只可能填1。许多简单、中级的数独都可以这样一路填完,更高级的谜题才需要若干次的瞎蒙。因此,这个程序:

当某个空格只可能填写某一个数字时,我们就可以自信满满地不需要回溯地填下去;当某个空格不可能为任何数字时,我们就可以得知矛盾已经发生从而提前开始回溯;如果两者皆否,那么就需要开始瞎蒙了。

贪心猜测

其次——正如上面提到的——更高级的谜题必然需要若干次地瞎蒙才能继续填写下去(之前也有了解到,对于高难度地题目不论用多风骚地技巧也无法避免猜测)。当我们发现无法自信满满地填写任何一个数字时,就不可避免地使用递归回溯进行猜测。既然避免不了要猜,那么自然要猜到点上,程序也因此遵循了局部最优的贪心原则,即:

例如,假设数独有唯一解,一个空格可能填12345五个数字,而另一个空格可能填78两个数字,那么从后者开始猜能够猜中的概率显然会大于前者。

任意阶数

9*9的经典数独是一种3阶的数独,而这个程序可以实现任意n阶的数独猜测,如16*16的4阶数独。不过,阶数提高后时间复杂度也会指数提高。

并行猜测

性能不够,并行来凑。程序使用OpenMP实现多线程。不过,目前而言仅在根部递归展开并行,又由于贪心猜测使得可猜的数字一般只有2~3个,因此并行度并不够高。

不足之处

这个程序的可能性数组仅由三条规则的直接推导得出,需要承认的是,直接推导的效力很弱,许多隐含的确定性是无法侦测到的。如果能够程序实现更加高效的间接推导,那么效率会进一步提高。


程序

是时候祭上自己看了都害怕“以前的代码”了。

头文件: SudokuKiller.hpp

#pragma once
#include <memory>
#include <vector>

using namespace std;

class SudokuKiller
{
	typedef pair<unsigned int, unsigned int> Grid;

private:
	inline unsigned int * InitData() {
		unsigned int * data = new unsigned int[Area];
		return data;
	}
	inline bool ** InitPossible() {
		bool ** possible = new bool*[Area];
		for (int i = 0; i < Area; i++) {
			possible[i] = new bool[Len];
		}
		return possible;
	}
	inline Grid ** InitGrid() {
		Grid ** gridData = new Grid*[GridCount];
		for (int g = 0; g < GridCount; g++) {
			gridData[g] = new Grid[Len];

			int rs = (g % Order) * Order;
			int cs = (g / Order) * Order;
			int idx = 0;
			for (int r = rs; r < rs + Order; r++) {
				for (int c = cs; c < cs + Order; c++) {
					gridData[g][idx].first = r;
					gridData[g][idx].second = c;
					idx++;
				}
			}
		}

		return gridData;
	}
	inline void DeleteData(unsigned int * data) {
		delete data;
	}
	inline void DeletePossible(bool ** possible) {
		for (int i = 0; i < Area; i++) {
			delete possible[i];
		}
		delete possible;
	}
	inline void DeleteGrid(Grid ** gridData) {
		for (int g = 0; g < GridCount; g++) {
			delete gridData[g];
		}
		delete gridData;
	}
	inline void CopyData(unsigned int * dataNew, unsigned int * dataOld) {
		memcpy(dataNew, dataOld, sizeof(unsigned int) * Area);
	}
	inline void CopyPossible(bool ** possibleNew, bool ** possibleOld) {
		for (int i = 0; i < Area; i++) {
			memcpy(possibleNew[i], possibleOld[i], sizeof(bool) * Len);
		}
	}

protected:
	unsigned int Order;
	unsigned int Len;
	unsigned int Area;
	unsigned int GridCount;
	unsigned int * Data = nullptr;
	bool ** Possible = nullptr;
	Grid ** GridData = nullptr;
	inline bool UpdatePossibilityByRow(unsigned int * data, bool ** possible, int r) {
		//The array "p" for all elements' possibility temporarily, true as default
		bool * p = new bool[Len];
		memset(p, 0xFF, sizeof(bool) * Len);

		//If an element is confirmed in this row, then we switch the corresponding "p" to "false"
		unsigned int d;
		for (int c = 0; c < Len; c++) {
			d = data[r * Len + c];
			if (d) {
				d--;
				if (p[d] == false)
					return false;		//We return a false here because this situation means that there're dumplicated elements in the row, which is invalid
				p[d] = false;
			}
		}

		//Then we can update "possible" refering to "p", by logical AND
		for (int c = 0; c < Len; c++) {
			bool anyPossible = false;
			for (int i = 0; i < Len; i++) {
				possible[r * Len + c][i] &= p[i];
				anyPossible |= possible[r * Len + c][i];
			}
			if (!data[r * Len + c] && !anyPossible)
				return false;		//When an unconfirmed element has no possible values, it means a contradiction occurs
		}

		delete p;
		return true;
	}
	inline bool UpdatePossibilityByCol(unsigned int * data, bool ** possible, int c) {
		//Similar to "UpdatePossibilityByRow"
		bool * p = new bool[Len];
		memset(p, 0xFF, sizeof(bool) * Len);

		unsigned int d;
		for (int r = 0; r < Len; r++) {
			d = data[r * Len + c];
			if (d) {
				d--;
				if (p[d] == false)
					return false;
				p[d] = false;
			}
		}

		for (int r = 0; r < Len; r++) {
			bool anyPossible = false;
			for (int i = 0; i < Len; i++) {
				possible[r * Len + c][i] &= p[i];
				anyPossible |= possible[r * Len + c][i];
			}
			if (!data[r * Len + c] && !anyPossible)
				return false;
		}

		delete p;
		return true;
	}
	inline bool UpdatePossibilityByGrid(unsigned int * data, bool ** possible, int g) {
		//Similar to "UpdatePossibilityByRow"
		int r, c;
		bool * p = new bool[Len];
		memset(p, 0xFF, sizeof(bool) * Len);

		unsigned int d;
		for (int n = 0; n < Len; n++) {
			r = GridData[g][n].first;
			c = GridData[g][n].second;
			d = data[r * Len + c];
			if (d) {
				d--;
				if (p[d] == false)
					return false;
				p[d] = false;
			}
		}

		for (int n = 0; n < Len; n++) {
			r = GridData[g][n].first;
			c = GridData[g][n].second;
			bool anyPossible = false;
			for (int i = 0; i < Len; i++) {
				possible[r * Len + c][i] &= p[i];
				anyPossible |= possible[r * Len + c][i];
			}
			if (!data[r * Len + c] && !anyPossible)
				return false;
		}

		delete p;
		return true;
	}
	inline bool UpdatePossibility(unsigned int  * data, bool ** possible) {
		for (int i = 0; i < Len; i++) {
			if (!UpdatePossibilityByRow(data, possible, i)) return false;
			if (!UpdatePossibilityByCol(data, possible, i)) return false;
		}
		for (int i = 0; i < GridCount; i++) {
			if (!UpdatePossibilityByGrid(data, possible, i)) return false;
		}
		return true;
	}
	inline void GetPossibleValues(bool ** possible, int i, vector<unsigned int> & possibleValues) {
		possibleValues.clear();
		for (int n = 0; n < Len; n++) {
			if (possible[i][n]) {
				possibleValues.push_back(n + 1);
			}
		}
	}
	inline bool Solve(unsigned int * data, bool ** possible, volatile bool & solved, int nest = 0) {
		int guessIndex;					//The index of an unconfirmed element with minimal possibilities, being -1 if all elements are confirmed
		std::vector<unsigned int> guessValues;		//The possible values of guessIndex element
		bool updated;					//Whether this Solve makes any difference

										//Now we keeps updating "data" as well as "possible", when we find any element can be confirmed
										//When a contradictory occurs, a false is returned in advance
		std::vector<unsigned int> possibleValues;
		do {
			updated = false;
			guessIndex = -1;
			for (int i = 0; i < Area; i++) {
				if (data[i]) continue;

				GetPossibleValues(possible, i, possibleValues);
				if (possibleValues.size() == 1) {
					data[i] = possibleValues[0];
					updated = true;
				}
				else {
					if (guessIndex == -1) {
						guessIndex = i;
						guessValues = possibleValues;
					}
					else if (possibleValues.size() < guessValues.size()) {
						guessIndex = i;
						guessValues = possibleValues;
					}
				}
			}
			if (updated) {
				if (!UpdatePossibility(data, possible)) return false;
			}
		} while (guessIndex != -1 && updated);

		//If we all elements are confirmed
		if (guessIndex == -1) return true;

		//If we cannot solve the sudoku by the methods above, we make guesses and check them by recursion
		//Note that we only need to guess one unconfirmed element at a time, since the remaining shall be done during the following recursions

		bool retval = false;
		if (nest == 0) {
			//Guess in parallel
			//We will malloc data for every thread
#pragma omp parallel for schedule(dynamic)
			for (int i = 0; i < guessValues.size(); i++) {
				if (solved) continue;

				unsigned int * dataNext = InitData();
				bool ** possibleNext = InitPossible();
				CopyData(dataNext, data);
				CopyPossible(possibleNext, possible);
				dataNext[guessIndex] = guessValues[i];
				if (UpdatePossibility(dataNext, possibleNext) && Solve(dataNext, possibleNext, solved, nest + 1)) {
#pragma omp critical
					{
						CopyData(data, dataNext);
						CopyPossible(possible, possibleNext);
						retval = true;
						solved = true;
					}
				}
				DeleteData(dataNext);
				DeletePossible(possibleNext);
			}
		}
		else {
			//Guess in sequential
			unsigned int * dataRaw = InitData();
			bool ** possibleRaw = InitPossible();
			CopyData(dataRaw, data);
			CopyPossible(possibleRaw, possible);

			for (int i = 0; i < guessValues.size(); i++) {
				if (solved) continue;

				data[guessIndex] = guessValues[i];
				if (UpdatePossibility(data, possible) && Solve(data, possible, solved, nest + 1)) {
					retval = true;
					solved = true;
				}
				else {
					CopyData(data, dataRaw);
					CopyPossible(possible, possibleRaw);
				}
			}
			DeleteData(dataRaw);
			DeletePossible(possibleRaw);
		}

		return retval;
	}

public:
	SudokuKiller(unsigned int order = 3) {
		Order = order;
		Len = Order * Order;
		Area = Len * Len;
		GridCount = Len;

		Data = InitData();
		Possible = InitPossible();
		GridData = InitGrid();
	}
	virtual ~SudokuKiller() {
		DeleteData(Data);
		DeletePossible(Possible);
		DeleteGrid(GridData);
	}

	bool SetData(unsigned int * input) {
		for (int i = 0; i < Area; i++) {
			if (input[i] < Len + 1) {
				Data[i] = input[i];
				for (int n = 0; n < Len; n++) Possible[i][n] = true;
			}
			else {
				return false;
			}
		}
		bool valid = UpdatePossibility(Data, Possible);
		return valid;
	}
	bool Solve(unsigned int * output) {
		volatile bool solved = false;
		if (Solve(Data, Possible, solved)) {
			memcpy(output, Data, sizeof(unsigned int) * Area);
			return true;
		}
		else {
			return false;
		}
	}
};

使用: main.cpp

#include <iostream>
#include <windows.h>
#include "SudokuKiller.hpp"

//高精度计时
unsigned long long NowTicks() {
	LARGE_INTEGER time;
	LARGE_INTEGER frequency;
	QueryPerformanceFrequency(&frequency);
	QueryPerformanceCounter(&time);
	return 1000 * 100 * time.QuadPart / frequency.QuadPart;
}

int main()
{
	//3阶经典数独求解
	SudokuKiller killer(3);

	//百度百科中的“最难数独”
	//https://baike.baidu.com/item/%E4%B8%96%E7%95%8C%E6%9C%80%E9%9A%BE%E6%95%B0%E7%8B%AC
	unsigned int data1[81] = {
		0, 0, 5, 3, 0, 0, 0, 0, 0,
		8, 0, 0, 0, 0, 0, 0, 2, 0,
		0, 7, 0, 0, 1, 0, 5, 0, 0,
		4, 0, 0, 0, 0, 5, 3, 0, 0,
		0, 1, 0, 0, 7, 0, 0, 0, 6,
		0, 0, 3, 2, 0, 0, 0, 8, 0,
		0, 6, 0, 5, 0, 0, 0, 0, 9,
		0, 0, 4, 0, 0, 0, 0, 3, 0,
		0, 0, 0, 0, 0, 9, 7, 0, 0,
	};

	//另一处的“最难数独”
	//https://blog.csdn.net/littlethunder/article/details/9749509
	unsigned int data2[81] = {
		8, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 3, 6, 0, 0, 0, 0, 0,
		0, 7, 0, 0, 9, 0, 2, 0, 0,
		0, 5, 0, 0, 0, 7, 0, 0, 0,
		0, 0, 0, 0, 4, 5, 7, 0, 0,
		0, 0, 0, 1, 0, 0, 0, 3, 0,
		0, 0, 1, 0, 0, 0, 0, 6, 8,
		0, 0, 8, 5, 0, 0, 0, 1, 0,
		0, 9, 0, 0, 0, 0, 4, 0, 0,
	};
	unsigned int output[81];
	unsigned long long time;

	//题目1
	killer.SetData(data1);
	time = NowTicks();
	killer.Solve(output);
	std::cout << 1.0 * (NowTicks() - time) / 100 << " ms" << std::endl;
	for (int r = 0; r < 9; r++) {
		for (int c = 0; c < 9; c++) {
			cout << output[r * 9 + c] << " ";
		}
		cout << endl;
	}
	cout << endl;

	//题目2
	killer.SetData(data2);
	time = NowTicks();
	killer.Solve(output);
	std::cout << 1.0 * (NowTicks() - time) / 100 << " ms" << std::endl;
	for (int r = 0; r < 9; r++) {
		for (int c = 0; c < 9; c++) {
			cout << output[r * 9 + c] << " ";
		}
		cout << endl;
	}
	cout << endl;

	return 0;
}

输出

1.67 ms
1 4 5 3 2 7 6 9 8
8 3 9 6 5 4 1 2 7
6 7 2 9 1 8 5 4 3
4 9 6 1 8 5 3 7 2
2 1 8 4 7 3 9 5 6
7 5 3 2 9 6 4 8 1
3 6 7 5 4 2 8 1 9
9 8 4 7 6 1 2 3 5
5 2 1 8 3 9 7 6 4

4.45 ms
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2

 

称谓(*)
邮箱
留言(*)