两道隐式图遍历的题目
如果不了解隐式图遍历,请看:八数码问题——隐式图遍历
隐式图遍历是非常暴力的操作,状态多,耗费内存严重,而且时间复杂度也不低。所以在编码时要考虑很多细节。
倒水问题(Fill, UVa 10603)
题目大意
有三个容积为a,b和c毫升的杯子(abc为整数并且不会大于200),第一和第二个杯子初始是空的,第三个杯子是盛满水的。你可以把水从一个杯子倒入另一个杯子直到目标的杯子满了或者手里的杯子空了,你可以做0,1或更多次这个操作。
你需要写一个程序去计算最少需要倒多少升水才能让至少有一个杯子中的水量为d升(d是整数且不超过200)。如果不可能量出d升,请找到一个能折腾出的最接近d的d',\(d'<d\)。
输入
第一行是有多少组输入数据T,以后的T行每组是一个输入,包含四个数,用空格分开,分别是a,b,c和d。
输出
对于每组输入,输出包含一行,两个数,分别是最少的倒水升数和找到的d'(或者是d)
测试用例
Sample Input
2
2 3 4 2
96 97 199 62
Sample Output
2 2
9859 62
思路
把\(a,b,c\)中的水量看作一个状态,初始时当前状态是{0,0,c}
(因为初始时前两个杯子没水,后一个有c升。对于每个状态,计算其所有可能的倒水方式,并生成新的状态,直到状态等于d。这就变成了隐式图遍历问题。
复杂的地方在于d不一定能找到,如果找不到得找到一个最接近并且小于d的d'。而且还要保证折腾的水的数量最少。
关于找d'这个我们可以很容易的在代码中去控制,水量最少这个是看刘汝佳的代码想到的,把bfs的队列换成优先级队列,并且把当前的倒水量作为key。每次弹出一个最小的,而不是按入队顺序弹出。
如何去重?这是所有图问题都应该考虑的问题。尤其是隐式图这种状态巨多且恶心的题目。
如果用vis数组来搞,这里就需要一个\(200\times 200\times 200 = 8000000\)的三维数组。这肯定不快,空间也不低。
但这里我们考虑一个事实——三个杯子里的水是一样多的。因为总水量不会变,所以如果杯子a确定了,杯子b确定了,那么c就确定了。所以只需要\(200\times 200\)也就大概是四万种状态,这是能承受并且不算太大的。
代码
#include "iostream"
#include "cstdio"
#include "cstring"
#include "queue"
#define MAX 3
#define MAX_N 205
using namespace std;
int C[MAX]; // 杯子的容积
typedef int State[MAX];
int vis[MAX_N][MAX_N];
int goal;
struct StateNode {
int amount;
State state;
StateNode(){
amount = 0;
memset(state, 0, sizeof(state));
}
void init(int a,const State& s) {
this->amount = a;
memcpy(this->state, s, sizeof(s));
}
bool operator < (const StateNode &a) const{
return amount > a.amount;
}
StateNode& operator=(const StateNode& a) {
init(a.amount, a.state);
return *this;
}
};
int d2, min_amount,min_diff;
void bfs() {
priority_queue<StateNode> q;
State initialState = { 0,0,C[2] };
StateNode initialNode;
initialNode.init(0, initialState);
q.push(initialNode);
while (!q.empty()) {
StateNode node = q.top(); q.pop();
State& s = node.state;
min_diff = abs(goal - d2);
for (int i = 0; i < MAX; i++) {
int diff = abs(s[i] - goal);
if (s[i] <= goal) { // 因为只找小于等于d的d',所以对于大于的,就算它倒水再少,也一律扔掉
if (diff < min_diff) { // 如果小于就更新,如果不小于就不用更新了,先到达的肯定比后到达的倒水少
d2 = s[i];
min_diff = diff;
min_amount = node.amount;
}
if (d2 == goal) {
return; // d2==d,直接结束
}
}
}
for (int j = 0; j < MAX; j++)
for (int i = 0; s[j] > 0 && i < MAX; i++) {
if (j != i) {
if (s[i] == C[i]) continue;//满了
StateNode new_node;
new_node.init(node.amount, s);
int out_amount = min(s[j], C[i] - s[i]);
new_node.state[i] += out_amount;
new_node.state[j] -= out_amount;
new_node.amount += out_amount;
if (!vis[new_node.state[0]][new_node.state[1]]) {
vis[new_node.state[0]][new_node.state[1]] = 1;
q.push(new_node);
}
}
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d %d %d %d", &C[0], &C[1], &C[2], &goal);
memset(vis, 0, sizeof(vis));
vis[0][0] = 1;
// 初始时认为离d最近的就是0,同时最小差值就是目标值,最小倒水数量就是没倒水
d2 = 0; min_diff = goal; min_amount = 0;
bfs();
printf("%d %d\n", min_amount, d2);
}
return 0;
}
万圣节后的早晨(The Morning after Halloween, Japan 2007, UVa1601)
此题在UVa上拿了个WA,也没心情再搞了,去Aizu上同样的题目看了看,AC了。
题目大意
你在一个游乐场的鬼屋做操作员。房子中由一些幽灵,是由你远程控制的机器人,它们藏在走廊中。一个早上,你发现幽灵们不在它应该在的位置上。哦吼~昨天是万圣节,不管你信不信,有一些无法描述的东西在夜里移动了它们。你需要在客人来之前移动它们到正确的位置。你的经理想知道需要多长时间才能恢复幽灵的位置。
你需要写一个程序,给你房子的平面图,找到移动幽灵到原本位置的最小步数。
图由一个矩阵组成,每个单元格是墙或者空地。
在每一步,你可以移动任意数量的幽灵。每个幽灵可以呆在原来的位置或者向上下左右移动,当然不能穿墙,还要满足以下条件:
- 在一个格子里不能同时有两个幽灵
- 两个幽灵不能在一步之内交换位置
输入
输入包含最多十个数据集,每个代表一个平面图,有如下格式:
w h n
c11 c12 ... c1w
c21 c22 ... c2w
... ... ... ...
ch1 ch2 ... chw
w,h和n是整数,以空格分割,w和h是房间的宽高,n是幽灵的数量。满足如下关系
下面的h行w列个字符是平面图,每个\(c_{ij}\)是:
- '#' 代表墙
- 一个小写字母,代表一个幽灵初始的位置
- 一个大写字母,代表对应的幽灵应该摆放的位置
- 一个空格代表空地
输入保证这些小写字母从a开始并且最大是c。
输入保证每\(2\times 2\)个区域中有至少一个#
。
最后一个数据集以三个零结束。
输出
对于每组数据集,输出恢复到原来状态的最小步数。
Sample Input
5 5 2
#####
#A#B#
# #
#b#a#
#####
16 4 3
################
## ########## ##
# ABCcba #
################
16 16 3
################
### ## # ##
## # ## # c#
# ## ########b#
# ## # # # #
# # ## # # ##
## a# # # # #
### ## #### ## #
## # # # #
# ##### # ## ##
#### #B# # #
## C# # ###
# # # ####### #
# ###### A## #
# # ##
################
0 0 0
Output for the Sample Input
7
36
77
思路
刚拿到这题觉得挺简单。当我运行第三个数据集花了1分45秒的时候我傻了。。。
首先还是先想状态。最多有三个幽灵,可以用一个三维数组存储幽灵的所在位置来代表状态,而不是存储整张图。
对于二维的位置,不好存储,写代码也啰嗦,难看。不如把它们编号成一维的,提供一个f(x,y)=id
的映射,\(id=x\times w + y\)。x,y最大是16,这个id最大是\(16^2=256\)。
然后只需要从初始状态开始,每一次把所有能走的状态放到队列中(注意,这里可能有\(5^3=125\)中可能,就是每步可能生成125个新图,肥肠大),循环这个过程直到队列为空。
这里可以看到整张图的大小和需要的时间已经非常多了,但是题目中有个有意思的限制:\(2\times 2\)个区域中最少有一个'#'。
也就是说图中不能走的地方很多,特别多。所以我们每步生成的新状态也没有那么多。
这里考虑把图中的空白部分,也就是能走的部分提取出来,做一个邻接表,这样能省很多时间在遍历上,也能节省空间。
最后就是去重,我之前选用的是STL中的unsorted_map
,就是哈希表,一分四十五秒的惨剧就是它造成的。可能是状态太多了,有一百万个,然后哈希表的容量小,导致的冲突增多,查找变慢。可以选择自己写哈希表也可以选择使用vis数组。这里使用vis数组,需要占用一百万多个空间。替换掉哈希表后最后一个数据集只用了5秒大概。
代码
考虑下,最多有三个幽灵,每个幽灵同时选5个行走方式,我们每次生成新状态都要把所有的这些状态生成出来。这里的代码怎么写?我只想到三重循环。。。但是太丑了!!!恶臭!!!
借鉴了网上代码的状态生成部分并且按照他的思路修改了一下,使用的递归思想,很牛逼。
#include "iostream"
#include "cstdio"
#include "cstring"
#include "string"
#include "sstream"
#include "queue"
#define MAX 3
#define MAX_WH 16
using namespace std;
struct V {
int id;
V *next;
V():id(0),next(NULL) {}
}G[MAX_WH*MAX_WH];
int w, h, n;
int dx[4] = { 1,-1,0,0 }, dy[4] = { 0,0,1,-1 };
int vis[MAX_WH * MAX_WH][MAX_WH * MAX_WH][MAX_WH * MAX_WH];
typedef int State[MAX];
struct StateNode {
int step;
State state;
StateNode(){
step = 0;
memset(state, 0, sizeof(state));
}
void init(int step,const State& s) {
this->step = step;
memcpy(this->state, s, sizeof(s));
}
StateNode& operator=(const StateNode& a){
init(a.step, a.state);
return *this;
}
};
State start,target;
// 图的字符串形式
string g_str;
int id(int x, int y) {
return x * w + y;
}
void create_conn(int sid, int tid) {
V *t = new V();
t->id = tid;
t->next = G[sid].next;
G[sid].next = t;
}
void explore_adj(int sid,int x,int y) {
for (int k = 0; k < 4; k++) {
int nx = dx[k] + x, ny = dy[k] + y;
if (nx >= 0 && nx < MAX_WH && ny >= 0 && ny < MAX_WH) {
int tid = id(nx, ny);
if (g_str.at(tid) != '#')
create_conn(sid, tid);
}
}
create_conn(sid, sid);
}
void build() {
stringstream ss("");
for (int i = 0; i < h; i++) {
getline(cin, g_str);
ss << g_str;
}
g_str = ss.str();
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
int cid = id(i, j);
char sig = g_str.at(cid);
if (sig == '#') continue;
if (sig >= 'A' && sig <= 'C') {target[sig - 'A'] = cid;}
else if (sig >= 'a' && sig <= 'c') start[sig - 'a'] = cid;
explore_adj(cid, i, j);
}
}
}
bool is_invalid(StateNode &s,StateNode &former) {
State &s1 = s.state,&s2 = former.state;
for (int i = 0; i < n; i++) { // 两两检测
for (int j = 0; j < n; j++) {
if (i != j && s1[i] == s1[j])return true; // 检测有没有重叠
if (i != j && s1[i] == s2[j] && s1[j] == s2[i]) return true; // 检测有没有直接交换
}
}
return false;
}
bool is_vis(State& s) {
return vis[s[0]][s[1]][s[2]];
}
void next_state(StateNode& s,int cur, queue<StateNode>& q, StateNode& former) {
int id = former.state[cur];
V* v = G[id].next;
while (v) {
s.state[cur] = v->id;
if (cur == n - 1 && !is_vis(s.state) && !is_invalid(s, former)) {
s.step = former.step + 1;
vis[s.state[0]][s.state[1]][s.state[2]] = 1;
q.push(s);
}
else if (cur < n - 1) {
next_state(s, cur + 1, q, former);
}
v = v->next;
}
}
int bfs() {
queue<StateNode> q;
StateNode _s;
_s.init(0, start);
q.push(_s);
vis[start[0]][start[1]][start[2]] = 1;
while (!q.empty()) {
StateNode cur_node = q.front(); q.pop();
State &cur_state = cur_node.state;
//printf("%d %d %d %d\n", cur_state[0], cur_state[1], cur_state[2],cur_node.step);
if (memcmp(cur_state,target,sizeof(target))==0)
return cur_node.step;
StateNode new_node;
next_state(new_node,0,q,cur_node);
}
return -1;
}
int main() {
while (scanf("%d %d %d", &w,&h,&n) != EOF) {
if (w == 0 && h == 0 && n == 0)break;
getchar();
fill(G, G + MAX_WH * MAX_WH, V());
memset(vis, 0, sizeof(vis));
build();
printf("%d\n", bfs());
}
return 0;
}