兩道隱式圖遍歷的題目
如果不瞭解隱式圖遍歷,請看:八數碼問題——隱式圖遍歷
隱式圖遍歷是非常暴力的操作,狀態多,耗費記憶體嚴重,而且時間複雜度也不低。所以在編碼時要考慮很多細節。
倒水問題(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是幽靈的數量。滿足如下關係
\[4 \le w \le 16\\ 4 \le h \le 16\\ 1 \le n \le 3 \]下面的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;
}