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/

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