地下城夺宝游戏——杭电OJ 1044题解析

题目来源:杭电OJ-1044

题目大意:一个探险家身处一个危险的地下城,城中很危险,并且城中分散着若干个珠宝;现在地下城即将塌陷,冒险家需要在有限的时间内逃出去,但他希望在逃生的过程中获取一些珠宝并使价值最大化。题目要求用程序计算出最优方案所能获取的珠宝价值。

以前也遇到过相似的题目,但一直未真正搞明白,最近再次遇到,阅读了一些大牛的博客,结合自己的理解,将这个知识点记录一下。

这道题之所以不能简单的使用DFS或BFS算法解决,是因为探险家为了获取更多价值,可以走重复路径。最近学习并实现了这道题的两种解决思路,一种是BFS Plus,另一种是BFS+DFS。

一.BFS Plus方案

经典的BFS算法用一个二维数组保存地图的访问状况(可阅读我上一篇转载的文章,讲解的很深刻),而这一题的不同在于每次获取一个珠宝后,可以走重复路径,为了解决这个问题,我们可以在每次获取一个珠宝之后用一个新的二位数组保存后面遍历的访问情况。本方案使用二进制中的一位来标识某个珠宝是否获得,已获得取1,否则取0,这样就可以用一个值来表示珠宝的获取状态,比如说有四个珠宝A,B,C,D,那么我们可以用四位二进制数来表示获取状态,如果已经获取A和D,那么状态值就为9(二进制形式为1001)。本方案就是使用这个状态值来表示不同的二位访问状态数组,所以总体来说需要一个三维的数组来记录访问信息。而本题中最多有10个珠宝,所以需要十位二进制数来标识珠宝的访问状态,故状态数组的第一维为1024。

接下来给出该方案的源代码:

#include <iostream>
#include <queue>
#include <algorithm>
#include <string.h>
using namespace std;

int W, H, L, M;
char dun[55][55]; //保存地下城地图
int jew[10]; //保存每个珠宝的价值
bool isVisited[1024][55][55]; //记录BFS过程中某个位置是否经过
//结构体保存每到一个位置时的信息
struct State
{
	int J, x, y;
	int jews, time;
};
int dx[] = { -1, 0, 1, 0 };
int dy[] = { 0, 1, 0, -1 };

int CollectJewels()
{
	memset(isVisited, 0, sizeof(isVisited));
	State begin, pos, nextpos;
	for (int i = 0; i < H; i++)
	{
		int j;
		for (j = 0; j < W; j++)
		if (dun[i][j] == '@')
		{
			begin.x = i;
			begin.y = j;
			break;
		}
		if (j != W) break;
	}
	begin.J = begin.jews = begin.time = 0;
	isVisited[0][begin.x][begin.y] = true;
	queue<State> Q;
	Q.push(begin);
	int res = -1;
	while (!Q.empty())
	{
		pos = Q.front();
		Q.pop();
		if (pos.time == L) break; //优化操作,如果当前位置的时间已经达到
		//L,那么队列后面位置的时间一定>=L,可终止遍历
		int i;
		for (i = 0; i < 4; i++)
		{
			nextpos = pos;
			nextpos.x += dx[i];
			nextpos.y += dy[i];
			nextpos.time++;
			if(nextpos.x<0 || nextpos.x>=H ||
			nextpos.y<0 || nextpos.y>=W) continue;
			char ch = dun[nextpos.x][nextpos.y];
			if (ch == '*' || isVisited[nextpos.J][nextpos.x][nextpos.y])
				continue;
			isVisited[nextpos.J][nextpos.x][nextpos.y] = true;
			if (ch >= 'A' && ch <= 'J')
			{
				int t = ch - 'A';
				//如果当前位置的珠宝还在,取之
				if (!(nextpos.J & (1 << t)))
				{
					nextpos.jews += jew[t];
					nextpos.J |= (1 << t);
					isVisited[nextpos.J][nextpos.x][nextpos.y] = true;
				}
			}
			else if (ch == '<')
			{
				res = max(res, nextpos.jews);
				//优化操作,如果某种方案可以取得城内所有珠宝,则可以停止
				//尝试其他方案
				if (nextpos.J == ((1 << M) - 1)) break;
				else continue;
			}
			Q.push(nextpos);
		}
		if(i!=4) break;
	}
	return res;
}

int main() {
	int T;
	cin >> T;
	for (int i = 1; i <= T; i++)
	{
		cin >> W >> H >> L >> M;
		for (int j = 0; j < M; j++)
			cin >> jew[j];
		for (int j = 0; j < H; j++)
			cin >> dun[j];
		int res = CollectJewels();
		cout << "Case " << i << ":\n";
		if (res == -1) cout << "Impossible\n";
		else cout << "The best score is " << res << ".\n";
		if (i != T) cout << endl;
	}

	return 0;
}

