UASCO checker, 不要滿足慣性思維

Frankly speaking,第一眼看這個題真沒勁,古董題,N皇后。不過,第一次提交代碼之後我明白了,是我自己太沒專研精神了。N皇后是回溯或者說深度優先搜索的典範,我就是初學回溯和DFS時接觸到N皇后的,所以我飛敲鍵盤寫出了下面的代碼(這個不是直接提交的代碼,是後來我測試時用的代碼,但是算法當然是一致的),當然,請輕拍。

#include<iostream>

#include<Windows.h>

using namespace std;

int x[100]; // at most 100 queues

int n, times, num;

inline void init(){for(int i =0; i < 100; ++i) x[i] = 0;}

inline int abs(int a) { return a >= 0 ? a : -a; }

 

inline bool check(int k, int i){

    for(int  j = 1; j < k; ++j)

        if((x[j]==i) || (abs(x[j]-i) == abs(j-k)) )

            return false;

 

    return true;

}

 

void backtrack(int k){

    for(int i = 1; i < n+1; i++){

        if(check(k, i)){

            x[k] = i;

            if(k == n){

                ++num;

                if(times < 3){

                    ++times;

                    for(int j = 1; j < n+1; ++j) cout<<x[j]<<' ';

                    cout<<endl;

                }

            }

            else backtrack(k+1);

        }

    }

}

 

int main(){

    while(cin>>n){

        times = 0, num = 0;

        init();

        DWORD t = GetTickCount();

        backtrack(1);

        std::cout<<"Find "<<num<<" solutions. "<<"Using "<<GetTickCount() - t<<"ms./n/n";

    }

    return 0;

}

痛定思痛,要節省時間,我琢磨着三個考慮:空間換時間;位運算提速;擴大剪枝力度甚至拿上構造法這個法寶。其實這個三個基本上也可以統一起來,尤其是構造思路,我最後的代碼也體現了這一點。先說說構造。見圖說話,這個是很需要抽象思維與建模能力的,以5*5爲例,下面是一個可能的狀態。

這個狀態有三個限制——其實也就是構造時可以利用的三個特性:橫豎排唯一,左對角線唯一,右對角線唯一。可能我們收到回溯、搜索的思維影響太大了,我們都會假設呈現在我們眼前的是整個棋盤,然後有步驟地去試每個位置,說到這,空間換時間的思路基本成型,我們設置一些標記表示某個位置是否可以放入,而不是如前面的代碼那樣再去計算,見inline bool check(int k, int i)。這樣當然會提速,不過我總覺得這個check用不了多少時間,check的時間應該是常數時間,當然,ACM題裏常數時間也有可能要人命;再進一步,如果開一個bool數組作爲標記,不但空間耗費大了不少,檢查數組的時間開銷也不得不計——如果你是個追求極致效率的geek。那麼就容易想到納入位運算了。用一個整數的一個bit表示是否佔領。代碼我沒專門寫,因爲我後面的最終程序也用到了這個思路,這個也不多說,我省點時間力氣說說分步構造——其實也還是搜索,待會便知。

接過前面一句話,“可能我們收到回溯、搜索的思維影響太大了,我們都會假設呈現在我們眼前的是整個棋盤,然後有步驟地去試每個位置”,跳出慣性,我們分步去搞,先看怎麼分步(how to do),再看分步的好處(why to do),最後說說這種比較巧妙的思維如何成爲我們的essential skillspongba老大名言:我們需要的不是相對論,而是想出了相對論的那個大腦!繼續看圖說話。

其實都不用我說話了。接下來看看分步的好處,主要是大量的剪枝與方便使用位運算(這個有點牽強,因爲如果按照這個分佈的法子來解題,位運算有噱頭的感覺),前者,那就是每次放入一個皇后後,下一個皇后要再放進來時所有的合法位置已經被算出來了,接下來就是怎麼表示的問題,可能用一個std::vector<int>來表示更簡潔明瞭,不過遞歸時傳vector可能很沒效率,不過估計也可以不計,但我有一種執意要使用位運算的感覺,總感覺會快一些。下面先貼代碼。

/*

ID: fairyroad

TASK:checker

LANG:C++

*/

#include<fstream>

using namespace std;

ifstream fin("checker.in");

ofstream fout("checker.out");

 

const int tag[21] = {0, 1, 1<<1, 1<<2, 1<<3, 1<<4, 1<<5, 1<<6, 1<<7, 1<<8, 1<<9, 1<<10,1<<11, 1<<12, 1<<13, 1<<14, 1<<15, 1<<16, 1<<17, 1<<18, 1<<19};

int n, flag, count;

int res[15];

 

inline int findbit(int num){

    int i = 1;

    while(i <= n){

        if(!(num & tag[i])) return i;

        ++i;

    }

    return n+1;

}

 

 

void nqueue(int row, int leftdiag/*left diagonal*/, int rightdiag, int len){

    if(len <= n){

        int opt, pos;

        opt = flag & (row|leftdiag|rightdiag);

        while(opt != flag){

            pos = findbit(opt);

            if(pos <= n){

              if(count < 3)

                    res[len] = pos;

                opt = opt|tag[pos];

                nqueue(row|tag[pos], (leftdiag|tag[pos])<<1, (rightdiag|tag[pos])>>1, len+1);

            }

            else return;

        }

    }

    else{

        ++count;

        if(count <= 3){

            for(int k = 1; k < n; ++k )

                fout<<res[k]<<' ';

            fout<<res[n]<<endl;

        }

    }

}

 

int main()

{

       fin>>n;

       flag=(1<<n)-1;

       nqueue(0,0,0,1);

       fout<<count<<"/n";

       return 0;

}

其實我這個也不是蠻好的代碼,現在是真懶了,到這裏,提交一下代碼,看結果,也算快的了。那就這樣了吧。前面圖中左邊的之前單純的回溯法的時間開銷,後者是使用分部構造後的時間測試。

        

   

這個辦法說是構造也不完全正確,但是有構造的思維痕跡在裏面。最後是前面留下來的一個問題。這種比較巧妙的思維如何成爲我們的essential skills,這個題目俺只有一個心得:多懷疑自己的直覺思維或者說慣性思維,對他們保持警惕,這種意識才是“我思故我在”的真實涵義,因爲謂語是“思”,不是“信”,就是說,一切有效的觀念,都是從懷疑開始,向可能性敞開。

 

微博: http://t.sina.com.cn/g7tianyi

豆瓣:http://www.douban.com/people/Jackierasy/

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