洛谷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函数)
这里写图片描述

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