一、什么是Futoshiki数独游戏
首先,我们需要了解Futoshiki数独游戏的规则,如下图所示。
说白了这个游戏就是通过代码实现,将一个带有大小逻辑关系的数独正确填充,这种数独不同于普通的数独游戏,还需要确保符合每一行每一列的数字大小关系。
二、根据该数独游戏来设计文本
对于该数独游戏来讲,我们需要设计一个文本来显示我们需要填充的数独,此时设计出以下两个未被填充完整的文本来进行Futoshiki数独游戏,并取好名字6.txt,8.txt以便后续可以读取该文本。
三、设计数据结构
为了解决这个游戏问题,我们首先需要把上述设计的文本内容转换成代码可实现的形式,这时我们就需要设计数据结构来存储这个文本内容,我们可以有多种方式来存储,动态数组,双重指针等,我采用的方法是用双重指针来存储文本内容。
现在创建一个结构体来储存数独中每一元素的左关系(该元素与左边元素的大小关系),上关系(该元素与上面一个元素的大小关系),由此可知,判断关系是否正确时需要从第二行或者第二列开始。
typedef struct
{
int value;//该元素的值
char leftRel;//与左边元素的关系
char upRel;//与上面元素的关系
int rowRepeat;//判断行元素是否重复
int colRepeat;//判断列元素是否重复
}Matrix;
接着用双指针声明变量,该变量用来存储文本内容。
Matrix** matrix = NULL;
接着,再设计一个结构体来保存数独游戏的多种结果,以链表的形式读取结果。
typedef struct result
{
Matrix** matrix;
struct result* next;
} Result;
同样用指针声明变量 ,这些变量分别用来存储填充的内容和显示数独游戏多种结果。
Result* results = NULL, *res1 = NULL;
四、设计函数
设计完基本的数据结构,就该设计函数来解决数独游戏问题。
下面就是整个数独游戏解决的主函数,包含着整体思路。
int main()
{
int i = 0, N = 0;
Result* results = NULL, *res1 = NULL;
int total = 0;
char filename[100];
cout<<"请输入读取的文件名:";
cin>>filename;
Matrix** matrix = NULL;
matrix = read_file(filename,&N);
//print_matrix(matrix,N);
if(matrix == NULL)
{
complete = 0;
valid = 0;
}
else
{
cout<<"读取文件中的矩阵如下:"<<endl;
print_matrix(matrix,N);
IsValid(matrix,N);
Matrix** backup = NULL;
backup = copymatrix(matrix,N);
if (complete)
{
cout<<"这是一个完整的矩阵!";
if (valid)
cout<<"而且是一个有效的矩阵."<<endl;
else
cout<<"但这不是一个有效的矩阵."<<endl;
}
else
{
cout<<"这不是一个完整的矩阵!";
if (valid)
{
if (solve_matrix(matrix,N,0))
{
cout<<"填充的矩阵如下:"<<endl;
//print_matrix(matrix,N);
matrix = copymatrix(backup, N);//将原本读取到的文件内容赋值回来
// 对结果分配头结点
results = new Result;//分配内存
results->next = NULL;
results->matrix = NULL;
solve_matrix_2(matrix, N, 0, results, &total);
// 将结果输出
res1 = results->next;
while(res1 != NULL)
{
print_matrix(res1->matrix, N);
res1 = res1->next;
}
cout<<"所能填充的情况为:"<<total<<endl;
}
else
cout<<"无法填充该矩阵!"<<endl;
}
else
cout<<"无法填充该矩阵!"<<endl;
}
}
free_matrix(matrix, N);
free_result(results,N);
}
这个函数主要用于读取所设计的文本内容到双重指针中去,主要步骤为利用嵌套循环分别读取数独中的左关系和上关系。
Matrix** read_file(char* filename,int* N)//读取文件内容
{
Matrix** matrix = NULL;
int n = 0, i, j, value = 0;
char a[10];//定义一个字符后续保存读取文件字符的值
int b;// 定义一个整型后续保存读取文件整数的值
//开始读取文件
ifstream file;
file.open(filename);
if(!file.is_open())
{
cout<<"文件不存在!";
return NULL;
}
file>>n;
file>>n;
*N = n;
matrix = initial_matrix(matrix,n);//初始化二维数组
//print_matrix(matrix,n);
//接下来就可以读取文件中的内容了
for( i = 0 ;i < n ; i++)
{
if( i > 0 )//读取文本文件中的上下关系
{
for (j = 0; j < n; j++)
{
file>>a;
matrix[i][j].upRel = a[0];//读取文件中的关系 ,-,<,>
//cout<<a<<endl;
if (j < n - 1)
{
file.ignore(2); // 读取一次大小关系便跳过中间的字符-
}
}
}
for( j = 0; j < n; j++)//读取文本文件中的左右关系
{
if( j > 0 )
{
file>>a;
matrix[i][j].leftRel = a[0];
}
file>>b;
matrix[i][j].value = b;
}
}
file.close();
//print_matrix(matrix,n);//将读取文本文件后的内容打印
return matrix;
}
其中还涉及到一个初始化函数,将所有元素的关系都初始化。
Matrix** initial_matrix(Matrix** matrix, int n)
{
int i, j;
matrix = new Matrix*[n];
if (matrix == NULL)
{
cerr << "Memory error!" << endl;
exit(1);
return 0;
}//判断分配内存是否成功
for(i = 0 ; i < n ; i++)
{
matrix[i] = new Matrix[n];//对每一个一维数组开辟空间
if (matrix[i] == NULL)
{
cerr << "Memory error!" << endl;
exit(1);
return 0;
}//判断分配行内存是否成功
for (j = 0; j < n; ++j)
{
matrix[i][j].value = 0;
matrix[i][j].leftRel = '|';
matrix[i][j].upRel = '-';
matrix[i][j].rowRepeat = 0;
matrix[i][j].colRepeat = 0;
}//整个二维数组 martix被成功地分配并初始化。每个元素都有默认的初始值
}
//print_matrix(matrix, n);
return matrix;
}
接着,这两个函数尤为重要,用来判断所填充的数字是否正确有效。
int check_matrix(Matrix** matrix, int row, int col,int n)//主要作用就是判断已有的数字合不合理放在此处
{
int value = matrix[row][col].value;
char relation;
if (col > 0 && matrix[row][col - 1].value != 0)
{
relation = matrix[row][col].leftRel; // 获取左边的关系符
if (matrix[row][col - 1].value > value && relation == '<') // 不用检测=的情况,因为每行数据不存在重复数
{
return 0;
}//relation为小于号,如果左边的数据大于右边的数据则是错误的
if (matrix[row][col - 1].value < value && relation == '>' )
{
return 0;
}//同理relation为大于号,如果左边的数据小于右边的数据则是错误的
}
if (row > 0 && matrix[row - 1][col].value != 0)
{
relation = matrix[row][col].upRel; // 获取上面的关系符
if (matrix[row - 1][col].value > value && relation == '^') // 不用检测=的情况,因为每行数据不存在重复数
{
return 0;
}//relation为小于号,如果左边的数据大于右边的数据则是错误的
if (matrix[row - 1][col].value < value && relation == 'v' )
{
return 0;
}//同理relation为大于号,如果左边的数据小于右边的数据则是错误的
}
if(row < (n - 1) && row > 0 && matrix[row][col].upRel == '^' && matrix[row+1][col].upRel == 'v' && matrix[row+1][col].value == 2)
{
return 0;//如果碰到<2>这种情况则该数独肯定是无效的
}
if(col < (n - 1) && col > 0 && matrix[row][col].leftRel == '<' && matrix[row][col+1].leftRel == '>' && matrix[row+1][col].value == 2 )
{
return 0;//如果2的上下关系是^v这种情况则该数独也肯定是无效的
}
if(row < (n - 1) && row > 0 && matrix[row][col].upRel == 'v' && matrix[row+1][col].upRel == '^' && matrix[row+1][col].value == n - 1)
{
return 0;//如果碰到> n - 1 <这种情况则该数独肯定是无效的
}
if(col < (n - 1) && col > 0 && matrix[row][col].leftRel == '>' && matrix[row][col+1].leftRel == '<' && matrix[row+1][col].value == n - 1)
{
return 0;//如果n - 1是v^这种情况则该数独也肯定是无效的
}
return 1;
}
int IsValid(Matrix** matrix,int n)//判断矩阵是否完整,完整的是否有效
{
for( int i = 0; i < n; i++)
{
for( int j = 0; j < n; j++)
{
if ( matrix[i][j].value == 0)
{
complete = 0;
}
else if ( matrix[i][j].value < 0 || matrix[i][j].value > n)
{
valid = 0;
}
else // val在1到n之间
{
if (matrix[i][matrix[i][j].value - 1].rowRepeat++) // value是数字几就让其对应其实际列数,让其等于1,若后续还有1,则代表重复了,
{
valid = 0;
} //用于检测同一行中是否存在重复数,若存在,则该方案失效
if (matrix[matrix[i][j].value - 1][j].colRepeat++)//value是数字几就让其对应其实际行数,让其等于1,若后续还有1,则代表重复了,
{
valid = 0;
} // 用于检测同一列中是否存在重复数,若存在,则该方案失效
if (check_matrix(matrix, i, j, n) == 0)//判断你输入进来的数据是不是对的,有没有满足本身的限制条件,调用check_relations函数,21行
{
valid = 0;
}
}
}
}
}
最后,到了最重要的函数设计,根据主函数的步骤solve_matrix()用来判断一个数独游戏是否能进行正确的填充,如果可以正确的填充那我们就运行solve_matrix_2()函数来找出该数独游戏的所有结果。该函数利用到的是递归和回溯,通过不断地判断数独的正确填充来找出数独的正确结果和多种可能。
int solve_matrix(Matrix** matrix,int n,int m)
{
int i = 0, j = 0, k = 0, rowUsed = 0, colUsed = 0;
i = m / n;
j = m % n;
if (m >= n * n) // 终止
{
return 1;
}
if (matrix[i][j].value) // 如果当前值不为0
{
if (check_matrix(matrix, i, j, n)) // 如果检测成功
{
return solve_matrix(matrix, n, m + 1); // 递归调用,继续检测下一个元素
}
else
{
// 检测不成功,返回失败
return 0;
}
}
// 如果当前值为0
for (k = 1; k <= n; ++k)
{
matrix[i][j].value = k;
rowUsed = matrix[i][k - 1].rowRepeat++;
colUsed = matrix[k - 1][j].colRepeat++;
if (rowUsed == 0 && colUsed == 0 && check_matrix(matrix, i, j, n) == 1) // 如果行和列都不存在重复数,并且关系合法
{
if (solve_matrix(matrix, n, m + 1)) // 继续检测下一个,如果合法返回1
{
return 1;
}
}
// 将当前k退出,继续检测下一个k
matrix[i][j].value = 0;
--matrix[i][k - 1].rowRepeat;
--matrix[k - 1][j].colRepeat;
}
return 0;
}
int solve_matrix_2(Matrix** matrix, int n, int step, Result* results, int* total)
{
int i = 0, j = 0, k = 0, rowUsed = 0, colUsed = 0;
Result* tmp = NULL;
i = step / n;
j = step % n;
if (step >= n * n)
{
++*total;
// 将结果记录
tmp = new Result;//分配内存
if (tmp == NULL)//判断是否分配成功
{
cerr<<"Memory error!"<<endl;
exit(1);
return 0;
}
// 拷贝结果
tmp->next = results->next;
tmp->matrix = copymatrix(matrix, n);
results->next = tmp;
return 1;
}
if (matrix[i][j].value != 0)
{
if (check_matrix(matrix, i, j, n))
{
return solve_matrix_2(matrix, n, step + 1, results, total);
}
else
{
return 0;
}
}
for (k = 1; k <= n; ++k)
{
matrix[i][j].value = k;
rowUsed = matrix[i][k - 1].rowRepeat++;
colUsed = matrix[k - 1][j].colRepeat++;
if (rowUsed == 0 && colUsed == 0 && check_matrix(matrix, i, j, n))
{
solve_matrix_2(matrix, n, step + 1, results, total);
}
// 将当前k退出,继续检测下一个k
matrix[i][j].value = 0;
--matrix[i][k - 1].rowRepeat;
--matrix[k - 1][j].colRepeat;
}
// 如果k>n则检测失败
return 0;
}
五、结果展示
可以看到以下的两种结果,如果该数独只有一种填充方式那么就只展示出一种来,如果该数独的填充方式有多种,就将所有的情况都列出来。
六、疑惑
为何通过该种方式无法正确的将双重指针和指针的内存完全释放。
void free_matrix(Matrix** matrix, int n)
{
if (matrix == NULL)
{
return;
}
for (int i = 0; i < n; ++i)
{
delete[] matrix[i];
}
delete[] matrix;
}