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,这个题目俺只有一个心得:多怀疑自己的直觉思维或者说惯性思维,对他们保持警惕,这种意识才是“我思故我在”的真实涵义,因为谓语是“思”,不是“信”,就是说,一切有效的观念,都是从怀疑开始,向可能性敞开。