AcWing 1131 拯救大兵瑞恩

題目描述:

1944 年,特種兵麥克接到國防部的命令,要求立即趕赴太平洋上的一個孤島,營救被敵軍俘虜的大兵瑞恩。

瑞恩被關押在一個迷宮裏,迷宮地形複雜,但幸好麥克得到了迷宮的地形圖。

迷宮的外形是一個長方形,其南北方向被劃分爲 N 行,東西方向被劃分爲 M 列, 於是整個迷宮被劃分爲 N×M 個單元。

每一個單元的位置可用一個有序數對 (單元的行號, 單元的列號) 來表示。

南北或東西方向相鄰的 2 個單元之間可能互通,也可能有一扇鎖着的門,或者是一堵不可逾越的牆。

注意: 門可以從兩個方向穿過,即可以看成一條無向邊。

迷宮中有一些單元存放着鑰匙,同一個單元可能存放 多把鑰匙,並且所有的門被分成 P 類,打開同一類的門的鑰匙相同,不同類門的鑰匙不同。

大兵瑞恩被關押在迷宮的東南角,即 (N,M) 單元裏,並已經昏迷。

迷宮只有一個入口,在西北角。

也就是說,麥克可以直接進入 (1,1) 單元。

另外,麥克從一個單元移動到另一個相鄰單元的時間爲 1,拿取所在單元的鑰匙的時間以及用鑰匙開門的時間可忽略不計。

試設計一個算法,幫助麥克以最快的方式到達瑞恩所在單元,營救大兵瑞恩。

輸入格式

第一行有三個整數,分別表示 N,M,P 的值。

第二行是一個整數 k,表示迷宮中門和牆的總數。

接下來 k 行,每行包含五個整數,Xi1,Yi1,Xi2,Yi2,Gi:當 Gi≥1 時,表示 (Xi1,Yi1) 單元與 (Xi2,Yi2) 單元之間有一扇第 Gi 類的門,當 Gi=0 時,表示 (Xi1,Yi1) 單元與 (Xi2,Yi2) 單元之間有一面不可逾越的牆。

接下來一行,包含一個整數 S,表示迷宮中存放的鑰匙的總數。

接下來 S 行,每行包含三個整數 Xi1,Yi1,Qi,表示 (Xi1,Yi1) 單元裏存在一個能開啓第 Qi 類門的鑰匙。

輸出格式

輸出麥克營救到大兵瑞恩的最短時間。

如果問題無解,則輸出 -1。

數據範圍

|Xi1−Xi2|+|Yi1−Yi2|=1,
0≤Gi≤P,
1≤Qi≤P
1≤N,M,P≤10,
1≤k≤150

輸入樣例:

4 4 9
9
1 2 1 3 2
1 2 2 2 0
2 1 2 2 0
2 1 3 1 0 
2 3 3 3 0
2 4 3 4 1
3 2 3 3 0
3 3 4 3 0
4 3 4 4 0
2
2 1 2 
4 2 1

輸出樣例:

14

樣例解釋:

迷宮如下所示:

1131.png

分析:

本題y總的思路看到一半,發現與自己的想法有所出入,再看下y總的代碼,大致理解了其思路,又覺得本題完全可以不用雙端隊列BFS求解,於是用普通的BFS求解,果然AC了,而且效率也不低。

試想下如果本題沒有鑰匙和門的條件,只要求從左上角走到右下角的最小步數,就是簡單的迷宮問題了,可以使用BFS解決。加上鑰匙和門的的條件,便是類似於八數碼問題了。實際上BFS解決的最短路問題都可以看作求從初始狀態到結束狀態需要的最小轉移次數,普通迷宮問題的狀態就是當前所在的座標,八數碼問題的狀態就是當前棋盤的局面。本題在迷宮問題上加上了鑰匙和門的條件,顯然,處在同一個座標下,持有鑰匙和不持有鑰匙就不是同一個狀態了,爲了能夠清楚的表示每個狀態,除了當前座標外還需要加上當前獲得的鑰匙信息,即f[x][y][st]表示當前處在(x,y)位置下持有鑰匙狀態爲st,將二維座標壓縮成一維就得到f[z][st]這樣的狀態表示了,或者說,z是格子的編號,從上到下,從左而右的編號依次爲1到n*m,st爲0110時,表示持有第1,2類鑰匙,這裏注意我在表示狀態時拋棄了最右邊的一位,因爲鑰匙編號從1開始,我想確定是否持有第i類鑰匙時,只需要判斷st >> i & 1是不是等於1即可。

知道了狀態表示,現在題目就轉化爲了從狀態f[1][0]轉化爲f[n*m][...]狀態的最小步數了,我們不關心到達終點是什麼狀態,只要到達了終點就成功了。現在進行第二步狀態轉移,兩個相鄰格子間有牆,就不能轉移;有門,持有該類門鑰匙就能轉移,沒有鑰匙就不能轉移;沒有障礙,正常轉移。下面討論轉移到有鑰匙的格子的情況,這點我與y總處理方式的不同決定了最終解法的不同,y總是先不管有沒有鑰匙,先轉移到這個格子再說,轉移到有鑰匙的格子的時候步數加一,然後拿起鑰匙不移動位置進入另一種狀態步數不變。我最初的想法就是,爲什麼要把這兩個過程分開,我們走到有鑰匙的格子上,並不用考慮要不要拿鑰匙,拿鑰匙又不會增加成本,只管拿就行。因此,轉移到某個格子時,直接計算下這個格子的狀態,格子上有鑰匙就在之前狀態基礎上加上這個鑰匙,沒有鑰匙就繼承之前的鑰匙狀態,這樣一來,問題中就不存在邊權爲0的邊了,只要狀態轉移了,步長都是加一,普通的BFS就可以解決了。

按照DP的一般流程,狀態表示和狀態轉移分析完就可以解決問題了。回想下最開始的摘花生問題,也是從左上角走到右下角,摘儘可能多的花生,是不是這題也能夠類似去處理呢?觀察摘花生問題條件可以發現,題目限制了只能向下或者向右走,類似的問題也都限制了總步數,這是在爲我們用迭代的形式進行狀態轉移提供方便,而本題可以向上下左右進行狀態轉移,迭代的形式不好實現,用BFS進行狀態轉移卻很方便。既然本題是類似於八數碼問題,自然也可以使用A*算法解決,這裏就僅用普通的BFS去求解了。

觀察輸入的格式,首先讀入兩個格子座標以及之間邊的類型,讀入x1,y1,x2,y2,Gi後,兩個格子的座標可以壓縮爲z1,z2兩個編號,直接g[z1][z2]=g[z2][z1]=Gi即可,因爲是雙向邊,所以要存兩次。接着讀入哪些座標放了鑰匙,如果一個格子只放一把鑰匙,直接key[z] = k即可,但是條件是可以放多把鑰匙,就直接用key數組記錄下該座標下存放的所有鑰匙的狀態吧,讀入時key[z] |= k就行了。BFS的過程不再贅述,放進隊列裏的應該是由格子編號z和持有鑰匙狀態st構成的二元組,初始狀態是{1,key[1]},然後在隊列非空時不斷出隊,取隊頭元素,嘗試向四個方向轉移,注意這裏我是將格子編號從1開始,因此將其轉化爲座標時需要先減一再加一。從0開始編號的話,x = z / m,y = z % m,m是列數;從1開始編號的話x = (z - 1) / m + 1,y = (z - 1) % m + 1,這裏從1開始編號的轉換特別容易出錯。如果轉移到棋盤外,或者遇見牆了就不向這個方向轉移;如果從當前狀態到下一個格子間有門,看下當前狀態是否有該類門的鑰匙,有就轉移,不然不轉移。轉移到新格子後,新格子上有鑰匙就嘗試更新下狀態,如果新格子的狀態之前沒有到達過就加入到隊列裏,到達終點就返回結果。

本題看起來複雜,實際上不過是動態規劃、BFS和狀態壓縮三者的結合,還是比較簡單的,總的代碼如下:

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N = 105,M = 12;
typedef pair<int,int> PII;
int g[N][N],key[N],d[N][1<<M];
bool st[N][1<<M];
int n,m,p,k,s;
int dx[] = {0,1,0,-1};
int dy[] = {1,0,-1,0};
queue<PII> q;
int get(int x,int y){
    return (x - 1) * m + y;
}
int bfs(){
    int t = get(1,1);
    q.push({t,key[t]});
    st[t][key[t]] = true;
    memset(d,0x3f,sizeof d);
    d[t][key[t]] = 0;
    while(q.size()){
        PII u = q.front();
        q.pop();
        int z = u.first,v = u.second;
        for(int i = 0;i < 4;i++){
            int x = (z - 1) / m + dx[i] + 1,y = (z - 1) % m + dy[i] + 1;
            int v1 = v,z1 = get(x,y);
            if(!x || !y || x > n || y > m || !g[z][z1]) continue;//越界或者有牆
            if(g[z][z1] != -1){//如果有門
                if(!(v >> g[z][z1] & 1))    continue;//沒該類門鑰匙就不轉移
            }
            v1 |= key[z1];//更新鑰匙狀態
            if(!st[z1][v1]){//每到達過的狀態才入隊
                q.push({z1,v1});
                st[z1][v1] = true;
                d[z1][v1] = d[z][v] + 1;
            } 
            if(z1 == n * m) return d[z1][v1];//到達終點
        }
    }
    return -1;
}
int main(){
    cin>>n>>m>>p;
    cin>>k;
    int x1,y1,x2,y2,z,z1,z2;
    memset(g,-1,sizeof g);//-1表示沒有障礙
    while(k--){
        cin>>x1>>y1>>x2>>y2>>z;
        z1 = get(x1,y1),z2 = get(x2,y2);
        g[z1][z2] = g[z2][z1] = z;
    }
    cin>>s;
    while(s--){
        cin>>x1>>y1>>z;
        key[get(x1,y1)] |= 1 << z;
    }
    cout<<bfs()<<endl;
    return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章