背景介绍
骑士旅行问题想必大家都很熟悉了吧
这道题的众人向做法就是深度搜索(直接给出一个很C_T风格的代码):
#include<iostream>
#include<iomanip>
using namespace std;
int zz[8][2]={{1,2},{2,1},{1,-2},{-2,1},{-1,2},{2,-1},{-1,-2},{-2,-1}};
int a[8][8]={0},tot=0,_x,_y;
void print() {
cout<<++tot<<endl;
for (int i=0;i<8;i++) {
for (int j=0;j<8;j++)
cout<<setw(3)<<a[i][j];
cout<<endl;
}
return;
}
void doit(int x,int y,int t) {
if (t>64) {
print();
return;
}
for (int i=0;i<8;i++) {
int xx=x+zz[i][0];
int yy=y+zz[i][1];
if ((xx>=8)||(xx<0)||(yy>=8)||(yy<0)||(a[xx][yy])) continue;
a[xx][yy]=t;
doit(xx,yy,t+1);
a[xx][yy]=0;
}
}
int main()
{
cout<<"Enter the initial position: ";
cin>>_x>>_y;
a[_x][_y]=1;
doit(_x,_y,2);
system("pause");
return 0;
}
下面的代码找到一个可行方案就停止搜索(但是在得到可行解之后的回溯过程意外的有些慢):
#include<iostream>
#include<iomanip>
using namespace std;
int zz[8][2]={{1,2},{2,1},{1,-2},{-2,1},{-1,2},{2,-1},{-1,-2},{-2,-1}};
int a[8][8]={0},tot=0,_x,_y;
bool flag=1;
void print() {
for (int i=0;i<8;i++) {
for (int j=0;j<8;j++)
cout<<setw(3)<<a[i][j];
cout<<endl;
}
return;
}
void doit(int x,int y,int t) {
if (t>64) {
flag=0;
print();
return;
}
for (int i=0;i<8&&flag;i++) {
int xx=x+zz[i][0];
int yy=y+zz[i][1];
if ((xx>=8)||(xx<0)||(yy>=8)||(yy<0)||(a[xx][yy])||(!flag)) continue;
a[xx][yy]=t;
doit(xx,yy,t+1);
a[xx][yy]=0;
}
}
int main()
{
cout<<"Enter the initial position: ";
cin>>_x>>_y;
a[_x][_y]=1;
doit(_x,_y,2);
system("pause");
return 0;
}
总的来说,这是一种很暴力的算法,相当于无优先级穷举所有可能路径
这就会带来一个问题:有些起始位置以当前的穷举顺序是无法在有限时间内得到可行解的
以前小憨憨没有考虑过这个问题,直到C先生不断睿智发问,我才意识到这几年我一直停留在众人向做法毫无进步
所以今天,C_T要带来一种更加优秀的做法!
透试算法研究
现在我们希望开发一个移动骑士的试探策略
试探不一定成功,但认真设计的试探方法能提高成功的机会
显然,外层格子比在棋盘中央的壳子更难移动,实际上,最难访问的是四角的格子
这里所谓的 ” 难访问 “ ,实际上就是相比较其他格子,有更少的格子能够到达该处
反过来说,就是从这个格子出发走一步能够到达的格子数更少
嗯?不太明白吗?
下面给出一个棋盘,每一格上标记能访问的格数(盲猜有助于理解)
我叫ta前进策略图(好正常的名字,不像我的一贯作风),格子上的值我就叫ta前进可能值:
2 3 4 4 4 4 3 2
3 4 6 6 6 6 4 3
4 6 8 8 8 8 6 4
4 6 8 8 8 8 6 4
4 6 8 8 8 8 6 4
4 6 8 8 8 8 6 4
3 4 6 6 6 6 4 3
2 3 4 4 4 4 3 2
凭直觉,应先将骑士移动到最难到达的格子,在旅行即将结束时再访问最容易到达的格子,这样成功的机率较高
意识流一下就可以发现,骑士起始时的路径选择应该尽可能少一点
因为在后面试探失败进行回溯的时候,如果起始时的路径选择较多,回溯引发的搜索时间代价会大幅上升
我们可以开发一个访问性试探,将每格进行访问性分类,然后总是设法把骑士移到最难访问的点
一句话总结:任何时候,总是设法将骑士移动到最难访问的点
从这句话中,我们就可以看出这个策略要维护的重点:
每走一步,我们就需要重新维护一遍前进策略图
并且每次在枚举策略的时候,按照该点周围前进可能值由小到大的顺序进行枚举
我在说些什么泡泡茶壶!!!!还是画图告诉大家吧!
然后可能就会有人问了,这三个 " 8 " 有没有什么先后顺序呢?
实际上应该可以有的,毕竟一号8和三号8比二号8更靠近边缘,选择一号8或者三号8,对于之后的选择可能更加有利,
但是如果这样区别的话,就牵扯到处理下一阶段的前进策略图了
整个问题的难度就会大幅上升,甚至与之前我们想要降低复杂度的初衷背道而驰,所以我们在这里初步简单的认为,这三个8先选择哪个都没有区别
那么我们的编程思路终于浮出水面了!!!!
在搜索函数中,
首先依据当前的前进策略图得到 ( x , y ) 处的最优前进策略
按照最有前进策略前进后,维护最新的前进策略图,之后进入下一层搜索
在确定这个算法时,我经历了痛苦的思维地狱:到底应该先维护前进策略图还是先找到最优前进策略?
显然,如果一个位置已访问,那么该点的前进可能值就可以视为正无穷(优先级最低)
但是我们还要保证能成功回溯,所以不能在初始数组上直接修改,需要复制地图
如果我头铁直接在原数组上修改,并且取消回溯过程,就相当于认定了在这种类似贪心的算法下一定能找到可行解,不过我怎么证明这种算法的解必存在性呢?
就算我们姑且抛开这个问题不考虑(默认正确)
在搜索过程中,先维护前进策略图还是先找到最优前进策略实际上是毫无区别的
但是因为我们初始点是直接进入搜索的,这个初始点的加入实际上打破了之前的前进策略图的
严谨起见,我们还是应该先维护前进策略图
而且维护前进策略图和获得最优策略的操作完全分开会比较清晰一点
但是为了压缩常数,实际上这两种操作可以杂糅一下
TIP
啊啊啊啊啊啊啊啊,我好蠢啊
整个思路的完善C先生帮了我很大的忙,比心心~
一开始我还以为每一次都要全棋盘维护前进策略图,然后C先生告诉我说每走一步只会影响到ta周围的可到达点
枚举策略顺序也不用全部抽出来排序,只要找到当前情况下的最优方案就可以了
(我真的是学了很多年竞赛的OIer嘛,被踩了啊TwT)
下面给出的是舍弃回溯的不完善代码,但是在博主的实践下,发现除了 ( 3 , 4 ) ( 4 , 3,) 点,其他点都可以靠这种 “ 贪心 ” 一次成功
#include<iostream>
#include<iomanip>
using namespace std;
const int INF=0x33333333;
int zz[8][2]={{1,2},{2,1},{1,-2},{-2,1},{-1,2},{2,-1},{-1,-2},{-2,-1}};
int strategy_map[8][8]={{2,3,4,4,4,4,3,2},
{3,4,6,6,6,6,4,3},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{3,4,6,6,6,6,4,3},
{2,3,4,4,4,4,3,2}};
int ans[8][8]={0},_x,_y;
bool flag=1;
void print() {
for (int i=0;i<8;i++) {
for (int j=0;j<8;j++)
cout<<setw(3)<<ans[i][j];
cout<<endl;
}
return;
}
int update(int x,int y) {
int mn=8,pos=9;
strategy_map[x][y]=INF;
for (int i=0;i<8;i++) {
int xx=x+zz[i][0];
int yy=y+zz[i][1];
if ((xx<0)||(xx>=8)||(yy<0)||(yy>=8)||ans[xx][yy]) continue;
strategy_map[xx][yy]--;
if (strategy_map[xx][yy]<mn) mn=strategy_map[xx][yy],pos=i;
}
return pos;
}
void doit_try(int x,int y,int t) {
if (t>64) {
flag=0;
print();
return;
}
int num=update(x,y);
if (num==9) return;
int xx=x+zz[num][0];
int yy=y+zz[num][1];
ans[xx][yy]=t;
doit_try(xx,yy,t+1); //省略回溯
return;
}
int main()
{
cout<<"Enter the initial position: ";
cin>>_x>>_y;
ans[_x][_y]=1;
doit_try(_x,_y,2);
system("pause");
return 0;
}
之后我和C先生又花了将近两天的时间研发出了回溯算法
主要还是C先生的功劳啦,谢谢大大对我不离不弃TwT
这里对于我们的思路再罗嗦两句~
回溯问题的难点就在于前进策略图的维护
我一开始的思路被INF的设定堵塞了
实际上,可以通过ans数组直接把这些点踢出讨论组
编辑一个update数组搞定更新地图的任务
之后我们可以再增加一个数组,记录8个方向的可通行性
这样在回溯get新的方向的时候,就可以排除掉之前的方案,选择剩下方案中的最优解
献!丑!了!
#include<iostream>
#include<iomanip>
using namespace std;
const int INF=0x33333333;
int strategy_map[8][8]={{2,3,4,4,4,4,3,2},
{3,4,6,6,6,6,4,3},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{4,6,8,8,8,8,6,4},
{3,4,6,6,6,6,4,3},
{2,3,4,4,4,4,3,2}};
int zz[8][2]={{1,2},{2,1},{1,-2},{-2,1},{-1,2},{2,-1},{-1,-2},{-2,-1}};
int ans[8][8]={0},_x,_y,tot;
void print() {
for (int i=0;i<8;i++) {
for (int j=0;j<8;j++)
cout<<setw(3)<<ans[i][j];
cout<<endl;
}
cout<<endl;
return;
}
void update(int x,int y,int z) {
for (int i=0;i<8;i++) {
int xx=x+zz[i][0];
int yy=y+zz[i][1];
if ((xx<0)||(xx>=8)||(yy<0)||(yy>=8)) continue;
strategy_map[xx][yy]+=z;
}
return;
}
int get_num(int x,int y,bool acc[]) {
int mn=8,pos=9;
for (int i=0;i<8;i++) {
if (!acc[i]) continue;
int xx=x+zz[i][0];
int yy=y+zz[i][1];
if ((xx<0)||(xx>=8)||(yy<0)||(yy>=8)||ans[xx][yy]) continue;
//注意这里的前提条件有ans[xx][yy]==0
if (strategy_map[xx][yy]<mn) mn=strategy_map[xx][yy],pos=i;
}
return pos;
}
void doit_try(int x,int y,int t) {
if (t>64) {
tot--;
print();
return;
}
bool accessible[8]; //方向可通行性
memset(accessible,1,sizeof(accessible));
update(x,y,-1); //更新地图
int num=get_num(x,y,accessible); //get最优策略
while (num!=9) {
int xx=x+zz[num][0];
int yy=y+zz[num][1];
accessible[num]=0; //之后即使发生回溯也不可能选择这个方向了
ans[xx][yy]=t;
doit_try(xx,yy,t+1);
if (!tot) return;
ans[xx][yy]=0;
num=get_num(x,y,accessible); //get最优策略
}
update(x,y,1); //恢复地图进行回溯
return;
}
int main()
{
cout<<"Enter the initial position: ";
cin>>_x>>_y;
cout<<"Methods number: ";
cin>>tot;
ans[_x][_y]=1;
doit_try(_x,_y,2);
system("pause");
return 0;
}