编写回溯算法文章时,文章里用到了八皇后案例。文章的初衷是为了讲好回溯算法,体现算法的核心逻辑,没有在案例的子逻辑上费太多心思。导致阅读过文章的粉丝留言说,检查皇后位置是否合法的代码略显冗余。回头再审查时,也觉得言之有理。
现在单独把此案例拿出来讲解,一是自证自己还是可以提供简洁明了的代码。二是试着选择不同的数据结构完成此代码,聊一聊不同数据结构对算法影响有多大。
开始之前,废话一下,数据结构与算法之间的关系。
解决问题的基本流程:
如上述描述,数据结构会影响算法对数据的获取。良好的数据结构,可以让算法很快得到数据,设计上有缺陷的数据结构,算法会折腾一会后才能得到数据。数据结构不应该改变算法自己的处理流程。我们从下面几种八皇后的解决方案中应该有所心得。
八皇后问题是一个经典案例。此处还是对此问题的要求稍做说明。
问题说明:
在一个8
行8
列的棋盘上,有 8
个皇后,请问让这 8
个皇后不在同一行、不在同一列、不在所有对角线上的摆放方式有多少种?
类似于这种求解多种方案的问题,自然要想到回溯算法。回溯算法能在找到一种结果后,通过回溯至前一步状态,然后再向前又去选择另一种新的结果。虽然本文下面提供了三种解决方案,但算法还是回溯算法。
八皇后两类数据,棋盘以及皇后。棋盘物理结构上是平面,自然想法是使用二维数组模拟盘。问题域中的皇后,代码层面上就是给二维数组中的某些位置赋值(赋的值无非就是一个数字标志),赋值时要满足同一行、同一列、同一对角线上是否有其它数据。
一切明了之后,开始在棋盘下棋。
算法流程:
先执一枚皇后下在二维数组的 (1,1)
处。代码层面,初始二维数组中的单元格中的值为0
,表示没有放置任何棋子,放置棋子后,设置为一个特定标识数字,标识数字的选择,也能影响到算法的处理过程。
然后再执一枚皇后,寻找一处在水平方向上、垂直方向以及对角线上没有其它皇后的位置落子。以此类推,直到所有皇后下在棋盘上。
当最后一枚皇后落子后,便得到一种方案。此时,可以起子,试着再在其它地方落子,便可以得到另一种方案。如果最后一枚棋子已经无法再找到可落子的地方,可以重新为倒数第二枚的皇后指定新位置,当倒数第二枚棋子确定新位置后,再重新设置最后一枚皇后的位置。
以此逻辑,得到所有方案。
描述中,实则体现了回溯算法的基本思路。回溯算法充分诠释了蝴蝶效应,昨天的某个不经意的改变,会让今天面目全非。回溯算法适用于求多种方案的题目。回溯算法底层逻辑是用递归进行深度搜索,属于穷举方案,时间性能较差。当然,可以使用如剪树等优化方案。
算法实现:
#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
//二维数组,用来存储皇后位置
int nums[9][9]= {0};
//记数
int total=0;
int show() {
total++;
for(int i=1; i<9; i++)
for(int j=1; j<9; j++)
if( nums[i][j]!=0 )
cout<<"("<<i<<","<<j<<")"<<nums[i][j] <<"\t";
cout<<"\n-----------------"<<endl;
}
/*
*判断位置是否符合要求
*/
bool isExist(int row,int col) {
for(int i=1; i<9; i++) {
for(int j=1; j<9; j++) {
//同一行
if(i==row && nums[i][j]!=0)return false;
//同一列
if(j==col && nums[i][j]!=0)return false;
//对角线一
if( (row+col)==(i+j) && nums[i][j]!=0 )return false;
// 对角钱二
if ( row>=col && (row-col)==(i-j) && nums[i][j]!=0 )return false;
if ( row<col && (col-row)==(j-i) && nums[i][j]!=0 )return false;
}
}
return true;
}
/*
*按行扫描遍历二维数组
*
*/
void search(int row) {
for(int col=1; col<=8; col++) {
if( isExist(row,col) ) {
//如果位置可用
nums[row][col]=col;
if(row==8) show();
else search(row+1);
nums[row][col]=0;
}
}
}
int main(int argc, char** argv) {
search(1);
cout<<"\共有"<<total<<"种摆放方案!";
return 0;
}
用二维数组构建棋盘模型,如果问题要求在nxn
大小的模盘上放置n
个皇后,则会造成空间浪费。优点是直观、操作性强。上述代码称得上实打实的,中规中矩,一切按照题目的本意展开。
在判定皇后位置是否合理时,确实略稍繁复。同一行、同一列应该没有更好的通用表示式。在判定对角线上有没有其它皇后时,因为没有找到更底层的规律,导致分了几种情况讨论。
这个冗余并不是核心算法中的内容,而是因为没有完全理解数据结构的特征,或者说是对正方形矩阵掌握的不是很好而导致把简单问题复杂。
下面用一维数组映射问题模型。
一维数组模拟八皇后中的数据,有两种方案。
一维数组中只存储结果,棋盘只存在代码的意识形态中。数组的下标映射至皇后在棋盘上的列号,值映射至皇后在棋盘上所在的行号。如下图所示:
现在深入分析棋盘对角线坐标的特点:
如下图所示,在棋盘的(3,5)
处放置有一个,与其在同一个对角线上的棋盘格的坐标与它的行坐标之差的绝对值等于列坐标之差。其实这个规律很简单。如果没发现,则会让问题变得复杂 。
有了这些信息后,可以开始编写回溯算法。
准备工作。
#include <bits/stdc++.h>
using namespace std;
const int CELLS =9;
//存储结果
int res[CELLS];
void init() {
memset(res,sizeof(res),0);
}
/*
*一维数组中的结果映射到二维平面上
*/
void showRes() {
//填充皇后位置用 1 表示,没有填充位置用 0
for(int r=1; r<CELLS; r++) {
for( int c=1;c<CELLS; c++ ) {
if( res[c]==r )cout<<"1\t";
else cout<<"0\t";
}
cout<<endl;
}
}
验证皇位位置是否合法:
//检查该列位置放置皇后是否合法
int validate(int col,int row) {
for (int i=1; i<col; i++) {
if (row==res[i] || abs(row-res[i])==(col-i))
return 0;
}
return 1;
}
回溯算法:
void dfs(int col) {
//可选择的位置
for( int r=1; r<CELLS; r++) {
if( validate(col,r) ) {
res[col]=r;
if( col==CELLS-1 ) {
//最后一列
showRes();
cnt++;
cout<<"----------------"<<endl;
} else {
dfs(col+1);
res[col]=0;
}
}
}
}
测试:
int main() {
init();
dfs(1);
cout<<cnt<<endl;
return 0;
}
所有算法的本质是一样的,在准备放入一个皇后时,需要检查此位置是否合法,检查的方式有复杂的也有简单的。复杂说明没有完全找出棋盘中棋子之间的数学规律,简单是因为归纳出了通用规则。
用一维数组模拟二维棋盘,需要在一维数组和二维数组坐标之间进行转换。代码的逻辑结构和流程没有本质上区别。八皇后棋盘有64个
格子,则一维数组的长度为64
。
预处理:
#include <bits/stdc++.h>
using namespace std;
//一维数组模拟二维数组
int nums[64]= {0};
//计数器
int total=0;
//初始化
void init() {
for(int i=0; i<64; i++) {
//因为 0 是合法位置
nums[i]=-1;
}
}
//输出结果
int show() {
total++;
for(int i=0; i<64; i++) {
//一维坐标转换成二维坐标
int r=i/8;
int c=i%8;
//如果存储有位置信息
if( nums[i]!=-1 )cout<<"("<<r<<","<<c<<")"<<nums[ i ] <<"\t";
}
cout<<"\n-------------------"<<endl;
}
检查放置皇后的位置是否冲突:
//检查该列位置放置皇后是否合法
int isExist(int row,int col) {
for (int i=0; i<row*8+col; i++) {
//一维坐标转换为二维坐标
int r=i/8;
int c=i%8;
int d=abs(row-r)-abs(col-c);
//如盯存储有皇后位置信息
if( nums[i]!=-1) {
//是否和即将放置的位置相冲突
if( r==row || c==col || d==0 )
return 0;
}
}
return 1;
}
深度搜索:
/*
*按行扫描
*/
void search(int row) {
//每行可有的选择
for(int col=0; col<8; col++) {
if( isExist(row,col) ) {
//如果位置可用
nums[row*8+col]=col;
if(row==7) show();
else search(row+1);
nums[row*8+col]=-1;
}
}
}
测试:
int main(int argc, char** argv) {
init();
search(0);
cout<<"\共有"<<total<<"种摆放方案!";
return 0;
}
无论是一维数组还是二维数组,仅仅是高层存储性质发生了变化,而底层算法流程一样。
数据结构的变化,会影响访问方式的变化。设计良好的数据结构,访问起来即方便又便捷,且会节约 空间。而设计有缺陷的数据结构,不仅让访问变得冗余也会产生空间的浪费。
看来学好数据结构很重要。解决问题之前,第一要素便是能数字化问题模型。