1. (简答题)
实战题:打地鼠
内容如附件所示:

测试数据为:1,2,4,8,9,10,11,14 答案为:10,2,4
原始分布:

击打10号

击打2号

击打4号

要求,所示实例解以图示的方式给出,并且5组测试数据都需要测试,还要有无解数据测试,及游戏方式(给出初始状态,由用户输入敲几号,给出变化状态)。代码要给详细注释并贴图上传,运行结果贴图上传,源文件做为附件上传。
1.递归做法
#include <iostream>
#define N 4
#define M 4
#define MAXDEPTH 8
#define DIRECTION 4
/*
* N: 行号
* M: 列号
* MAXDEPTH: 递归最大深度
* DIRECTION: 方向数
*/
using namespace std;
/* 定义图案的数据类型 只存放01, 可以只用char类型 */
typedef char ElemType;
/* 定义方向, 分别是右下左上 */
const int dx[] = {0, 1, 0, -1};
const int dy[] = {1, 0, -1, 0};
/* 原始数组 和 当前数组 */
ElemType ori[N * M], cur[N * M];
/* “敲打”数组, 存储每次敲了哪些地鼠 */
int kno[MAXDEPTH];
/* 储存答案 及 答案的长度 */
int ans[MAXDEPTH]; int ans_length;
/* 改变pos及其上下左右的状态 */
// change一次, 能模拟敲击一次的效果; 再change一次, 能恢复到敲击之前的状态; 当然要先由其它函数判断是否可敲
void change(ElemType arr[], ElemType pos)
{
// ^ 按位异或; 特别地, 1 ^1 = 0, 0 ^1 = 1
arr[pos] ^= 1;
int x = pos / M, y = pos % M;
for (int i = 0; i < DIRECTION; ++i)
{
int xx = x + dx[i], yy = y + dy[i];
if (xx >= 0 && xx < N && yy >= 0 && yy < M)// 看各个方向是否越界, 如果没有, 就改变当前的状态
arr[xx * M + yy] ^= 1;
}
}
/* 更新当前ans */
void update(int now_depth)
{
// 如果当前深度(0开始)大于已存储的答案的长度(0开始), 则无需更新
if (now_depth > ans_length) // 改为 >= 可得到字典序最小的答案, 此处不加, 与样例的字典序最大的答案保持一致
return;
for (int i = 0; i <= now_depth; ++i)
{
ans[i] = kno[i];
}
ans_length = now_depth; // 更新答案的长度
}
/* 检查是否找到答案 */
void check(int now_depth)
{
for (int i = 0; i < N * M; ++i)
{
if (cur[i]) // 遇到一个1, 就证明当前状态不是答案
return;
}
update(now_depth); // 找到一种敲法, 让update函数判断是否需要更新ans
}
/* 递归每一层代表第i次的敲击 穷举指定次数内所有合法的可能的敲法 */
void dfs(int now_depth) // now: 当前递归层数, 从0开始
{
if (now_depth >= MAXDEPTH) // 判断结束继续递归的条件
return;
for (int i = 0; i < N * M; ++i) // 0 - N * M 选择一个可以敲的地方
{
if (cur[i]) // 如果可以敲
{
kno[now_depth] = i; // 记录当前敲击的地方
change(cur, i); // 敲下去
check(now_depth); // 检查是否找到敲法
dfs(now_depth + 1); // 递归到下一层
change(cur, i); // 回到此处时, “敲回来”, 回溯到之前没有敲击的状态;
}
}
}
/* 答案可视化 */
void visu(ElemType arr[])
{
// 输出当前状况
for (int i = N - 1; i >= 0; --i) // 为了与样例图案的数字顺序保持一致, 倒序输出各行
{
for (int j = 0; j < M; ++j)
{
cout << (arr[i * M + j] ? "●" : "○"); // 这符号非ascii表中的字符, 使用双引号
}
cout << endl;
}
cout << endl;
}
int main()
{
int i, t, n;
for (i = 0; i < N * M; ++i) cur[i] = ori[i] = 0; // 初始化原数组和当前数组 0代表没有地鼠
cout << "请输入地鼠数目: "; cin >> n;
cout << "请输入地鼠的分布情况: ";
for (i = 0; i < n; ++i)
{
cin >> t; --t; // 转化为物理位序
cur[t] = ori[t] = 1; // 1 代表有地鼠
}
ans_length = MAXDEPTH + 1; // 默认ans长度为最大深度+1, 好判断是否找到过答案
dfs(0); // 从第0层开始判断是否找到答案
if (ans_length == MAXDEPTH + 1)
cout << "该情况在" << MAXDEPTH << "次敲击内无解" << endl;
else
{
cout << "敲击次数最少的敲法为: ";
for (i = 0; i <= ans_length; ++i)
cout << ans[i] + 1 << ' '; // 输出时转化为逻辑位序, 下同
cout << endl << endl;
cout << "原始状态为: " << endl; visu(ori); // ori数组保留了最初的状态, 先输出
for (i = 0; i <= ans_length; ++i)
{
cout << "敲了" << ans[i] + 1 << "后的状态为: " << endl; // 输出对应的操作
change(ori, ans[i]); // 改变ori, 模拟敲击
visu(ori); // 输出图案
}
}
return 0;
}
2.队列做法
#include <iostream>
#define N 4
#define M 4
#define MAXSIZE 2000000
#define DIRECTION 4
/*
* N: 行号
* M: 列号
* MAXSIZE: 最大队列节点数
* DIRECTION: 方向数
*/
using namespace std;
/* 队列内的二维数组的数据类型, 因为只存放0和1, 故用char类型也可以 */
typedef char ElemType;
/*
* 定义队列的每一个节点存储的数据
* pre: 前驱节点的下标
* kno: 敲了第几个之后变成该cur数组
* cur: 二维数组, 存放敲了kno之后剩下的图案
*/
typedef struct {
int pre, kno;
ElemType cur[N][M];
} Box;
/* 定义队列 */
typedef struct {
Box data[MAXSIZE];
int front, rear;
} Queue;
/* 定义方向, 分别是右下左上 */
const int dx[] = {0, 1, 0, -1};
const int dy[] = {1, 0, -1, 0};
/* 原始数组, 存储最开始的情况 */
ElemType ori[N][M];
/* --------------------------------------队列相关操作开始-------------------------------------- */
/* 初始化队列 */
void InitQueue(Queue*& q)
{
q = new Queue;
q->front = q->rear = 0;
}
/* 进队列; 注: 有可能因无解导致拓展节点数过多或者队列最大节点数过少, 导致进队列失败 */
bool EnQueue(Queue*& q, Box& e)
{
if (q->rear + 1 > MAXSIZE)
return false;
q->data[q->rear++] = e;
return true;
}
/* 判断队列是否为空 */
bool QueueEmpty(Queue* q)
{
return q->front == q->rear;
}
/* 获取队头元素 */
bool GetFront(Queue* q, Box& e)
{
if (QueueEmpty(q))
return false;
e = q->data[q->front];
return true;
}
/* 为了对应课本的各种操作, 此处DeleQueue时得到弹出的节点, 但是在求解本题中并不需要得到弹出节点 */
bool DeleQueue(Queue* q, Box& e)
{
if (QueueEmpty(q))
return false;
e = q->data[q->front++];
}
/* 销毁队列 */
void DestroyQueue(Queue* q)
{
delete q;
}
/* --------------------------------------队列相关操作结束-------------------------------------- */
/* 检查是否找到答案 */
bool check(Box& e)
{
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < M; ++j)
{
if (e.cur[i][j]) //只有还有一个值为1
return false;
}
}
return true; //所有值都为0,则返回true
}
/* 改变第x行y列, 及其上下左右的情况 */
void change(Box& e, int x, int y)
{
// ^ 按位异或; 特别地, 1 ^1 = 0, 0 ^1 = 1
e.cur[x][y] ^= 1;
for (int i = 0; i < DIRECTION; ++i)
{
int xx = x + dx[i], yy = y + dy[i];
if (xx >= 0 && xx < N && yy >= 0 && yy < M)// 看各个方向是否越界, 如果没有, 就改变对应位置上的状态
e.cur[xx][yy] ^= 1;
}
}
/* 求解最短步骤 */
bool minStep(Queue*& q)
{
int i, j;
Box e;
// 将当前状况存到e中, 加入队列, 特别地, 得到该情况的操作kno = -1, 该节点的前驱节点为-1, 代表该节点为第一个节点
for (i = 0; i < N; ++i)
{
for (j = 0; j < M; ++j)
{
e.cur[i][j] = ori[i][j];
}
}
e.kno = -1;
e.pre = -1;
EnQueue(q, e);
// 由于该题的情况是无序的, 所以不存在队列为空(即front == rear) 的情况, 故用一个死循环重复判断(队列满或找到答案终止)
while (true)
{
GetFront(q, e);// 取队头
for (i = N - 1; i >= 0; --i) // 逆序查找敲击位置, 得到字典序最大的答案, 与样例保持一致
{
for (j = M - 1; j >= 0; --j)
{
// 找到一个可以敲的地方
if (e.cur[i][j])
{
// 敲下去, 得到新的情况
change(e, i, j);
// 该情况是由当前队头元素扩展而来, 故前驱为队头下标
e.pre = q->front;
// 这是敲了第几个得来的, 顺便转化为逻辑位序
e.kno = i * M + j + 1;
// 将新得到的节点加入队尾, 如果加不了, 那么求解失败
if (!EnQueue(q, e))
return false;
// 加入队尾后再来判断是否找到答案, 如果找到, 将队尾就是最终状态
if (check(e))
return true;
// 再次取队头, 继续寻找队头下一个可扩展的点
GetFront(q, e);
}
}
}
DeleQueue(q, e); // 当前队头的所有可扩展情况都加入到了队尾, 队头指针向后移动
}
}
/* 递归输出操作的情况 */
void dispStep(Queue* q, int now)
{
// 找到起始情况的节点时, 终止继续递归
if (now == 0)
return;
// 如果不是第一个节点, 那么通过前驱继续查找
dispStep(q, q->data[now].pre);
// 如果到了这里, 证明找到第一个节点, 且前面的操作都已输出, 则可以输出当前节点的操作
cout << q->data[now].kno << ' ';
}
/* 答案可视化 */
void visu(Queue* q, int now)
{
// 与输出答案稍微有点不同, 这里要输出起始情况
int pre = q->data[now].pre;
int i;
// 如果前驱节点的下标不为-1(即当前节点不是第一个节点)
if (pre != -1)
{
visu(q, pre);// 递归到前驱节点
// 非第一个节点, 找到第一个节点并回到此处时, 输出相应提示
cout << "敲了" << q->data[now].kno << "后的状态为: " << endl;
}
else
{
// pre = -1, 该节点是第一个节点, 输出相应的提示
cout << "原始状态为: " << endl;
}
// 输出当前状况
for (i = N - 1; i >= 0; --i) // 为了与样例图案的数字顺序保持一致, 倒序输出各行
{
for (int j = 0; j < M; ++j)
{
cout << (q->data[now].cur[i][j] ? "●" : "○"); // 这符号非ascii表中的字符, 使用双引号
}
cout << endl;
}
cout << endl;
}
int main() {
int i, j, t, n;
Queue* q1; InitQueue(q1);
for (i = 0; i < N; ++i)
{
for (j = 0; j < M; ++j)
{
ori[i][j] = 0; // 0代表没有地鼠
}
}
cout << "请输入地鼠数目: "; cin >> n;
cout << "请输入地鼠的分布情况: ";
for (i = 0; i < n; ++i)
{
cin >> t; --t; // 转化为物理位序
ori[t / M][t % M] = 1; //对应的行号就是t / M, 列号就是t % M, 注意这里都是与列号做运算
// 1 代表有地鼠
}
if (minStep(q1)) // 找到结果的话就输出步骤及答案的可视化过程
{
cout << "敲击次数最少的敲法为: ";
dispStep(q1, q1->rear - 1); // rear指针始终指向最后一个有效节点的下一个位置, 故 -1 后为最后的节点
cout << endl << endl;
visu(q1, q1->rear - 1); // 将最小步骤的敲地鼠的过程可视化
}
else
cout << "无法对该图求解" << endl; // 无解, 或者队列最大节点数过小无法求解
DestroyQueue(q1);
return 0;
}
3.栈的做法
#include <iostream>
#define N 4
#define M 4
#define MAXSIZE 8
#define DIRECTION 4
/*
* N: 行数
* M: 列数
* MAXSIZE: 最大栈节点数
* DIRECTION: 方向数
*/
using namespace std;
/* 定义图案的数据类型 只存放01, 可以只用char类型 */
typedef char ElemType;
/* 定义栈 */
/* kno数组: 存储每次敲击的位置 top: 栈顶指针 */
typedef struct {
int kno[MAXSIZE];
int top;
} Stack;
/* 储存答案的数组 答案的长度 */
int ans[MAXSIZE]; int ans_length;
/* 定义方向, 分别是右下左上 */
const int dx[] = {0, 1, 0, -1};
const int dy[] = {1, 0, -1, 0};
/* 定义两个数组, 分别存储最开始的状态, 和求解过程中的状态 */
ElemType ori[N * M], cur[N * M];
/* --------------------------------------栈相关操作开始-------------------------------------- */
/* 初始化栈 */
void InitStack(Stack*& s)
{
s = new Stack;
s->top = -1;
}
/* 判断栈是否为空 */
bool StackEmpty(Stack* s)
{
return -1 == s->top;
}
/* 进栈 */
bool Push(Stack* s, ElemType kno)
{
if (s->top + 1 >= MAXSIZE)
return false;
s->kno[++s->top] = kno;
return true;
}
/* 出栈 */
bool Pop(Stack* s, ElemType& e)
{
if (StackEmpty(s))
return false;
e = s->kno[s->top--];
return true;
}
/* 获取栈顶元素 */
bool GetTop(Stack* s, ElemType& e)
{
if (StackEmpty(s))
return false;
e = s->kno[s->top];
return true;
}
/* 销毁栈 */
void DestroyStack(Stack*& s)
{
delete s;
}
/* --------------------------------------栈相关操作结束-------------------------------------- */
/* 更新ans */
void update(Stack* s)
{
// 遇到s->top == ans_length 时, 仍会更新ans
// 最终得到的较后一点的最少敲法, 使得最终的答案的字典序最大, 与样例保持一致
if (s->top > ans_length)
return;
for (int i = 0; i <= s->top; ++i)
{
ans[i] = s->kno[i];
}
ans_length = s->top;
}
/* 检查是否找到答案 */
void check(Stack* s)
{
for (int i = 0; i < N * M; ++i)
{
if (cur[i])
return;
}
update(s); // 找到一种敲法, 让updata函数判断是否需要更新
}
/* 改变pos及其上下左右的状态 */
void change(ElemType arr[], int pos)
{
// ^ 按位异或; 特别地, 1 ^1 = 0, 0 ^1 = 1
arr[pos] ^= 1;
int x = pos / M, y = pos % M;
for (int i = 0; i < DIRECTION; ++i)
{
// 看各个方向是否越界, 如果没有, 就改变目标方向上的状态
int xx = x + dx[i], yy = y + dy[i];
if (xx >= 0 && xx < N && yy >= 0 && yy < M)
arr[xx * M + yy] ^= 1;
}
}
/* 返回从now开始, 第一个可以敲击的位置, 如果没有返回-1 */
int toNext(ElemType now)
{
for (int i = now; i < N * M; ++i)
{
if (cur[i])
return i;
}
return -1;
}
/* 在所有敲法中寻找最少次数的敲法 */
void minStep(Stack* s)
{
ans_length = MAXSIZE + 1; // 初始化ans_length 便于找最小值
int pos = toNext(0); // 找第一个能敲的地方
ElemType e;
Push(s, pos); // 将该位置进栈
change(cur, pos); // 改变cur数组, 模拟出敲击的效果
check(s); // 检查下是否敲一次就是答案
while (!StackEmpty(s)) // 当栈不为空时
{
GetTop(s, e); // 取栈顶元素
if (e == -1) // 如果这个元素是-1的话
{
Pop(s, e); // 弹出该元素
if (!StackEmpty(s)) // 如果弹出后不为空栈的话
{
Pop(s, e); // 弹出头部并得到该元素
change(cur, e); // 恢复到敲击前的图案
pos = toNext(e + 1);// 找下一个可以敲击的位置
Push(s, pos); // 先进栈(即使是非法位置) (因为如果是非法位置的话, 就要改变前一个元素了)
if (pos != -1) // 如果不是非法位置的话
{
change(cur, pos); // 敲下去
check(s); // 检查是否找到一种敲法
}
}
}
else if (MAXSIZE == s->top + 1) // 当栈满的时候
{
Pop(s, e); // 弹出并得到头部
change(cur, e); // 恢复到敲击以前的状态
pos = toNext(e + 1); // 寻找下一个可以敲击的位置
Push(s, pos); // 进栈
if (pos != -1)
{
change(cur, pos);
check(s);
}
}
else
{
pos = toNext(0); // 栈顶不为非法位置, 且栈没满的话, 找下一个可以敲击的位置
Push(s, pos); // 先进栈
if (pos != -1)
{
change(cur, pos);
check(s);
}
}
}
}
/* 答案可视化 */
void visu(ElemType arr[])
{
// 输出当前状况
for (int i = N - 1; i >= 0; --i) // 为了与样例图案的数字顺序保持一致, 倒序输出各行
{
for (int j = 0; j < M; ++j)
{
cout << (arr[i * M + j] ? "●" : "○"); // 这符号非ascii表中的字符, 使用双引号
}
cout << endl;
}
cout << endl;
}
int main()
{
int i, t, n;
Stack* s1; InitStack(s1);
for (i = 0; i < N * M; ++i) ori[i] = cur[i] = 0; // 默认没有地鼠为0
cout << "请输入地鼠数目: "; cin >> n;
cout << "请输入地鼠的分布情况: ";
for (i = 0; i < n; ++i)
{
cin >> t; --t; // 转物理位序
ori[t] = cur[t] = 1; // 有地鼠为1
}
minStep(s1); // 求解敲击次数最少的方法
if (ans_length == MAXSIZE + 1) // 没有更新过答案的话,证明在指定长度内无解 或 真的无解
cout << "该情况在" << MAXSIZE << "次敲击内无解" << endl;
else
{
cout << "敲击次数最少的敲法为: ";
for (i = 0; i <= ans_length; ++i)
cout << ans[i] + 1 << ' '; // 输出时转化为逻辑位序, 下同
cout << endl << endl;
cout << "原始状态为: " << endl; visu(ori); // ori数组保留了最初的状态, 先输出
for (i = 0; i <= ans_length; ++i)
{
cout << "敲了" << ans[i] + 1 << "后的状态为: " << endl; // 输出对应的操作
change(ori, ans[i]); // 改变ori, 模拟敲击
visu(ori); // 输出图案
}
}
DestroyStack(s1);
return 0;
}
4706

被折叠的 条评论
为什么被折叠?



