1. 程式人生 > >八數碼的八境界(待更新...)

八數碼的八境界(待更新...)

寫的很棒的一篇部落格。

Eight

Time Limit: 1000MS Memory Limit: 65536K

Total Submissions: 30591Accepted: 13309Special Judge

Description

The 15-puzzle has been around for over 100 years; even if you don't know it by that name, you've seen it. It is constructed with 15 sliding tiles, each with a number from 1 to 15 on it, and all packed into a 4 by 4 frame with one tile missing. Let's call the missing tile 'x'; the object of the puzzle is to arrange the tiles so that they are ordered as: 

 1  2  3  4 

 5  6  7  8 

 9 10 11 12 

13 14 15  x 

where the only legal operation is to exchange 'x' with one of the tiles with which it shares an edge. As an example, the following sequence of moves solves a slightly scrambled puzzle: 

 1  2  3  4    1  2  3  4    1  2  3  4    1  2  3  4 

 5  6  7  8    5  6  7  8    5  6  7  8    5  6  7  8 

 9  x 10 12    9 10  x 12    9 10 11 12    9 10 11 12 

13 14 11 15   13 14 11 15   13 14  x 15   13 14 15  x 

           r->           d->           r-> 

The letters in the previous row indicate which neighbor of the 'x' tile is swapped with the 'x' tile at each step; legal values are 'r','l','u' and 'd', for right, left, up, and down, respectively. 

Not all puzzles can be solved; in 1870, a man named Sam Loyd was famous for distributing an unsolvable version of the puzzle, and 

frustrating many people. In fact, all you have to do to make a regular puzzle into an unsolvable one is to swap two tiles (not counting the missing 'x' tile, of course). 

In this problem, you will write a program for solving the less well-known 8-puzzle, composed of tiles on a three by three 

arrangement. 

Input

You will receive a description of a configuration of the 8 puzzle. The description is just a list of the tiles in their initial positions, with the rows listed from top to bottom, and the tiles listed from left to right within a row, where the tiles are represented by numbers 1 to 8, plus 'x'. For example, this puzzle 

 1  2  3 

 x  4  6 

 7  5  8 

is described by this list: 

 1 2 3 x 4 6 7 5 8 

Output

You will print to standard output either the word ``unsolvable'', if the puzzle has no solution, or a string consisting entirely of the letters 'r', 'l', 'u' and 'd' that describes a series of moves that produce a solution. The string should include no spaces and start at the beginning of the line.

Sample Input

 2  3  4  1  5  x  7  6  8 

Sample Output

ullddrurdllurdruldr

境界一、 暴力廣搜+STL

  開始的時候,自然考慮用最直觀的廣搜,因為狀態最多不超過40萬,計算機還是可以接受的,由於廣搜需要記錄狀態,並且需要判重,所以可以每次圖的狀態轉換為一個字串,然後儲存在stl中的容器set中,通過set的特殊功能進行判重,由於set的內部實現是紅黑樹,每次插入或者查詢的複雜度為Log(n),所以,如果整個演算法遍歷了所有狀態,所需要的複雜度為n*Log(n),在百萬左右,可以被計算機接受,由於對string操作比較費時,加上stl全面性導致 速度不夠快,所以計算比較費時,這樣的程式碼只能保證在10秒內解決任何問題。但,明顯效率不夠高。POJ上要求是1秒,無法通過。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>
#include <algorithm>
#include <queue>
#include <set>
#include <map>
using namespace std;

const int maxn = 1e6+10;
int dir[4][2]={{-1,0},{1,0},{0,-1},{0,1}};
string d="durl";
map<string,string>mp;
set<string>f;
string beg="12345678x";

struct node{
    string str;
    int pos;
    string path;
    node(){}
    node(string s,int p,string pa){
        str=s;
        pos=p;
        path=pa;
    }
}st;

