1.問題描述:在炸彈人遊戲中,你有一顆炸彈,該炸彈的威力是十字形的,即只能爆出上下左右型的火焰,且火焰不能穿牆,問應該將炸彈放到哪裏炸彈才能殺死最多的小怪?(注:該位置必須是炸彈人能有初始位置走到的位置,畢竟炸彈人不能飛,牆或者小怪都能阻擋炸彈人的路徑),現在遊戲地圖如下圖所示,炸彈人的位置在(3,3)處。
2.解題思路:需要先將問題抽象出來,我們可以用一個 二維數組 來表示 遊戲地圖,用 *表示平地,炸彈人可以到,用#表示牆,用$表示小怪,炸彈人初始位置在(3,3),該問題的目標是搜索到地圖中的一個點,該點處滿足能炸死的小怪最多,所以我們首先需要知道炸彈放在某一位置 (x,y)處時,能炸死多少小怪;
所以,我們的首要目標就是抽象出地圖,並得知地圖上某點能炸死的小怪數量。
抽象出地圖 容易,上面提到的二維數組即可;統計小怪數量,因爲炸彈爆炸只能產生“十”字型火路,所以統計(x,y)放置炸彈威力只要統計 該點向上下左右移動(未碰到牆時)遇到的小怪數量即可
C++程序如下:
#include "iostream"
using namespace std;
#include "vector"
#include "string"
class game_boom//聲明該類
{
public:
void game_map();//構建地圖
int num_boom(int x, int y);//x,y爲炸彈放置的位置
public:
vector<string> map;//地圖屬性
};
//具象問題抽象化,抽象出來地圖與人物;用#代表牆,用*代表空地,用$來代表怪物
//地圖是一個平面,所以用一個二維數組來代替
void game_boom::game_map()
{
string s0 = "#############";
string s1 = "#$$*$$$#$$$*#";
string s2 = "###*#$#$#$#$#";
string s3 = "#*******#**$#";
string s4 = "#$#*###*#$#$#";
string s5 = "#$$*$$$*#*$$#";
string s6 = "#$#*#$#*#*#*#";
string s7 = "##$***$*****#";
string s8 = "#$#*#$###*#$#";
string s9 = "#***$#$$$*$$#";
string s10 = "#$#*#$#$#*#$#";
string s11 = "#$$*$$$#$*$$#";
string s12 = "#############";
//已知地圖是13*13的,所以針對該問題可以靜態創建地圖
map.push_back(s0);
map.push_back(s1);
map.push_back(s2);
map.push_back(s3);
map.push_back(s4);
map.push_back(s5);
map.push_back(s6);
map.push_back(s7);
map.push_back(s8);
map.push_back(s9);
map.push_back(s10);
map.push_back(s11);
map.push_back(s12);
}
int game_boom::num_boom(int i, int j)//統計炸彈的威力
{
int res = 0;
//統計向上的威力(炸死怪物的個數)
int x = i;//必須用臨時變量將 i,j接過來,因爲每一次統計的時候 都是從原位置出發,不能改變
int y = j;//防止初始位置發生變化,造成統計錯誤
while (map[x][y] != '#')//表明沒有撞到牆
{
if (map[x][y] == '$')
{
res++;
}
x--;//向上走
}
//統計向下的威力(炸死怪物的個數)
x = i;
y = j;//防止初始位置發生變化,造成統計錯誤
while (map[x][y] != '#')//表明沒有撞到牆
{
if (map[x][y] == '$')
{
res++;
}
x++;//向下走
}
//統計向左的威力(炸死怪物的個數)
x = i;
y = j;//防止初始位置發生變化,造成統計錯誤
while (map[x][y] != '#')//表明沒有撞到牆
{
if (map[x][y] == '$')
{
res++;
}
y--;//向左走
}
//統計向右的威力(炸死怪物的個數)
x = i;
y = j;//防止初始位置發生變化,造成統計錯誤
while (map[x][y] != '#')//表明沒有撞到牆
{
if (map[x][y] == '$')
{
res++;
}
y++;//向右走
}
return res;
}
有了地圖模型 也知道了每一點(x,y)出放置炸彈時能夠炸死的小怪數,那麼接下來就是搜索炸彈應該放置的位置了;
現在炸彈人在 (3,3)出,因爲其每次只能上下左右移動一步,且不能 過牆和小怪,所以每一步的搜索都是這樣的
所以,我們的搜索策略也是多種多樣的,我們可以一條路走到黑,一直往上走,直到遇到小怪或撞到牆(俗稱不撞南牆不回頭)
即深度優先搜索
走之前要判斷該點是否已經走過(不走重複路,因爲本問題的目標是要到一點使炸彈威力最大,已經判斷過的點不需要再次做判斷),並記錄每一點炸彈的威力,與當前最大威力相比,如果大於當前最大威力,則替換之;
在這裏面重要的需要表達的是 如何判斷是否重複,我們用一個二維數組,對應地圖,初始時,二維數組中每一個元素均爲0,表示地圖中的所有位置都還沒有走過,當走過某一點時,我們將其賦值爲1,表示該點已經走過
上下左右的移動 可以用一個數組表示
int next[4][2] = { {-1,0},{+1,0},{0,-1},{0,1} }; //小人可以擴展的方向,上下左右
之後,利用深度搜索DFS最重要的就是解決當前這一步做什麼,因爲下一步和這一步的操作是完全相同的
僞代碼:
首先,我們的初始點爲(3,3)我們需要將其標記置爲1,表示該點已經走過,判斷炸彈在該點的威力,然後賦值給最大當前威力,並保存該點,
開始循環向上下左右四個方向分別嘗試
{
判斷擴展的點是否越界,查出地圖,查出則進行下一個方向的嘗試
判斷該點是否已經走過並且判斷該點是否平地(只有平地才能放炸彈),如果該點沒有走過且可以放炸彈
則以此點爲初始點 進一步 按原策略搜索
}
搜索完畢後返回
完整程序如下:(需要將上面的地圖初始化程序保存爲game_map.h文件)
#include "iostream"
using namespace std;
#include "vector"
#include "game_map.h"
//深度優先搜索是通過遞歸來實現的
//我們需要保存的信息是遍歷過的以及該位置安裝炸彈時炸死的怪物個數,所以需要一個新的數據類型
class Position//保存每一個位置和該點炸彈的威力
{
public:
void set_x_y_nums(int tx, int ty, int nums)
{
this->x = tx;
this->y = ty;
this->nums = nums;
}
public:
int x;
int y;
int nums;//炸死小人的個數
};
void DFS(int x, int y,game_boom Game/*遊戲地圖和炸彈威力計算*/,vector<vector<int>> &book/*標記某一點是否走過*/,Position &max_pos/*炸彈最大威力位置*/)
{
book[x][y] = 1;
int sum = Game.num_boom(x, y);
if (sum > max_pos.nums)
{
max_pos.nums = sum;
max_pos.x = x;
max_pos.y = y;
}
int next[4][2] = { {-1,0},{+1,0},{0,-1},{0,1} }; //小人可以擴展的方向,上下左右
//沒有邊界,邊界即遍歷完所有點
//開始循環
for (int k = 0;k < 4;k++)
{
int tx = x + next[k][0];
int ty = y + next[k][1];
if (tx < 0 || tx > 12 || ty < 0 || ty >12)
{
continue;
}
if (Game.map[tx][ty] == '*' && book[tx][ty] == 0)
{
//如果下一個點可以擴展,而且沒有走過,則可以進一步擴展
DFS(tx,ty, Game, book, max_pos);
}
}
return;
}
int main()
{
game_boom Game;//開始遊戲
Game.game_map();//創建地圖
vector<int> v(13, 0);
vector<vector<int>> book(13, v);//book用來表示該位置是否走過
Position max_pos;
max_pos.set_x_y_nums(3,3,0);
DFS(3, 3, Game, book, max_pos);
printf("炸彈應該安放的位置是(%d,%d),它能夠炸死%d個小怪\n",max_pos.x,max_pos.y,max_pos.nums);
system("pause");
return 0;
}
當然,我們可以不那麼軸,不一條路走到黑,
即廣度優先搜索:
而是每條路我們都走走看,也就是說,我們由初始位置,先走上,然後再由初始位置走下,再由初始位置走左,再由初始位置走右,一輪擴展完畢,然後我們應該由 初始位置的上位置 從頭出發,擴展,初始位置上位置的上下左右位置,(這些位置需要保存下來,以便接下來擴展),然後由初始位置的下位置出發開始擴展,
發現沒有,這是一個隊列:有初始位置開始(入隊),將初始位置擴展的位置(上下左右)依次入隊,然後初始位置出隊,開始擴展初始位置的上位置(即現在的隊首元素),然後將其擴展的位置入隊,如此循環,直到 隊列爲空(無法進一步走下去)
僞算法:
初始位置入隊列,標記初始位置已經走過置爲1,判斷該位置的炸彈最大威力並記錄
開始上下左右循環,每一次都以隊列首元素爲初始出發點,擴展位置入隊列(擴展時也需要判斷是否越界,能否安裝炸彈以及是否已經走過)滿足條件則判斷該點的最大威力 與當前最大威力對比,大於則替換之
然後隊首元素出隊
如此往復循環
完整程序:(注:該程序也是在上面地圖初始化程序的基礎上實現的,也需要導入game_boom.h文件)
#include "iostream"
using namespace std;
#include "vector"
#include "queue"
#include "game_map.h"
//廣度優先搜索是藉助隊列來完成的
//我們需要保存的信息是遍歷過的以及該位置安裝炸彈時炸死的怪物個數,所以需要一個新的數據類型
class Position
{
public:
void set_x_y_nums(int tx, int ty, int nums)
{
this->x = tx;
this->y = ty;
this->nums = nums;
}
public:
int x;
int y;
int nums;//炸死小人的個數
};
int main()
{
game_boom Game;//開始遊戲
Game.game_map();//創建地圖
vector<int> v(13, 0);
vector<vector<int>> book(13,v);//book用來表示該位置是否走過
int next[4][2] = {{-1,0},{+1,0},{0,-1},{0,1}}; //小人可以擴展的方向,上下左右
queue<Position> Queue;//構建一個裝小人走到位置的隊列
//小人的初始位置 入隊列
int x = 3;
int y = 3;
//統計目前能炸死的怪物個數
int nums = Game.num_boom(x,y);
Position p;
p.set_x_y_nums(x,y,nums);//設置對象p的屬性
Queue.push(p);
book[3][3] = 1;//將(3,3)位置標記爲已經走過,之後不再走該位置
Position max_pos;
max_pos.set_x_y_nums(x,y,nums);//設計一個對象 保存 能炸死最多怪物的點
while (!Queue.empty())//開始擴展,找其可以一步走到的地方
{
for (int k = 0;k < 4;k++)//向上下左右四個方向擴展
{
int tx = Queue.front().x + next[k][0];
int ty = Queue.front().y + next[k][1];
//(tx,ty)表示新的位置,在新的位置入隊列之前我們需要判斷是否越界,該點點能否到達,或者是否已經到過
if(tx < 0 || tx > 12 || ty <0 || ty >12)
{
continue;
}
if (Game.map[tx][ty] == '*' && book[tx][ty] == 0)//可以入隊列等待下一次擴展
{
book[tx][ty] = 1;
int tnums = Game.num_boom(tx,ty);
Position tmp;
tmp.set_x_y_nums(tx, ty, tnums);
if (tnums > max_pos.nums)//如果該位置能炸死的怪物數比目前目前記錄的多,則替換max_pos
{
max_pos.set_x_y_nums(tx, ty, tnums);
}
Queue.push(tmp);
}
}
Queue.pop();//出隊,因爲該點已經擴展完畢,開始擴展下一個點
}
printf("炸彈應該安放的位置是(%d,%d),它能夠炸死%d個小怪\n", max_pos.x, max_pos.y, max_pos.nums);
system("pause");
return 0;
}
參考:《啊哈算法》