问题描述
8*8
的国际象棋棋盘上的一只马,恰好走过除起点外的其他63个位置各一次,最后回到起点。这条路线称为马的一条Hamiltion周游路线。对于给定的m*n
的国际象棋盘,m,n均为大于5的偶数,且|m-n|<=2,试分析分治算法找出马的一条Hamilton周游路线。
算法设计:对于给定的偶数m,n>=6,且|m-n|<=2,计算m*n的国际象棋盘上马的一条Hamilton周游路线。
数据输入:由文件input.txt给出输入数据。第1行有两个正整数m和n,表示给固定的国际象棋棋盘有m行,每行有n个格子组成
结果输出:将计算的马的Hamilton周游路线用下面两种表达方式输出到文件output.txt.
- 第1种表达方式是按照马步的次序给出的Hamilton的周游路线。马的每一步用坐标方格(x,y)来表示,x表示行坐标,y表示列坐标
- 第2种表达方式在棋盘方格中表明马到达该方格的步数,(0,0)为起跳方格,并标明为第1步
问题分析
① 问题形式化描述:
给定一个棋盘E(m,n),棋盘规模满足m,n均为大于5的偶数,且|m-n|<=2,确定一个起点start<x,y>,在图中寻找一条哈密顿回路。
② 哈密顿回路定义:
由指定的起点前往指定的终点,途中经过所有其他节点且只经过一次。在图论中是指含有哈密顿回路的图,闭合的哈密顿路径称作哈密顿回路(Hamiltonian cycle),含有图中所有顶点的路径称作哈密顿路径。
③ 问题特性:
哈密顿路径问题被证明是“NP完备”的,有这样性质的问题,难于找到一个有效的算法。设一个无向图中有 N 个节点,若所有节点的度数都大于等于 N/2,则汉密尔顿回路一定存在。注意,“N/2” 中的除法不是整除,而是实数除法。如果 N 是偶数,当然没有歧义;如果 N 是奇数,则该条件中的 “N/2” 等价于 “⌈N/2⌉”。
首先想到是深度搜索,可以很方便地解决这个问题。除此之外,书上要求用分治法,不过这个分治法我没想到,只能去理解这个算法了。下面我们将对这两种算法进行分析:
深度搜索法
深度搜索算法实现很简单,算法思想如下:
- 按照日字型走法历遍节点
- 当整个棋盘已经全部走满,并且当前这一步到达起点,那么搜索结束
- 剪枝1:如果当前节点已经被访问过,那么我们不能在访问这个节点,因为这意味着这个节点已经是当前的路径上的一个节点或者通过这个节点无法访问到终点
- 剪枝2:如果马所在的及节点在棋盘外,这无法进行历遍
算法实现:
#include <iostream>
using namespace std;
const int maxn=105;
int board[maxn][maxn]={0};//棋盘
int num=0;
int m,n;//棋盘大小
int startX,startY;//起点位置
//马的8种走法,用数组做一个记录
int dx[8]={-2,-1,1,2,-2,-1,2,1};
int dy[8]={-1,-2,-2,-1,1,2,1,2};
//打印棋盘
void output()
{
for(int i = 0;i<m;i++)
{
for(int j = 0;j<n;j++) cout<<board[i][j]<<" ";
cout<<endl;
}
}
//判断下一步是否到达起始点
int toStart(int x,int y){
for(int i = 0;i<8;i++)
if(board[x+dx[i]][y+dy[i]]==1) return 1;
return 0;
}
//马走的函数
bool moveHorse(int x,int y,int num)
{
if(num==m*n+1&&toStart(x,y))//达到终点并且走满棋盘
{
output();//输出棋盘
return true;//找到路线
}
int xx=0,yy=0;
for(int i = 0;i<8;i++)
{
xx=x+dx[i],yy=y+dy[i];//下一步的坐标
if((!board[xx][yy])&&(xx>=0&&xx<m&&yy>=0&&yy<n))
{//下一步为空并且未越界
board[xx][yy]=num;//在棋盘上记录马的步数
if(moveHorse(xx,yy,num+1)) return true;
board[xx][yy]=0;//清零以便下一次查找
}
}
return false;
}
int main(){
//输入规模
cin>>m>>n;
//输入起始位置
cin>>startX>>startY;
board[startX][startY]=1;//将起始位置为1
cout<<moveHorse(startX,startY,2);
}
/*
6 6
0 0
1 26 17 14 35 12
18 5 36 11 24 15
27 2 25 16 13 34
6 19 4 31 10 23
3 28 21 8 33 30
20 7 32 29 22 9
1
*/
算法复杂度:
我们可以发现这个算法可以这样描述:
T(n)=8T(n-1)+C,做一个简单的等比数列求和,算法复杂度是O(8^n),显然这样的复杂度是不可接受的,我们考虑另外一种方法。
分治法
这个算法是算法书上提出的,当时我也没有想到,只能理解它了(另外这条代码是由我一位朋友整理,临表涕零感激不尽!),算法思想如下:
(1)问题分析:
在nn的国际象棋棋盘上的一只马,可按8个不同方向移动。定义nn的国际象棋棋盘上的马步图为G=(K,E)。棋盘上的每个方格对应于图G中的一个顶点,V={(i,j)0≤i,j<n}。从一个顶点到另一个马步可跳达的顶点之间有一条边 E= {(u, ), (s,0){ur s, |v-+1}={1,2}}。L图G有几2个顶点和4n2-12n+8条边。马的Hamilton周游路线问题即是图G的Hamilton回路问题。容易看出,当n为奇数时,该问题无解。事实上,由于马在棋盘上移动的方格是黑白相间的,如果有解,则走到的黑白格子数相同,因此棋盘格子总数应为偶数,然而n为奇数,此为矛盾。下面给出的算法可以证明,当n≥6是偶数时,问题有解,而且可以用分治法在线性时间内构造出一个解。
(2)构造结构化的解:
①|m-n|<=2,m,n>5,所以说最小的解是66,按照|m-n|<2规则可以继续扩展mm,m*(m+2)………在哪停止?当可以构造出6的2倍数也就是12的时候,结构化的解构造完毕。
②证明可以由基础解求得原问题的解
因为问题的可行域在|m-n|<=2,m,n>5范围内,二我们已经构造了612的结构化解,那么所有的解都能用612的解构造出,这是分治法核心
(3)划分和合并
①划分:
将棋盘尽可能平均地分割成4块。
当m,n=4k时,分割为2个2k;
当m,n=4k+2时,分割为1个2k和1个2k+2划分点记作nn1和mm1
两个原因,子问题必须是偶数,所以取模4,原问题是偶数所以只有4k和4k+2两种规模
① 合并:
合并方法采用临近合并,合并如图所示的8个点:
算法实现:
#include <iostream>
#include <fstream>
using namespace std;
struct grid
{
//表示坐标
int x;
int y;
};
class Knight{
public:
Knight(int m,int n);
~Knight(){};
void out0(int m,int n,ofstream &out);
grid *b66,*b68,*b86,*b88,*b810,*b108,*b1010,*b1012,*b1210,link[20][20];
int m,n;
int pos(int x,int y,int col);
void step(int m,int n,int a[20][20],grid *b);
void build(int m,int n,int offx,int offy,int col,grid *b);
void base0(int mm,int nn,int offx,int offy);
bool comp(int mm,int nn,int offx,int offy);
};
Knight::Knight(int mm,int nn){
int i,j,a[20][20];
m=mm;
n=nn;
b66=new grid[36];b68=new grid[48];
b86=new grid[48];b88=new grid[64];
b810=new grid[80];b108=new grid[80];
b1010=new grid[100];b1012=new grid[120];
b1210=new grid[120];
//cout<<"6*6"<<"\n";
ifstream in0("66.txt",ios::in); //利用文件流读取数据
ifstream in1("68.txt",ios::in); //利用文件流读取数据
ifstream in2("88.txt",ios::in); //利用文件流读取数据
ifstream in3("810.txt",ios::in); //利用文件流读取数据
ifstream in4("1010.txt",ios::in); //利用文件流读取数据
ifstream in5("1012.txt",ios::in); //利用文件流读取数据
for(i=0;i<6;i++)
{
for(j=0;j<6;j++)
{
in0>>a[i][j];
}
}
step(6,6,a,b66);
//cout<<"6*8"<<"\n";
for(i=0;i<6;i++)
{
for(j=0;j<8;j++)
{
in1>>a[i][j];
}
}
step(6,8,a,b68);
step(8,6,a,b86);
//cout<<"8*8"<<"\n";
for(i=0;i<8;i++)
{
for(j=0;j<8;j++)
{
in2>>a[i][j];
}
}
step(8,8,a,b88);
for(i=0;i<8;i++)
{
for(j=0;j<10;j++)
{
in3>>a[i][j];
}
}
step(8,10,a,b810);
step(10,8,a,b108);
//cout<<"10*10"<<"\n";
for(i=0;i<10;i++)
{
for(j=0;j<10;j++)
{
in4>>a[i][j];
}
}
step(10,10,a,b1010);
for(i=0;i<10;i++)
{
for(j=0;j<12;j++)
{
in5>>a[i][j];
}
}
step(10,12,a,b1012);
step(12,10,a,b1210);
}
//将读入的基础棋盘的数据转换为网格数据
void Knight::step(int m,int n,int a[20][20],grid *b)
{
int i,j,k=m*n;
if(m<n)
{
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
int p=a[i][j]-1;
b[p].x=i;
b[p].y=j;
}
}
}
else
{
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
int p=a[j][i]-1;
b[p].x=i;
b[p].y=j;
}
}
}
}
//分治法的主体部分
bool Knight::comp(int mm,int nn,int offx,int offy)
{
int mm1,mm2,nn1,nn2;
int x[8],y[8],p[8];
if(mm%2||nn%2||mm-nn>2||nn-mm>2||mm<6||nn<6) return 1;
if(mm<12||nn<12)
{
base0(mm,nn,offx,offy);
return 0;
}
mm1=mm/2;
if(mm%4>0)
{
mm1--;
}
mm2=mm-mm1;
nn1=nn/2;
if(nn%4>0)
{
nn1--;
}
nn2=nn-nn1;
//分割
comp(mm1,nn1,offx,offy);//左上角
comp(mm1,nn2,offx,offy+nn1);//右上角
comp(mm2,nn1,offx+mm1,offy);//左下角
comp(mm2,nn2,offx+mm1,offy+nn1);//右下角
//合并
x[0]=offx+mm1-1; y[0]=offy+nn1-3;
x[1]=x[0]-1; y[1]=y[0]+2;
x[2]=x[1]-1; y[2]=y[1]+2;
x[3]=x[2]+2; y[3]=y[2]-1;
x[4]=x[3]+1; y[4]=y[3]+2;
x[5]=x[4]+1; y[5]=y[4]-2;
x[6]=x[5]+1; y[6]=y[5]-2;
x[7]=x[6]-2; y[7]=y[6]+1;
for(int i=0;i<8;i++)
{
p[i]=pos(x[i],y[i],n);
}
for(int i=1;i<8;i+=2)
{
int j1=(i+1)%8,j2=(i+2)%8;
if(link[x[i]][y[i]].x==p[i-1])
link[x[i]][y[i]].x=p[j1];
else
link[x[i]][y[i]].y=p[j1];
if(link[x[j1]][y[j1]].x==p[j2])
link[x[j1]][y[j1]].x=p[i];
else
link[x[j1]][y[j1]].y=p[i];
}
return 0;
}
根据基础解构造子棋盘的Hamilton回路
void Knight::base0(int mm,int nn,int offx,int offy)
{
if(mm==6&&nn==6)
build(mm,nn,offx,offy,n,b66);
if(mm==6&&nn==8)
build(mm,nn,offx,offy,n,b68);
if(mm==8&&nn==6)
build(mm,nn,offx,offy,n,b86);
if(mm==8&&nn==8)
build(mm,nn,offx,offy,n,b88);
if(mm==8&&nn==10)
build(mm,nn,offx,offy,n,b810);
if(mm==10&&nn==8)
build(mm,nn,offx,offy,n,b108);
if(mm==10&&nn==10)
build(mm,nn,offx,offy,n,b1010);
if(mm==10&&nn==12)
build(mm,nn,offx,offy,n,b1012);
if(mm==12&&nn==10)
build(mm,nn,offx,offy,n,b1210);
}
void Knight::build(int m,int n,int offx,int offy,int col ,grid *b)
{
int i,p,q,k=m*n;
for(i=0;i<k;i++)
{
int x1=offx+b[i].x,y1=offy+b[i].y,x2=offx+b[(i+1)%k].x,y2=offy+b[(i+1)%k].y;
p=pos(x1,y1,col);
q=pos(x2,y2,col);
link[x1][y1].x =q;
link[x2][y2].y =p;
}
}
//计算方格的编号
int Knight::pos(int x,int y,int col)
{
return col*x+y;
}
void Knight::out0(int m,int n,ofstream &out)
{
int i,j,k,x,y,p,a[20][20];
if(comp(m,n,0,0))
return;
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
a[i][j]=0;
}
}
i=0;j=0;k=2;
a[0][0]=1;
out<<"(0,0)"<<"";
for(p=1;p<m*n;p++)
{
x=link[i][j].x;
y=link[i][j].y;
i=x/n;j=x%n;
if(a[i][j]>0)
{
i=y/n;
j=y%n;
}
a[i][j]=k++;
out<<"("<<i<<","<<j<<")";
if((k-1)%n==0)
{
out<<"\n";
}
}
out<<"\n";
for(i=0;i<m;i++)
{
for(j=0;j<n;j++)
{
out<<a[i][j]<<" ";
}
out<<"\n";
}
}
int main()
{
int m,n;
ifstream in("input.txt",ios::in); //利用文件流读取数据
ofstream out("output.txt",ios::out);//利用文件流将数据存到文件中
in>>m>>n;
Knight k(m,n);
k.comp(m,n,0,0);
k.out0(m,n,out);
in.close();
out.close();
}
4.算法的时空分析
运用算法分析的知识,分析并阐述求解本问题算法的各个模块的时空复杂度。
① 时间复杂度:T(n)=O(n^2)
对于comp算法,设算法复杂度为T(n),分治过程中划分了4个区域,并且规模下降为n/2,初次之外做了一些常数时间的划分和合并操作,由主定理可知:k=4,m=2,d=1,显然k>md,时间复杂度:O(n(log2(4)))=O(n^2) 对于out输出来说,算法需要遍历棋盘,所以复杂度为O(m*n)由于|m-n|<=2,复杂度也为O(n^2),综上,算法总的时间复杂度为T(n)=O (n^2)
② 空间复杂度:S(n)=O(n^4)
由于算法要存储棋盘,不可避免的需要O(m*n)的空间,但是空间复杂度的来源在于link[][]存储棋盘的时候要将二维的棋盘线性化,其规模为O(n2*n2)=O(n4),所以说算法空间复杂度为S(n)=O(n4)