打地鼠 Whack-a-mole

1. (简答题)

实战题:打地鼠

内容如附件所示:

Problem 5.pdf

测试数据为: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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值