程序设计思维与实践 Week2 作业2道

这周作业主要是对广度优先搜索BFS的应用,包括求最短路径的迷宫问题及隐式图问题:倒水问题。

A - Maze

东东有一张地图,想通过地图找到妹纸。地图显示,0表示可以走,1表示不可以走,左上角是入口,右下角是妹纸,这两个位置保证为0。既然已经知道了地图,那么东东找到妹纸就不难了,请你编一个程序,写出东东找到妹纸的最短路线。

Input

输入是一个5 × 5的二维数组,仅由0、1两数字组成,表示法阵地图。

Output

输出若干行,表示从左上角到右下角的最短路径依次经过的坐标,格式如样例所示。数据保证有唯一解。

Sample Input

0 1 0 0 0
0 1 0 1 0
0 1 0 1 0
0 0 0 1 0
0 1 0 1 0

Sample Output

(0, 0)
(1, 0)
(2, 0)
(3, 0)
(3, 1)
(3, 2)
(2, 2)
(1, 2)
(0, 2)
(0, 3)
(0, 4)
(1, 4)
(2, 4)
(3, 4)
(4, 4)

Hint

坐标(x, y)表示第x行第y列,行、列的编号从0开始,且以左上角为原点。另外注意,输出中分隔坐标的逗号后面应当有一个空格。

之前数据结构课程学习到的迷宫老鼠问题可以用DFS,用先进后出的栈数据结构来求得一条路径,但不一定是最短路径。最短路径则要用到BFS,一般在程序中需要用到先进先出的队列数据结构。

可能距离上学期也过去了一段时间,对DFS和BFS不太熟悉了,在此来让我们先回顾下相关知识。

深度优先搜索

深度优先搜索(DFS,Depth-First Search)是搜索的手段之一。它从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态,继续转移到其他状态,如此不断重复,直至找到最终的解。例如求解数独。根据深度优先搜索的特点,采用递归函数实现比较简单。

宽度优先搜索

宽度优先搜索(BFS,Breadth-First Search)也是搜索的手段之一。它与深度优先搜索类似,从某个状态出发探索所有可以到达的状态。与深度优先搜索的不同之处在于搜索的顺序,宽度优先搜索总是先搜索距离初始状态近的状态。也就是说, 它是按照开始状态→只需1次转移就可以到达的所有状态→只需2次转移就可以到达的所有状态→……这样的顺序进行搜索。对于同一个状态,宽度优先搜索只经过一次,因此复杂度为O(状态数×转移的方式)。
宽度优先搜索按照距开始状态由近及远的顺序进行搜索, 因此可以很容易地用来求最短路径、 最少操作之类问题的答案。

宽度优先搜索时首先将初始状态添加到队列里, 此后从队列的最前端不断取出状态, 把从该状态可以转移到的状态中尚未访问过的部分加入队列,如此往复,直至队列被取空或找到了问题的解。通过观察这个队列,我们可以就知道所有的状态都是按照距初始状态由近及远的顺序被遍历的。

深度优先搜索与宽度优先搜索的比较

深度优先搜索(隐式地)利用了栈进行计算,而宽度优先搜索则利用了队列。

宽度优先搜索与深度优先搜索一样, 都会生成所有能够遍历到的状态, 因此需要对所有状态进行处理时使用宽度优先搜索也是可以的。 但是递归函数可以很简短地编写, 而且状态的管理也更简单,所以大多数情况下还是用深度优先搜索实现。反之,在求取最短路时深度优先搜索需要反复经过同样的状态,所以此时还是使用宽度优先搜索为好。
宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间。反之,深度优先搜索是与最大的递归深度成正比的。一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存

——摘自《挑战程序设计竞赛(第2版)》

其图示详细过程可参考:基本算法——深度优先搜索(DFS)和广度优先搜索(BFS)

分析

其中关键操作有:因为要向4个不同方向移动,用dx[4]和dy[4]两个数组来表示四个方向向量。这样通过一个循环就可以实现四方向移动的遍历。同理,八个方向也可用类似数组。

C++

#include <iostream>
#include<queue>
using namespace std;

class Position
{
public:
	int row;
	int col;
	Position(int x, int y) :row(x), col(y) {};
};
class Path :public Position
{
public:
	Path *parent;//存储上一个节点
	Path(int x, int y, Path* p) :Position(x, y), parent(p) {}
};
const int size_of = 7;
int maze[size_of][size_of];
queue<Path*> path;
Path *s = new Path(1, 1, NULL), *end_ = new Path(5, 5, NULL);
const int dx[] = { 0,0,1,-1 };//x的四个方向,八个方向同样可以类比
const int dy[] = { 1,-1,0,0 };//y的四个方向

void Bfs(int x,int y) {
	path.push(s);
	maze[x][y] = 1;
	while (!path.empty()) {
		Path* here = path.front();
		path.pop();
		int X = here->row, Y = here->col;
		if (X == end_->row && Y == end_->row)
		{
			end_ = here;//将终点之前的点的信息赋过去
			break;
		}
		for (int i = 0; i < 4; i++) {//四个方向进行判断
			if (X + dx[i] >= 0 && X + dx[i] < size_of && Y + dy[i] >= 0 && Y + dy[i] < size_of&&maze[X + dx[i]][Y + dy[i]] == 0) {//不超过边界且未被访问过时
				maze[X + dx[i]][Y + dy[i]] = 1;
				path.push(new Path(X + dx[i], Y + dy[i],here));//该点加入队列,其前一个层的点为here
			}
		}
	}
}

