洛谷Oj-P1092 蟲食算-深度優先搜索

問題描述:
所謂蟲食算,就是原先的算式中有一部分被蟲子啃掉了,需要我們根據剩下的數字來判定被啃掉的字母。來看一個簡單的例子:
http://paste.ubuntu.com/25448822/
其中#號代表被蟲子啃掉的數字。根據算式,我們很容易判斷:第一行的兩個數字分別是5和3,第二行的數字是5。
現在,我們對問題做兩個限制:
首先,我們只考慮加法的蟲食算。這裏的加法是N進制加法,算式中三個數都有N位,允許有前導的0。
其次,蟲子把所有的數都啃光了,我們只知道哪些數字是相同的,我們將相同的數字用相同的字母表示,不同的數字用不同的字母表示。如果這個算式是N進制的,我們就取英文字母表午的前N個大寫字母來表示這個算式中的0到N-1這N個不同的數字:但是這N個字母並不一定順序地代表0到N-1)。輸入數據保證N個字母分別至少出現一次。
http://paste.ubuntu.com/25448824/
上面的算式是一個4進制的算式。很顯然,我們只要讓ABCD分別代表0123,便可以讓這個式子成立了。你的任務是,對於給定的N進制加法算式,求出N個不同的字母分別代表的數字,使得該加法算式成立。輸入數據保證有且僅有一組解
AC代碼:

int n,id = 1;
int a[30],b[30],c[30];//a + b = c
int num[30];//num[i]是第i個字母(第一個字母爲A)所對應的數字
int Next[30];//確定先給哪一個字母賦值(即搜索順序)
bool book[30];//標記數組
bool stop;//使得深度優先搜索立即停止
int c2i(char c)//將字符轉換爲數字
{
    return c - 'A' + 1;
}
void print()//將答案數組打印輸出
{
    for(int i = 1; i <= n; ++i)
        cout << num[i] << ' ';
    cout << endl;
}
bool prune()//是否應該剪枝
{
    if(num[a[1]] + num[b[1]] >= n)//如果最高位需要進位
        return true;//剪枝
    for(int i = n; i >= 1; --i)
    {
        int A = num[a[i]];
        int B = num[b[i]];
        int C = num[c[i]];
        if(A == -1 || B == -1 || C == -1)//如果還沒有搜出來
            continue;//先不管它
        if((A + B) % n != C && (A + B + 1) % n != C)//如果不進位和進位都不滿足要求
            return true;//剪枝
    }
    return false;
}
bool check()
{
    int t = 0;
    //注意是num[a[i]]、num[b[i]]和num[c[i]]
    //纔是對應的數字
    for(int i = n; i >= 1; --i)//從豎式的左邊向右掃
    {
        int A = num[a[i]];
        int B = num[b[i]];
        int C = num[c[i]];
        if((A + B + t) % n != C)
            return false;
        t = (A + B + t) / n;
    }
    return true;
}
void dfs(int step)//step爲搜索的層數
{
    if(stop == true)//如果找到答案了
        return;
    if(prune() == true)//如果符合剪枝的條件
        return;
    if(step == n + 1)//如果到達了第n+1層,檢查一下答案
    {
        if(check() == true)//如果找到了解
        {
            print();//輸出
            stop = true;//結束
        }
        return;
    }
    for(int i = n - 1; i >= 0; --i)//枚舉n-1到0
    {
        if(book[i] == false)//如果該數字沒被用過
        {
            book[i] = true;//標記
            //就是在這裏展示Next數組的威力了
            num[Next[step]] = i;//第Next[step]個字母所代表的數字是i
            dfs(step + 1);//繼續搜索
            num[Next[step]] = -1;//爲什麼不寫着一句程序就不對呢,請看圖解(注①)
            book[i] = false;//取消標記
        }
    }
}
void get_next(int n)//對於第n個字母
{
    if(book[n] == false)//如果第n個字母還沒有遍歷到,就說明其出現的順序比較靠後
    {
        book[n] = true;//標記一下
        Next[id] = n;//第n個字母在Next數組中的下標是id
        id++;
    }
    return;
}
int main()
{
    cin >> n;//n進制
    string s1,s2,s3;//字符串
    cin >> s1 >> s2 >> s3;
    for(int i = 0; i <= n - 1; ++i)//將字符串轉換爲整型數組。A->1,B->2。a[i]的含義是第a[i]個字母
    {
        //注意數組a、b、c的下標從1開始了
        a[i + 1] = c2i(s1[i]);
        b[i + 1] = c2i(s2[i]);
        c[i + 1] = c2i(s3[i]);
    }
    for(int i = n; i >= 1; --i)//先垂直方向上從上到下,再水平方向上從右到左
    {
        get_next(a[i]);
        get_next(b[i]);
        get_next(c[i]);
    }
    memset(num,-1,sizeof(num));//初始化答案數組
    memset(book,0,sizeof(book));//重置標記數組
    dfs(1);//從樹的第一層開始搜
    return 0;
}

解決方法:
題目要求的是一個序列,依次代表着A,B,C,D等字母。
全排列的時間複雜度爲O(n!),會嚴重超時,所以剪枝對本題來說顯得特別重要
同樣是求排列,爲什麼不用next_permutation()呢,因爲它是O(n!)的,而搜索可以進行剪枝,從而提高速度
《啊哈!算法》中我們學習過如何搜索一個序列,同樣的方法可以拿來用。其中當前搜索的層次數是一個很重要的數字
由於本題目是一個大工程,所以要寫許多函數,基本的有將字符轉換成數字(方便操作)的函數、打印函數。關鍵的有搜索函數、答案判斷函數、剪枝函數以及確定搜索順序的函數
對解空間有一個認識對解本題是非常有幫助的。請想象一棵狀態轉移樹
兩個剪枝:①最高位剪枝②進位剪枝(由於是加法,所以進位要麼爲0要麼爲1)
最高位不進位,說明最高位字母所代表的數字比較小,將較大的數賦給在較低位出現的字母,那麼高位上出現的字母所代表的數就小了,這是符合題意的(get_next數組的作用)
搜索順序也對提高程序的效率有很大的幫助!!!
在畫狀態轉移樹時,數據規模要儘可能地小,才能畫開
注①
如果不寫該語句,在回溯到④時,num數組內的元素還是2 1 0,此時剪枝函數會錯誤地將該枝剪去(沒通過check函數必定通不過prune函數)
這裏寫圖片描述

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