二.BFS+DFS方案 本方案先进行BFS操作,计算出每个珠宝以及起点和出口两两之间的距离(也即时间),然后将这些位置作为结点,将距离作为边的权值,构造出一个虚拟图;接下来使用DFS算法计算出图中从起点到出口的最优路径。这种方案相对于上一方案来说更容易理解。 接下来给出该方案的源代码:

#include <iostream>
#include <queue>
#include <string.h>
using namespace std;

int W, H, L, M, ans, sum;
char dun[55][55]; //保存地下城地图信息
int jew[15]; //保存每个珠宝的价值
bool isVisited[55][55]; //记录BFS过程中地图的访问状态
int Graph[15][15]; //保存BFS过程中构造的虚拟图的信息
bool isUsed[15]; //记录DFS过程中图中节点的访问状态

struct State
{
	int x, y;
	int time;
	State(int a, int b, int c) :x(a), y(b), time(c){}
};
int dx[] = { -1, 0, 1, 0 };
int dy[] = { 0, 1, 0, -1 };

void BFS(int x, int y)
{
	memset(isVisited, 0, sizeof(isVisited));
	char ch = dun[x][y];
	int u;
	if(ch=='@') u=0;
	else if (ch >= 'A') u = ch - 'A' + 1;
	State pos(0, 0, 0), nextpos(0, 0, 0);
	queue<State> Q;
	isVisited[x][y] = true;
	Q.push(State(x, y, 0));
	while (!Q.empty())
	{
		pos = Q.front();
		Q.pop();
		if (pos.time == L) break; //优化操作
		for (int i = 0; i < 4; i++)
		{
			nextpos.x = pos.x + dx[i];
			nextpos.y = pos.y + dy[i];
			nextpos.time = pos.time + 1;
			if (nextpos.x < 0 || nextpos.x >= H ||
				nextpos.y < 0 || nextpos.y >= W) continue;
			if (dun[nextpos.x][nextpos.y] == '*' || isVisited[nextpos.x][nextpos.y])
				continue;
			isVisited[nextpos.x][nextpos.y] = true;
			char ch = dun[nextpos.x][nextpos.y];
			if (ch == '@')
			{
				Graph[u][0] = Graph[0][u] = nextpos.time;
			}
			else if (ch >= 'A' && ch <= 'J')
			{
				int v = ch - 'A' + 1;
				Graph[u][v] = Graph[v][u] = nextpos.time;
			}
			else if (ch == '<')
			{
				Graph[u][M + 1] = Graph[M + 1][u] = nextpos.time;
			}
			Q.push(nextpos);
		}
	}
}

void DFS(int pos, int Time, int je)
{
	if (Time > L) return;
	if(ans==sum) return; //剪枝操作
	if (pos == M + 1)
	{
		if (je > ans) ans = je;
		return;
	}
	
	for (int i = 1; i < M + 2; i++)
	{
		if (isUsed[i] || Graph[pos][i] == 0) continue;
		
		isUsed[i] = true;
		DFS(i, Time + Graph[pos][i], je + jew[i]);
		isUsed[i] = false;
	}
}
int main()
{
	int T;
	cin >> T;
	for (int i = 1; i <= T; i++)
	{
		cin >> W >> H >> L >> M;
		sum=0;
		for (int j = 1; j <= M; j++)
		{
			cin >> jew[j];
			sum+=jew[j];
		}
		//把起始位置和出口记作价值为0的位置,分别用0和M+1标识
		jew[0]=jew[M+1]=0;
		for (int i = 0; i < H; i++)
			cin >> dun[i];
		memset(Graph,0,sizeof(Graph));
		//BFS过程中构造虚图
		for (int i = 0; i < H;i++)
		for (int j = 0; j < W; j++)
		{
			if (dun[i][j] == '@' || (dun[i][j] >= 'A' && dun[i][j] <= 'J'))
				BFS(i, j);
		}
		memset(isUsed, 0, sizeof(isUsed));
		isUsed[0]=true;
		ans = -1;
		//DFS在途中寻找一条最优路径
		DFS(0, 0, 0);
		cout<<"Case "<<i<<":\n";
		if (ans == -1) cout << "Impossible\n";
		else cout << "The best score is " << ans << ".\n";
		if (i != T) cout << endl;
		
	}

	return 0;
}

两方案比较:可以很容易看出,方案一占用的空间较大,效率方面方案一在杭电OJ上运行,耗时390MS,占用空间5784K;而方案二耗时46MS,占用空间1624K。总的来说方案二更胜一筹。

点赞