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 skills,pongba老大名言:我們需要的不是相對論,而是想出了相對論的那個大腦!繼續看圖說話。
其實都不用我說話了。接下來看看分步的好處,主要是大量的剪枝與方便使用位運算(這個有點牽強,因爲如果按照這個分佈的法子來解題,位運算有噱頭的感覺),前者,那就是每次放入一個皇后後,下一個皇后要再放進來時所有的合法位置已經被算出來了,接下來就是怎麼表示的問題,可能用一個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,這個題目俺只有一個心得:多懷疑自己的直覺思維或者說慣性思維,對他們保持警惕,這種意識才是“我思故我在”的真實涵義,因爲謂語是“思”,不是“信”,就是說,一切有效的觀念,都是從懷疑開始,向可能性敞開。