void Bfs(){
    queue<node>q;
    st=node(beg,8,"");
    mp[beg]="";
    f.insert(beg);
    q.push(st);
    while(!q.empty()){
        st=q.front();q.pop();
        int a=st.pos/3,b=st.pos%3;
        for(int i=0;i<4;i++){
            int x=a+dir[i][0],y=b+dir[i][1];
            if(x<0||x>2||y<0||y>2) continue;
            int pos=3*x+y;
            swap(st.str[st.pos],st.str[pos]);
            if(f.find(st.str)!=f.end()){
                swap(st.str[st.pos],st.str[pos]);
                continue;
            }
            q.push(node(st.str,pos,d[i]+st.path));
            mp[st.str]=d[i]+st.path;
            f.insert(st.str);
            swap(st.str[st.pos],st.str[pos]);
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    Bfs();
    char str[15];
    while(~scanf("%s",str)) {
        string v="";
        v=v+str[0];
        for(int i=1;i<=8;i++) {
            scanf("%s",str);
            v=v+str[0];
        }
        if(mp[v]=="") cout<<"unsolvable"<<endl;
        else cout<<mp[v]<<endl;
    }
    return 0;
}


境界二、廣搜+雜湊

  考慮到費時主要在STL,對於大規模的遍歷,用到了ST的set和string,在效率上的損失是很大的,因此,現在面臨一個嚴重的問題,必須自己判重,為了效率,自然是自己做hash。有點麻煩,hash函式不好想,實際上是9!種排列,需要每種排列對應一個數字。網上搜索,得知了排列和數字的對應關係。取n!為基數,狀態第n位的逆序值為雜湊值第n位數。對於空格,取其為9,再乘以8!。例 如,1 3 7 24 6 9 5 8 的雜湊值等於:0*0! + 2*1! + 0*2! + 1*3! + 3*4! +1*5! + 0*6! + 1*7! + 0*8! <9!具體的原因可以去查查一些數學書,其中1 2 34 5 6 7 8 9 的雜湊值是0 最小,9 8 7 6 54 3 2 1 的雜湊值是(9!-1)最大。而其他值都在0 到(9!-1) 中,且均唯一。然後去掉一切STL之後,甚至包括String之後,得到單向廣搜+Hash的程式碼,演算法已經可以在三秒鐘解決問題,可是還是不夠快!POJ時限是1秒,後來做了簡單的更改,將路徑記錄方法由字串改為單個字元,並記錄父節點,得到解,這次提交,266ms是解決單問題的上限。當然,還有一個修改的小技巧,就是逆序對數不會改變,通過這個,可以直接判斷某輸入是否有可行解。由於對於單組最壞情況的輸入,此種優化不會起作用,所以不會減少單組輸入的時間上限。

境界三、廣搜+雜湊+打表

  好,問題可以在200—300ms間解決,可是,這裡我們注 意到一個問題,最壞情況下,可能搜尋了所有可達狀態,都無法找到解。如果這個題目有多次輸入的話,每次都需要大量的計算。其實,這裡可以從反方向考慮下,從最終需要的狀態,比如是POJ 1077需要的那種情況,反著走,可達的情況是固定的。可以用上面說的那種相應的Hash的方法,找到所有可達狀態對應的值,用一個bool型的表,將可達狀態的相應值打表記錄,用“境界三”相似的方法記錄路徑,打入表中。然後,一次打表結束後,每次輸入,直接呼叫結果!這樣,無論輸入多少種情況,一次成功,後面在O(1)的時間中就能得到結果!這樣,對於ZOJ的多組輸入,有致命的幫助!

境界四、雙向廣搜+雜湊

  Hash,不再贅述,現在,我們來進行進一步的優化,為了減少狀態的膨脹,自然而然的想到了雙向廣搜,從輸入狀態點和目標狀態1 2 3 4 5 6 7 8 9同時開始搜尋,當某方向遇到另一個方向搜尋過的狀態的時候,則搜尋成功,兩個方向對接,得到最後結果,如果某方向遍歷徹底,仍然沒有碰上另一方向,則無法完成。

境界五、A*+雜湊+簡單估價函式

  用到廣搜,就可以想到能用經典的A*解決,用深度作為g(n),剩下的自然是啟發函數了。對於八數碼,啟發函式可以用兩種狀態不同數字的數目。接下來就是A*的套路,A*的具體思想不再贅述,因為人工智慧課本肯定比我講的清楚。但是必須得注意到,A*需要滿足兩個條件:

1.h(n)>h'(n),h'(n)為從當前節點到目標點的實際的最優代價值。

2.每次擴充套件的節點的f值大於等於父節點的f值小。

自然,我們得驗證下我們的啟發函式,h驗證比較簡單不用說,由於g是深度,每次都會較父節點增1。再看h,認識上, 我們只需要將h看成真正的“八數碼”,將空格看空。這裡,就會發現,每移動一次,最多使得一個數字迴歸,或者說不在位減一個。 h最多減小1,而g認為是深度,每次會增加1。所以,f=g+h, 自然非遞減,這樣,滿足了A*的兩個條件,可以用A*了!

境界六、A*+雜湊+曼哈頓距離

  A*的核心在啟發函式上,境界五若想再提升,先想到的是啟發函式。這裡,曼哈頓距離可以用來作為我們的啟發函式。曼哈頓距離聽起來神神祕祕,其實不過是“絕對軸距總和”,用到八數碼上,相當與將所有數字歸位需要的最少移動次數總和。作為啟發函式,自然需要滿足“境界五”提到的那兩個條件。現在來看這個曼哈頓距離,第一個條件自然滿足。對於第二個,因為空格被我們剝離出去,所以交換的時候只關心交換的那個數字,它至多向目標前進1,而深度作為g每次是增加1的,這樣g+h至少和原來相等,那麼,第二個條件也滿足了。A*可用了,而且,有了個更加優化的啟發函式。

境界七、A*+雜湊+曼哈頓距離+小頂堆

  經過上面優化後,我們發現了A*也有些雞肋的地方,因為需要每次找到所謂Open表中f最小的元素,如果每次排序,那麼排序的工作量可以說是很大的,即使是快排,程式也不夠快!這裡,可以想到,由於需要動態的新增元素,動態的得到程式的最小值,我們可以維護一個小頂堆,這樣的效果就是。每次取最小元素的時候,不是用一個n*Log(n)的排序,而是用log(n)的查詢和調整堆,好,演算法又向前邁進了一大步。

境界八、IDA*+曼哈頓距離

  IDA*即迭代加深的A*搜尋,實現程式碼是最簡練的,無須狀態判重,無需估價排序。那麼就用不到雜湊表,堆上也不必應用,空間需求變的超級少。效率上,應用了曼哈頓距離。同時可以根據深度和h值,在找最優解的時候,對超過目前最優解的地方進行剪枝,這可以導致搜尋深度的急劇減少,所以,這,是一個致命的剪枝!因此,IDA*大部分時候比A*還要快,可以說是A*的一個優化版本!