简要说明:
(1)题目来源:课程(上机考题)。
(2)由于作者水平限制和时间限制,代码本身可能仍有一些瑕疵,仍有改进的空间。也欢迎大家一起来讨论。
——一个大二刚接触《数据结构》课程的菜鸡留
题目简介
- 你处在一个由M×N的网格组成的房间中,每个格子包含一个正整数x。行编号为1, 2, …, M,列编号为1, 2, …, N,记行编号为i、列编号为j的格子为(i,j)。房间从左上角的即(1,1)开始,从右下角(M,N)退出。如果你在值为x的格子中,则可以跳转到任意格子(a,b),其中满足a×b=x。
- 例如,给定6×6的房间,你目前处于包含值为6的格子中,那么你可以跳转到(1,6)、(2,3)、(3,2)、(6,1)中。如果给定房间是5×6的,你只能跳入(1,6)、(2,3)、(3,2)(因为没有第6行)。
- 编写一个程序,确定你是否可以从给定房间里逃出来。
你需要遵循以下输入格式:
- 输入第一行为整数M,表示房间的最大行编号(1≤M≤100);
- 输入第二行为整数N,表示房间的最大列编号(1≤N≤100);
- 剩余的输入给出房间的网格中存放的正整数,每个正整数xij都满足xij≤1000000。要求输入给出M行,每一行有N个用单个空格隔开的正整数。
参考样例:
输入样例:
3
4
3 10 8 14
1 11 12 12
6 2 3 9
输出样例:
yes1
思路分析
注:仅代表个人思路。
- 简要地来说,这道题是迷宫问题的一个“升级版本”,整体的思路仍然按照深度优先探查(DFS)来完成。由于题目只要求给出能否到达,如果需要找出路径总数将可能需要用到BFS算法,在此不表。由于和迷宫问题类似,如果对于迷宫问题及回溯法掌握不够深刻的读者可以依据上面链接或另寻题目加深理解。在这里假设读者已掌握迷宫问题的思路并能加以代码实现,在此将侧重点放在与迷宫问题不同的地方上。
- 在存储网格时,除了要存放对应的整数值xij,也需要存放一个标记来判断是否已经走过,否则当网格设计巧妙时会进入死循环,即反复在两个格子间跳跃。因此同迷宫问题,设两个数组int dot[M][N]和bool mark[M][N]。同样的,关于mark[M][N]=true带来的额外意义,在此不多赘述。
- 如何确定探测。由于题目要求是下标之积等于xij,因此这又涉及到寻找因数的算法。由于(i,j)和(j,i)(i≠j)代表的值不一定相等,因此在这里使用最朴素的依次遍历,从1开始,一直到xij本身。迷宫问题会走到边界,同样地本问题也有可能会跳出M×N中,因此探测方向的确定除了要看目标是否已经走过(mark[xdest][jdest]是否为true)之外,还需要判断是否在范围内(xdest≤M,ydest≤N?)。2
- 探测格子的存储,同迷宫问题一样压入栈中,同样除了坐标之外还需要保存当前探测的进度,以避免重复探测。由于a×b=xij,如果我们逐一升序探测的是a,当a探测到xij的最后一个因数即a=xij发现也无法到达终点后,就需要回退,栈顶弹出上一次的记忆位置。
代码部分
#include <iostream>
#include <stack>
using namespace std;
struct node {
node(int _I_=1, int _J_=1, int _CO_=1):i(_I_),j(_J_),co(_CO_) {}
int i;
int j;
int co; //表示第一坐标已经探测到多少.
};
class Dot {
public:
Dot(int m=0, int n=0);
bool solution(); //TODO:解决目标问题
private:
int *dot;
bool *mark;
int size_m;
int size_n;
int& askDot(int i, int j);
bool& askMark(int i, int j);
bool isInDot(int i, int j);
};
//Dot类的构造函数
Dot::Dot(int m, int n) {
size_m=m;
size_n=n;
dot=new int[m*n];
mark=new bool[m*n];
for (int i=0;i<m;i++)
for (int j=0;j<n;j++) {
scanf("%d",&dot[i*n+j]);
mark[i*n+j]=false;
}
}
//访问dot数组的行为i,列为j的元素(第一行第一列分别为1)
int& Dot::askDot(int i, int j) {
return dot[(i-1)*size_n+(j-1)];
}
//访问mark数组的行为i,列为j的元素(第一行第一列分别为1)
bool& Dot::askMark(int i, int j) {
return mark[(i-1)*size_n+(j-1)];
}
//坐标i和j是否越界
bool Dot::isInDot(int i, int j) {
return (i<size_m+1)&&(j<size_n+1);
}
//TODO
bool Dot::solution() {
stack<node> st;
//c_i, c_j表示当前格子的行坐标与列坐标, e_i和e_j表预期, c_co表当前探测到的目标行坐标值.
int c_i,c_j,e_i,e_j,c_co,c_data;
st.push(node(1,1,1));
askMark(1,1)=true;
while (!st.empty()) {
c_i=st.top().i;
c_j=st.top().j;
c_data=askDot(c_i,c_j);
c_co=st.top().co;
//直到找到可以被c_data整除的co.
while (c_co<=c_data&&c_data%c_co!=0) c_co++;
if (c_co>c_data) { //代表当前结点探测完成了, 需要回退.
st.pop();
continue;
}
e_i=c_co;
e_j=c_data/c_co;
if (!isInDot(e_i,e_j)||askMark(e_i,e_j)) { //表示越界或已经走过, 需要修正.
st.top().co=c_co+1;
}
//接下去是正常情况.
else if (e_i==size_m&&e_j==size_n) return true; //表示找到了这条路.
else { //表示需要探测目标格子.
askMark(e_i,e_j)=true;
st.push(node(e_i,e_j,1));
}
}
return false;
}
int main() {
int m,n;
cin>>m>>n;
Dot d(m,n);
if(d.solution()) cout<<"yes";
else cout<<"no";
}
改进空间
- 这段代码由于是当堂写的,有一小部分空间仍然可以改进。例如在针对目标格子越界或已经访问过的判断上,采用了修改栈顶元素并进入下一次循环的方法,事实上是可以改进的。初步考虑下来循环体可以改进如下:
//直到找到可以被c_data整除的co, 并且目标是可以进行下一步探测的.
while (c_co<=c_data&&c_data%c_co!=0&& (!isInDot(c_co,c_data/c_co)||askMark(c_co,c_data/c_co) )
c_co++;
if (c_co>c_data) { //代表当前结点探测完成了, 需要回退.
st.pop();
continue;
}
e_i=c_co;
e_j=c_data/c_co;
//接下去是正常情况.
//if ... else ..
- 与之前迷宫问题类似,迷宫问题的代码并没有严格仿照思路来写,因此也可以将代码改成如迷宫问题类似的代码,在此略去。
- 如果使用广度优先原则?
补充部分
- 如果题目要求不仅仅判断能否到达,还要给出一条路径并打印,格式仿照参考样例和注脚1的格式。这个实现是比较简单的,可以考虑使用另一个栈相互出栈入栈,这样将栈中的所有数据的顺序颠倒一下,最后依次输出。需要注意的是,按照本文代码,终点是没有入栈的,因此在打印时需要注意补上终点,或者在到达终点前先入栈。代码部分可修改如下:
//if ...
//接下去是正常情况.
else if (e_i==size_m&&e_j==size_n) {
//在这里进行修改. 方法是使用第二个栈.
stack<node> ast;
//借助ast, 在ast中放入相反顺序的数据.
while (!st.empty()) {
ast.push(st.top());
st.pop();
}
//ast顺序整理完毕, 逐一输出.
while (!ast.empty()) {
cout<<'('<<ast.top().i<<','<<ast.top().j<<")->";
ast.pop();
}
//最后补充一下终点.
cout<<'('<<size_m<<','<<size_n<<')';
//或者也可以在最开始的时候往st中压入终点的node, 即node(size_m,size_n).
//正常地返回true.
return true;
}
//else ... 表示需要探测目标格子.
- 如果题目在此基础上再作加难,要求打印所有可能的简单路径,其中简单路径的定义是路径中不出现重复的网格,格式仿照参考样例和注脚1的格式。关于这个问题的实现,如果继承目前已经完成解决的DFS算法的思路应该如何实现?我个人的想法是回到终点后不立刻输出,而是打印路径。在打印路径如果借助第二个栈ast,则需要在从ast弹出数据并打印时同时重新压入st,以保留当前的记忆。按照这样的方法能够访问几次终点,就说明会有几条路径。同时对于mark[M][N]来说,当退回之前的网格时,需要重新设置mark[i][j]=false,因为有可能两条不同的路径中出现了公共网格,但两条路径的网格及其顺序可能不同。这样的改进带来的好处是规避了错误,但同时降低了探测路径的效率。
- 继续第2点的讨论,如果使用BFS算法该如何实现?比较棘手的问题是如何避免可能出现回路,即在当前格跳跃到之前已经访问过的格子。由于广度优先原则导致在考察队列中的两个相邻元素时可能是针对不同路径而言的,一个比较朴素的方法是修改node数据结构,在储存具体的行坐标和列坐标之外,存储之前网格的信息。一种方法是存储所有的网格,使用一个(或两个,对应两个坐标)数组;抑或是使用new给每次探查到的格子,根据之前结点信息分配空间,这样就形成了一个带有前驱指针的链表。这样,可能同一个网格因出现在不同的路径中,可能会被分配多次。出现死路时同样可以释放内存,这涉及到线性表中链表的相关知识,在此不多赘述。
- 说一些比较主观的事吧。一来我并不知道这道题是助教原创的还是什么样的,反正毕竟也是当堂做的,也是对于迷宫问题和DFS算法的一次巩固。再者,当我想写一篇博客时,室友也在“煽动”我写,因此就赶出了这么一篇博客,既能记下我在考场上和过了一晚的今天从不同的角度回顾这个问题的不同思路,也能给大家提供一道有趣的样题吧。