void print(Path *p)//递归顺序输出
{
	if (!p->parent)
	{
		cout << "(" << p->row-1 << ", " << p->col-1 << ")";//第一行前不换行
	}
	else
	{
		print(p->parent);
		cout << "\n(" << p->row-1 << ", " << p->col-1 << ")";
	}
}

int main()
{
	for (int i = 0; i <= 6; i++) {
		for (int j = 0; j <= 6; j++) {
			if (i == 0 || j == 0 || i == 6 || j == 6) { maze[i][j] = 1; }
			else
				cin >> maze[i][j];
		}
	}
	Bfs(1,1);
	print(end_);//从最后一个开始找前一个点直到起点
	system("pause");
	return 0;
}

B - Pour Water

倒水问题 “fill A” 表示倒满A杯,"empty A"表示倒空A杯,“pour A B” 表示把A的水倒到B杯并且把B杯倒满或A倒空。

Input

输入包含多组数据。每组数据输入 A, B, C 数据范围 0 < A <= B 、C <= B <=1000 、A和B互质。

Output

你的程序的输出将由一系列的指令组成。这些输出行将导致任何一个罐子正好包含C单位的水。每组数据的最后一行输出应该是“success”。输出行从第1列开始,不应该有空行或任何尾随空格。

Sample Input

2 7 5
2 7 4

Sample Output

fill B
pour B A
success
fill A
pour A B
fill A
pour A B
success

Notes

如果你的输出与Sample Output不同,那没关系。对于某个"A B C"本题的答案是多解的,不能通过标准的文本对比来判定你程序的正确与否。 所以本题由 SPJ(Special Judge)程序来判定你写的代码是否正确。

此题是一个隐式图问题,那么什么是隐式图呢?
隐式图是仅给出初始结点、目标结点以及生成子结点的约束条件(题意隐含给出),要求按扩展规则应用于扩展结点的过程,找出其他结点,使得隐式图的足够大的一部分编程显式,直到包含目标结点为止。

一种很典型的隐式图问题便是倒水问题。

  • 给你两个容器(容量分别为A, B)、一台饮水机 ,问是否能够经过有限的步骤倒水,得到容量为 C 的水

  • 对于每一种状态,都有以下几种转移方式:

    • A倒入B
    • B倒入A
    • A倒空/满
    • B倒空/满
  • 可以转移到不同的状态。

  • 对于每一种状态,我们需要记录是否已经被访问过了(BFS的过程)

  • 可以直接使用高维数组记录(本题),有时需要用 map/set 或哈希算
    法来记录状态

C++

#include <iostream>
#include<algorithm>
#include<map>
#include<queue>
using namespace std;

int A, B, C;
struct Status
{//进行结构体的封装
	int x, y, sta;
	//构造函数
	Status() {}
	Status(int x1, int y1, int sta1) {
		x = x1;
		y = y1;
		sta = sta1;//状态编号
	}

	bool operator<(const Status &b) const {//进行运算符的重载以比较两个结构体,一般只要重载<就行了
		return x == b.x ? y < b.y : x < b.x;//学习这种形式的使用
	}
	//不同种状态
	Status AToB() {
		Status c(max(x + y - B, 0), min(B, x + y), 1);
		return c;
	}

	Status BToA() {
		Status c(min(A, x + y), max(x + y - A, 0), 2);
		return c;
	}

	Status FillA() {
		return Status(A, y, 3);
	}

	Status FillB() {
		return Status(x, B, 4);
	}

	Status EmptyA() {
		return Status(0, y, 5);
	}

	Status EmptyB() {
		return Status(x, 0, 6);
	}
};

map<Status, bool> visited;
map<Status, Status> from;
queue<Status> q;

void check(Status x, Status y) {
	if (visited[x] == 0) {
		visited[x] = 1;//标记已经访问过
		from[x] = y;//记录来自于的状态,以实施BFS中最终通路的回溯
		q.push(x);
	}
}

void judge(int i) {
	switch (i)
	{
	case 1:
		cout << "pour A B" << endl;
		break;
	case 2:
		cout << "pour B A" << endl;
		break;
	case 3:
		cout << "fill A" << endl;
		break;
	case 4:
		cout << "fill B" << endl;
		break;
	case 5:
		cout << "empty A" << endl;
		break;
	case 6:
		cout << "empty B" << endl;
		break;
	default:
		break;
	}
}

void print(Status t) {
	if (from.find(t) != from.end()) {
		print(from[t]);//递归
		judge(t.sta);
	}
	else {
		judge(t.sta);
	}
}

void bfs(int a, int b) {
	Status t(a, b, 0);
	q.push(t);
	visited[t] = 1;
	while (!q.empty()) {
		t = q.front();
		q.pop();//队列中一个个进行BFS,直到满足条件终止
		if (t.x == C || t.y == C) {
			print(t);
			return;
		}
		check(t.AToB(), t);
		check(t.BToA(), t);
		check(t.FillA(), t);
		check(t.FillB(), t);
		check(t.EmptyA(), t);
		check(t.EmptyB(), t);
	}
}

int main() {
	while (cin >> A >> B >> C) {
		//每次输入一组数之前都清空存储体
		visited.clear();
		from.clear();
		while (!q.empty())q.pop();

		bfs(0, 0);
		cout << "success" << endl;
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值