無聊的時候用java實現的推箱子演算法~
相信對於推箱子這種東西大家都一定很熟悉了吧,本人身為一個宅男最近迷上了一款叫做遊戲俱樂部的game,聽上去是不是很高大上的樣子。。。結果----這TM不就是個推箱子麼!!!
當時被這個關卡卡的欲仙欲死。。。所以果斷拿出了自己大學的存貨,自動推箱子搞起!
-------------------------------------------------------------------無情的分割線-------------------------------------------------------------------
廢話說了這麼多不會被打死吧。。。
正題開始,首先是思路:
first:做過ACM的一看就知道這TM不就是個圖論的題目麼,所以一開始自己想到的就是進行DFS(深度優先搜尋),儲存每次走過的地圖肯定是必要的,用於防止進行重複的搜尋。然後。。。然後就是讓他自己搜吧。。。雖然覺得很不靠譜,走一步算一步是不。
關於地圖的儲存,因為有一部分元素是可以重疊放置的,所以用了一個類似二進位制的儲存方式,就是4種物件分別有是否存在狀態,使得用一個數字可以表示多個物件。
1:是否存在目的地
2:是否存在箱子
4:是否存在人
8:是否存在牆壁
這樣就解決了地圖儲存問題。使用short[][]就存下了(這裡沒有做更小的儲存了,本來用java不用c++就是怕麻煩不是。。。)
之後用這個思路進行了一系列思考,發現數據量還是太大了,一步一步慢慢走簡直不能忍,隨隨便便就爆表了,所以漫漫優化之路開始了。
1.在不移動箱子的情況下其實無論人在哪裡對於map來說是沒有影響的,所以填充可移動區域可以讓需要儲存地圖的數量有一個大的下降。例如之前那副地圖:
8888888
8103018
8002008
8320238
8012108
8403008
8888888
經過變換之後就成了:
8888888
8103018
8002008
8320238
8452108
8443008
8888888
這樣就把儲存量縮小了四分之三。至於怎樣填充,相信對圖論有一點了解的都可以隨便想出方案,我這裡用的是BFS:
2.關於箱子的移動方式,alai04的博文給了我啟示,類似於八皇后問題,直接用整幅地圖的BFS搜尋會比較靠譜。因為可以確定箱子的位置和在不移動箱子情況下人能到的位置,所以箱子可移動的位置也就能確定了,再加上之前儲存的所有箱子的位置,這樣就能計算出箱子每動作一次地圖能更新的情況。/** * 填充可移動位置 */ public void findStart() { //位置佇列 Queue<Part> manList = new LinkedList<Part>(); for (short i = 0; i < H; i++) { for (short j = 0; j < W; j++) { //能站人的地方都加入佇列 if (map[i][j] == 4 || map[i][j] == 5) { manList.offer(new Part(i, j)); } //這裡用於同時記錄箱子的位置,以後會用到,用於減少遍歷次數 if (map[i][j] == 2 || map[i][j] == 3) { boxList.offer(new Part(i, j)); } } } //提出沒一個站立位置並將其周圍填充之後加入佇列 while (!manList.isEmpty()) { Part temp = manList.poll(); fillMovable(manList, (short) (temp.x - 1), temp.y); fillMovable(manList, (short) (temp.x + 1), temp.y); fillMovable(manList, temp.x, (short) (temp.y - 1)); fillMovable(manList, temp.x, (short) (temp.y + 1)); } }
一次BFS就是每個箱子往不同可移動位置進行一次移動。
/**
* 獲取子節點地圖(進行一次BFS搜尋)
*
* @return
*/
public LinkedList<Map> getChildMap() {
LinkedList<Map> list = new LinkedList<Map>();
for (Part part : boxList) {
Map temp;
temp = move(part, -1, 0);
if (null != temp)
list.offer(temp);
temp = move(part, 1, 0);
if (null != temp)
list.offer(temp);
temp = move(part, 0, -1);
if (null != temp)
list.offer(temp);
temp = move(part, 0, 1);
if (null != temp)
list.offer(temp);
}
return list;
}
/**
* 移動箱子
*
* @param box
* @param x
* @param y
* @return
*/
private Map move(Part box, int x, int y) {
Map temp = null;
//如果箱子可以向當前方向移動
if ((map[box.x + x][box.y + y] == 0 || map[box.x + x][box.y + y] == 1
|| map[box.x + x][box.y + y] == 4 || map[box.x + x][box.y + y] == 5)
&& (map[box.x - x][box.y - y] == 4 || map[box.x - x][box.y - y] == 5)) {
temp = this.clone();
// TODO待優化
for (int i = 0; i < H; i++) {
for (int j = 0; j < W; j++) {
if (temp.map[i][j] == 4 || temp.map[i][j] == 5) {
temp.map[i][j] -= 4;
}
}
}
temp.map[box.x][box.y] += 4;
temp.map[box.x][box.y] -= 2;
temp.map[box.x + x][box.y + y] += 2;
//因為箱子已經移動,所以需要重新計算可以站立的位置
temp.fatherMap = this;
temp.findStart();
temp.getCode();
}
return temp;
}
3.至於什麼時候結束搜尋,基本上就只有三種情況:
所有目的地被填充完畢-------計算完成退出程式。
有箱子被推到角落並且不是在目的地--------說明不是正確的路線,搜尋不再往下走。
當前地圖在以前已經被達成過--------說明是重複路線,搜尋不再往下走。
4.關於地圖的儲存,因為要保證不能重複,我是用的是hashSet,並重寫了equals和hashCode的實現,用來自動判斷地圖是否重複。
<span style="white-space:pre"> </span>public void getCode() {
hashCode = 0;
int i = 0;
for (short[] temp : map) {
for (short temp2 : temp) {
hashCode += ((int) Math.pow(7, NUM - i)) * temp2;
++i;
}
}
}
<span style="white-space:pre"> </span>@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
Map o = (Map) obj;
if (this.hashCode != o.hashCode)
return false;
boolean flag = true;
for (int i = 0; i < H; i++) {
for (int j = 0; j < W; j++) {
if (o.map[i][j] != this.map[i][j]) {
flag = false;
break;
}
}
if (!flag)
break;
}
return flag;
}
最後就是完成之後的地圖顯示問題,我的解決方案就是每個節點儲存自己父親節點的地址,當節點發現自己已經完成之後根據地址向上查詢直到樹頂。
好了,java版的推箱子就這麼完成了~~試了一下效率還不錯~基本上java解不出來的c++估計也懸,時間複雜度主要取決於箱子的數量與空地的數量。話說做完這麼一個東西還是蠻有成就感的說。
本人第一次寫這種博文,有什麼意見或者想法歡迎來交流~~
bye~bye~~
請原諒我放蕩不羈的要了1分。。。