問題描述
試題編號: | 201604-4 |
試題名稱: | 遊戲 |
時間限制: | 1.0s |
內存限制: | 256.0MB |
問題描述: |
問題描述 小明在玩一個電腦遊戲,遊戲在一個n×m的方格圖上進行,小明控制的角色開始的時候站在第一行第一列,目標是前往第n行第m列。 輸入格式 輸入的第一行包含三個整數n, m, t,用一個空格分隔,表示方格圖的行數n、列數m,以及方格圖中有危險的方格數量。 輸出格式 輸出一個整數,表示小明最快經過幾個時間單位可以過關。輸入數據保證小明一定可以過關。 樣例輸入 3 3 3 樣例輸出 6 樣例說明 第2行第1列時刻1是危險的,因此第一步必須走到第1行第2列。 評測用例規模與約定 前30%的評測用例滿足:0 < n, m ≤ 10,0 ≤ t < 99。 |
求解思路:
剛看到這道題的時候,心想:這不就是BFS麼,easy one.......,然後就直接用BFS做,馬上20分打臉。這道題與普通BFS的區別在於:普通BFS訪問過的節點是不會再去訪問的,而在本題中,由於處於危險中的節點並不是一直處於危險中,只是某一個時間段,因此就會出現這種情況:當小明走到某一點時,與該點相鄰的一個節點處於危險狀態,但僅僅有1個時間單位的危險時間,並且這個相鄰點的下一個相鄰點就是終點(只是舉了一種比較極端的情況),而其餘節點均不在危險狀態,但是這些節點的相鄰節點均不是終點,並且這些節點會把小明引到遠離終點的方向。如下圖:
如果用原版BFS,那麼小明知道3號節點當前危險,因此將3號標記爲已訪問,之後就不會再去訪問,故,小明會選擇2號或是4號節點(選哪個取決於轉移數組的順序),假如小明選擇了1號節點,該節點當前安全,故小明站到了1號節點上,時間來到了1s,然後與1號節點相鄰的4號節點在下一秒(2s)是危險的,故小明不會選擇4號,而是選擇另外兩個相鄰節點(圖中沒有給出),不論小明選擇了哪個節點,他最終到達終點的時間肯定超過了3s。回過頭來想,如果小明在最初的位置逗留1s,那麼這時3號節點就安全了,小明可以站在3號節點上,然後再經過1s小明就到達終點了,用時爲3s,顯然更短。可見,原版的BFS失效了,這是因爲無法選擇曾經危險的節點。
因此需要對原版BFS進行修改,需要增加原地踱步處理,由於當前節點+1s然後進行判斷,這是在看能不能直接走,所以可以讓當前節點+2s後直接進入隊列,而不進行判斷,這就是踱步,有人可能會問:你這不是纔等待1s麼,如果節點的危險時間不止1s咋整?嗯,注意,我們+2s後直接進入隊列,也就是說,這種情況還可以再次被取出來,然後在此基礎上+2s進隊列,這就已經將需要繼續等待的情況放入隊列中了。但是我們對原版BFS的改造還沒結束,原版BFS用到的隊列是普通的隊列,先進先出,那麼就有可能處於隊列中的之前處在等待狀態的情況更早的到達終點,而隊頭情況尚未到達,因此需要選用優先隊列,將當前時間最小的情況放在隊列的隊頭,這樣就能保證每次取出的情況都是當前最早的。那麼我們肯定會疑惑,原版的BFS怎麼不用考慮這個問題?其實,區別就在於需不需要踱步,原版情況是:這個節點不能訪問就一定不會再去訪問,沒有回溯,相等於圖的層序遍歷,而時間t隨着層數增加而增加,因此是遞增的。而當前這種情況存在回溯,那麼就有可能在回溯的過程中,出現時間更小的分支。
綜上所述:原版的BFS被修改爲:+2s逗留處理以及用優先隊列選擇當前最小t的情況
代碼(BFS+優先隊列):
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
struct Node {
long long left, right;
Node () { left = right = 0x3f3f3f3f; }
};
struct Time {
long long x, y, time;
bool operator < (const Time &A)const{//重載小於號,爲了構造小根堆
return time>A.time;
}
};
long long n, m, t, vis[105][105] = {};
long long move[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
Node map[105][105];
long long BFS()
{
priority_queue<Time> Q;//優先隊列
Time tmp;
tmp.x=1;
tmp.y=1;
tmp.time=0;
Q.push(tmp);
vis[1][1] = 1;
while (1) {
long long x = Q.top().x, y = Q.top().y, time = Q.top().time;
if (x == n && y == m) return time;
Q.pop();
Time ttmp;
ttmp.x=x;
ttmp.y=y;
ttmp.time=time+2;//+2s踱步
Q.push(ttmp);//將踱步的情況放入隊列中
if (time >= map[x][y].left && time <= map[x][y].right) continue;//處於危險狀態
for (long long i = 0; i < 4; ++i) {
long long xx = x + move[i][0], yy = y + move[i][1];
if (xx < 1 || xx > n || yy < 1 || yy > m) continue;
if (time + 1 >= map[xx][yy].left && time + 1 <= map[xx][yy].right) continue;
if (vis[xx][yy]) continue;
vis[xx][yy] = true;
Time ttmp1;
ttmp1.x=xx;
ttmp1.y=yy;
ttmp1.time=time+1;
Q.push(ttmp1);
}
}
}
int main()
{
cin >> n >> m >> t;
for (long long i = 0, u, v, l, r; i < t; ++i) {
cin >> u >> v >> l >> r;
map[u][v].left = l;
map[u][v].right = r;
}
long long ans = BFS();
cout << ans;
}
注:此代碼選自CSDN博主:姬小野
網上大佬普遍給出的是另一種方法:三維BFS
所謂三維,就是在橫縱座標的基礎上又增加了時間這一維度,上面我們討論過,這道題的關鍵就是某段時間不安全的節點在之後有可能會被回溯到。那麼我們爲何非要把不同時間的節點看做是同一個節點呢?我們可以把不同時間的節點看做是不同的節點,這樣所有的情況就都有被遍歷的能力,這樣就成功的將這道題轉化爲普通的BFS了。
代碼(三維BFS):
#include<iostream>
#include<queue>
using namespace std;
typedef struct E{
int x,y;
int t;
bool operator <(const E &A)const{
return t>A.t;
}
}E;
bool mark[101][101][500]={};
int go[][2]={
1,0,
-1,0,
0,1,
0,-1,
};
priority_queue<E> Q;
long long BFS(int n,int m)
{
while(!Q.empty())
{
E now=Q.top();
Q.pop();
for(int i=0;i<4;i++)
{
int nx=now.x+go[i][0];
int ny=now.y+go[i][1];
int t=now.t+1;
if(nx>n||ny>m||nx<1||ny<1) continue;
if(mark[nx][ny][t]) continue;
E tmp;
tmp.x=nx;
tmp.y=ny;
tmp.t=t;
Q.push(tmp);
mark[nx][ny][t]=true;
if(nx==n&&ny==m) return t;
}
}
}
int main()
{
int n,m,t,r,c,a,b;
scanf("%d%d%d",&n,&m,&t);
while(t--)
{
scanf("%d%d%d%d",&r,&c,&a,&b);
for(a;a<=b;a++)
{
mark[r][c][a]=true;//不安全時刻的節點直接設置爲已訪問,因爲即使遍歷到這些節點,也不會將這些節點選中,索性就不去遍歷它們
}
}
E first;
first.x=1;
first.y=1;
first.t=0;
Q.push(first);
long long res=BFS(n,m);
printf("%lld\n",res);
return 0;
}
注意:在讀入節點數據時,我們將不安全時刻的節點直接設置爲已遍歷,這是因爲即使遍歷到這些節點,也不會將這些節點選中,索性就不去遍歷它們 ,這是BFS的一種簡化方式。
運行結果: