可怕的宇宙射線(dfs/bfs+剪枝)

問題描述

\hspace{17pt}衆所周知,瑞神已經達到了CS本科生的天花板,但殊不知天外有天,人外有苟。在浩瀚的宇宙中,存在着一種叫做苟狗的生物,這種生物天生就能達到人類研究生的知識水平,並且天生擅長CSP,甚至有全國第一的水平!但最可怕的是,它可以發出宇宙射線!宇宙射線可以摧毀人的智商,進行降智打擊!
\hspace{17pt}宇宙射線會在無限的二維平面上傳播(可以看做一個二維網格圖),初始方向默認向上。宇宙射線會在發射出一段距離後分裂,向該方向的左右45°方向分裂出兩條宇宙射線,同時威力不變!宇宙射線會分裂n次,每次分裂後會在分裂方向前進aia_i個單位長度。
\hspace{17pt}現在瑞神要帶着他的小弟們挑戰苟狗,但是瑞神不想讓自己的智商降到普通本科生zjm那麼菜的水平,所以瑞神來請求你幫他計算出共有多少個位置會被"降智打擊"

Input

輸入第一行包含一個正整數n,(n<=30)n,(n<=30),表示宇宙射線會分裂n次。
第二行包含n個正整數a1,a2,,ana_1,a_2,\cdots,a_n,第ii個數ai(ai5)a_i(a_i\le 5)表示第ii次分裂的宇宙射線會在它原方向上繼續走多少個單位長度。

Output

輸出一個數ans,表示有多少個位置會被降智打擊。

Sample input

4
4 2 2 3

Sample output

39

數據範圍

數據點 n
10% <=10
40% <=20
100% <=30

時間與內存限制 1000ms 2632144KB

樣例解釋

下圖描繪了樣例中宇宙射線分裂的全過程,僅做參考
在這裏插入圖片描述

解題思路

\hspace{17pt}這個題很明顯是一個搜索類問題,使用dfs還是bfs都可以,但是要注意的是剪枝的重要性。這個題n的數據最大達到了30,如果每次分裂的點都進行搜索的話,時間複雜度是O(2n)O(2^n),絕對頂不住,只能拿到40分。那我們想要100分就需要剪枝操作。
\hspace{17pt}剪枝的方法有很多種,關鍵是如何找到正確的方法,我最開始的想法是,如果當前點被搜索過,並且搜索這個點的時候,方向和向後需要搜索的距離一致,就剪掉。這樣一個node節點存儲的就是橫座標x,縱座標y,方向dir,距離dis。沒錯,這就是我最開始的想法,所以我的結果如下:
在這裏插入圖片描述
\hspace{17pt}靜下心來仔細思考一下,就知道錯在哪裏了,挺明顯的,有可能以後搜索到這個位置,搜索的距離和方向一致,但是下一步搜索的方向和距離和最開始標記的有可能不同了,這樣就剪枝過度了,造成部分位置的丟失。
\hspace{17pt}後來的思路是,僅僅剪掉這一層搜索的時候,重複的位置(也要保證方向相同才剪掉),因爲最終搜索的範圍是300*300這樣的區間,但是當搜索深度很大的時候,分裂的節點必定有很多重複,這樣做到了剪枝操作,並且沒有重複節點的丟失。node結構體中存儲的dis就換成了當前搜索的層數fl(floor的簡寫)。
\hspace{17pt}我的代碼使用的是bfs搜索,利用兩個map來存儲當前節點是否搜索過和當前節點在當層此方向是否搜索過,利用隊列來保存當前要搜索的直線路徑的起點。和傳統bfs不同的是,這種題目需要兩個層次之間分離,可以做一個標記表明隊列中彈出的節點是哪一層,也可以直接用兩個隊列來回倒。我使用的是後者,當前層次的直線起始點放在qdir中,下一層的放在qdir2中,當qdir爲空,則將qdir2中的數據放在qdir中(其實這個樣子還增加了一定的時間,推薦使用標記標出層次)。更加詳細的解析見代碼註釋。

完整代碼

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <string>
#include <climits>
#include <algorithm>
#include <queue>
#include <vector>
#include <map>
using namespace std;

struct node
{
    int x,y;//當前節點
    int dir;//當前節點搜索的方向,從1-8分別爲上,右上,右,右下,下,左下,左,左上
    int fl;//當前所屬於的層數
    node(int _x,int _y,int _dir,int _fl):x(_x),y(_y),dir(_dir),fl(_fl){}
    bool operator<(const node &a) const
    {
        if(x<a.x) return true;
        else if(x==a.x){
            if(y<a.y) return true;
            else if(y==a.y){
                if(dir<a.dir) return true;
                else if(dir==a.dir){
                    if(fl<a.fl) return true;
                }
            }
        }
        return false;
    }
};
int n,a[31],ans;
int dx[]={0,1,1,1,0,-1,-1,-1};
int dy[]={1,1,0,-1,-1,-1,0,1};
map<node,bool> mp;//判斷當前節點是否搜索過(包含方向和距離),判斷該節點是否應該再次搜索
map<long long ,bool> mp2;//單純記錄當前節點是否搜索過,使用long long是直接用x*1000+y來哈希一下
queue<node> qdir;//存儲下一層的所有起始節點
queue<node> qdir2;
void bfs()
{
    node start(150,149,0,1);
    //假設初始節點位置爲(150,149),向着上方前進,層數爲1(注意,(150,149)這個點是不要的,是從這裏開始的,這樣整個射線的起點就是(150,150)了)
    //解釋:本體數據量不大,從初始節點開始,向上下左右最多走150格,默認座標是平面直角座標,左下角爲(0,0)
    qdir.push(start);

    for (int i=2; i<=n+1; i++)//a[1]生成的節點要走的距離是a[2],所以從i=2開始
    {
        while(!qdir.empty())
        {
            node lattice=qdir.front(); qdir.pop();
            for (int j=1; j<=a[lattice.fl]; j++)//有了起始節點後,向後搜索a[fl]個節點
            {
                if(!mp2[(lattice.x+j*dx[lattice.dir])*1000+(lattice.y+j*dy[lattice.dir])]){//當前節點沒有搜索過
                    mp2[(lattice.x+j*dx[lattice.dir])*1000+(lattice.y+j*dy[lattice.dir])]=true; ans++;
                }
            }
            if(i!=n+1)//最後一次不用分裂了,直接跳出就行了
            {
                //開始分裂成兩個節點
                node newnode1(lattice.x+a[i-1]*dx[lattice.dir],lattice.y+a[i-1]*dy[lattice.dir],(lattice.dir+1)%8,i);
                node newnode2(lattice.x+a[i-1]*dx[lattice.dir],lattice.y+a[i-1]*dy[lattice.dir],(lattice.dir+7)%8,i);
                if(!mp[newnode1]){
                    mp[newnode1]=true; qdir2.push(newnode1);
                }
                if(!mp[newnode2]){
                    mp[newnode2]=true; qdir2.push(newnode2);
                }
            }
        }
        while(!qdir2.empty()){
            qdir.push(qdir2.front());qdir2.pop();
        }
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin>>n;
    for (int i=1; i<=n; i++)
        cin>>a[i];

    bfs();

    cout<<ans<<endl;
    return 0;
}

關於此題的優秀博客鏈接

傳送門 這個大佬使用的剪枝方法是對稱剪枝,利用的dfs只向右搜索,我提交後發現時間比我的要快3倍,空間少6倍。這個應該纔是正解。
傳送門 這個文章裏面提到的和我的方法類似,不過他是使用的dfs,可以參考。

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