• DFS做题小结


    一、深入理解DFS

    • 采用递归写法
    • 深度优先,思路就是沿着一条路一直走,直到走到死胡同,原路返回,返回到有多条道路的地方换其他路走。直到这条支路全部都访问过了,按照原路返回,回到起点,如果起点还有别的支路,那么继续访问,反之结束整个搜索过程。
    • 实际解题的时候不可能无所约束的搜索下去,原因之一是会超时(TLE),原因之二就是没有那个必要。那么就需要减小搜索的规模,俗称剪枝。个人的理解是,当搜索到某一步的时候,继续搜索下去的解,明显不满足题目的要求时,终止这次搜索。

    下面用一张图来加深理解:

    Tip:如图1-1,数字为访问顺序,红色代表前进的过程,蓝色代表返回的过程。这里可以看到,是永远先访问上边的节点,其次是下面的节点。

    Tip:如图1-2,绿色节点均为不满足题意的解,那么当我搜索到绿色箭头所指向的点的时候,就没必要继续往下搜索了,即后续的3、4、5、6、7、8步骤均为多余的。

     

    二、例题讲解——hdu1342

    题意:给出k(6 < k < 13)个数字,要求从这k个数字中选出升序的6个数字,并且按照字典序输出全部的可能,给出的k个数字已经按照升序排列好。

    对于给定数字,面临的选择就是选或者不选,是不是和上面的树逻辑上很相似。先上代码,揉碎了慢慢写。。

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    int a[20],b[10],n;
    void dfs(int num, int pos)
    {
        if(num == 7){
            for(int i =1 ;i<num; ++i){
                if(i == 1) printf("%d",b[i]);
                else printf(" %d",b[i]);
            }
            printf("
    ");return;
        }
        if(pos>n) return;
        b[num] = a[pos];
        dfs(num+1,pos+1);
        dfs(num,pos+1);
    }
    int main()
    {
        int t = 0;
        while(scanf("%d",&n) && n != 0){
            if(t!=0) printf("
    ");
            for(int i = 1; i<=n; ++i) scanf("%d",&a[i]);
            dfs(1,1);
            t++;
        }
        return 0;
    }
    View Code

    从main函数开始:

    while(scanf("%d",&n) && n != 0){
            if(t!=0)     printf("
    ");
            for(int i = 1; i<=n; ++i)     scanf("%d",&a[i]);
            t++;
    }
    

    这里是数据的读入部分,题目要求每组数据中间要有空行,所以引入变量t。
    那么关键就在于dfs函数。

    void dfs(int num, int pos)
    {
        if(num == 7){
            for(int i =1 ;i<num; ++i){
                if(i == 1) printf("%d",b[i]);
                else printf(" %d",b[i]);
            }
            printf("
    ");return;
        }
        if(pos>n) return;
        b[num] = a[pos];
        dfs(num+1,pos+1);
        dfs(num,pos+1);
    }
    

    dfs函数有2个形参,num和pos,乍一看不知道他们的作用,先姑且放着。
    之后是对于num是否为7的一个判断。如果为7的话,进行一个输出,应该就是题目要求的输出,数组b中保存着结果。可见num应该是判断是否构成了6位的排列,当num为7递归调用dfs时,用return语句终止这次搜索。原因很简单,题目只需要我找6位排列数,干嘛找7位的。
    这样的判断,叫做递归边界(如有错误请各位指正)。递归边界可以是判断是否找到了解,如果找到了一组可行的解,就不进行递归了。当然要具体问题具体分析。
    向下看,是对pos是否大于n的判断,如果大于n也就终止搜索了。n表示的是每组数据数字的个数,根据这个也可以想到,应该是从n个数中选6个,如果现在的位置是n+1(数据中根本没有这个数),当然不符合题意,终止。接着是一个赋值语句,应该可以想到是选中a数组pos这个位置的数字,把它写到b的num这个位置。

    下面关键来了:

    dfs(num+1,pos+1);
    dfs(num,pos+1);
    

    现在已经选中了a数组pos位置的数字,如果选它的话,那么就看下一位置选谁(这个位置是相对于数组b而言的),如果不选这个数字,那么对于这一位置,我们看看a数组下一个数字选还是不选。

    如图所示,对于a中某一个数,有选或者不选2中选择(蓝色代表选,红色代表不选),组成了这样一颗树,直到num==7的是,结束搜索。

    由此可以总结出dfs大概的函数模型:

    void dfs( 参数 )
    {
        // 递归边界
        // 可以是检查是否满足解的要求
    
        // 完成某系动作
        // 继续递归调用dfs
    }
    

    三、例题讲解——poj1011

    题意:给出一定数量的小木棒的长度,它是由等长的若干木棒随意砍断所得到的。对于给定的一组小木棒,请求出原始木棒的最小长度。

    #include <iostream>
    #include <cstdio>
    #include <algorithm>
    #include <cstring>
    using namespace std;
    int a[65];
    int n;                        //n:n根小棍子                     
    int vis[65];                    //记某一小棒在当前状态下是否已经被用于组合原棒
    
    bool cmp(const int a,const int b)  
    {  
        return a>b;  
    }  
    
    int dfs(int len,int rest,int num)        //len:要拼成的大棍子的长度,rest:还要多长  ,num:可用的小棍子数 
    {
        //rest==0&&num==0说明能用的棒已经没有,而且拼成一根原棒还需的长度为0,这就表示原棒已经完整的由这所有的小棒拼接出来。
        //此时只需返回len(试探成功)
        if(rest==0 && num==0){
            return len;
        }
        //当rest减小到0时,说明一根大木棍拼接完成,它将重新被赋值为len,从而进行下一根大木棍的搜索
        if(rest==0){
            rest = len;    
        }
        for(int i=1;i<=n;i++){
            if(!vis[i] && rest>=a[i]){
                vis[i] = 1;
                if(dfs(len,rest-a[i],num-1)){
                    return len;
                }
                //此条路不通,标记取消 
                vis[i] = 0;        //当换一个原木棒长度进行试探时,要置vis为0,否则会与上次搜索混在一起
                if(a[i]==rest || len==rest){
                    break;
                }
                //跳过重复长度的木棍,当前木棍跟它后面木棍的无法得出合法解,后面跟它一样长度的木棍也不可能得到合法解
                //因为后面相同长度木棍能做到的,前面这根木棍也能做到。
                while(a[i]==a[i+1]){
                    i++;
                }
            }
        }
        return 0;    //深搜完成,仍未返回试探成功,到了函数的最后,只能说明这个试探失败。直接返回0
    }
    
    int main()
    {
        while(cin>>n && !(n==0)){
            int ans=0,sum = 0;        //sum:所有小棒长度之和
            for(int i=1;i<=n;i++){
                scanf("%d",&a[i]);
                sum += a[i];
            }
            sort(a+1,a+1+n,cmp);    //剪枝:从大到小排序后可大大减少递归次数
            //原木棒长度的取值范围为[a[1],sum]
            //枚举这个区间内的数,要满足sum%len==0 
            for(int len=a[1];len<=sum;len++){
                memset(vis,0,sizeof(vis));
                if(sum%len==0){
                    ans = dfs(len,0,n);
                    if(ans)    break;
                }
            }
            printf("%d
    ",ans);    
        }
        return 0;
    }
    View Code

    【dfs形参】

    1. len:当前要拼成的原始木棒长度
    2. rest:当前选用的小木棒的长度之和距离len还缺rest (当它减小到0时,说明一根原始木棒拼接完成,它将重新被赋值为len,从而进行下一根木棒的搜索)
    3. num:当前可用的小木棒的数量

    【剪枝】

    • 将所有题目给的小木棒的长度按照从大到小的顺序排列,这样最长的小木棒为a[1],最短的小木棒为a[1+n];
    • 原始木棒的长度的取值范围为[ a[1],sum ]中;(sum为小棍子的总长度)
    • 当前木棒跟它后面木棒无法得出合法解,后面跟它一样长度的木棒也不可能得到合法解,因为后面相同长度木棒能做到的,前面这根木棒也能做到。

    【重要思想】

    由于过程中要确定某根小棒是否确定成功被接收,它就得提前预知加入这根小棒后其它的小木棒能不能匹配成功,就叫要求在遍历某个小木棒时,对其后的木棒进行递归搜索(深搜的特点)。

    若能匹配成功,则标记当前小木棒为用过,可以直接返回(试探成功);若不能匹配,说明此棒目前不可用,将标记取消,待下一次搜索用。

    若当前木棒不可用,那么与这根小木棒长度相同的木棒也将不可用,直接跳过(剪枝),而且若这个小木棒的长度刚好是rest的长度,那么更能说明后面的不能匹配了,因为如此合适的小棒被接收都不能导至试探成功,后面的小棒更不可能,直接返回0(试探失败 )(剪枝)。还有就是如果len=rest(说明这是新一根原棒,还没有进行匹配),而在预先判断匹配与否时已经判断不能匹配,这样都不能匹配,那么说明以后都不能匹配了(这就是深搜的效果了)。返回0(试探失败)(剪枝)。

     【他家之言】

    • 如果当前木棍能恰好填满一根原始木棍,但因剩余的木棍无法组合出合法解而返回,那么让我们考虑接下来的两种策略,一是用更长的木棍来代替当前木棍,显然这样总长度会超过原始木棍的长度,违法。二是用更短的木棍组合来代替这根木棍,他们的总长恰好是当前木棍的长度,但是由于这些替代木棍在后面的搜索中无法得到合法解,当前木棍也不可能替代这些木棍组合出合法解。因为当前木棍的做的事这些替代木棍也能做到。所以,当出现加上某根木棍恰好能填满一根原始木棍,但又在后面的搜索中失败了,就不必考虑其他木棍了,直接退出当前的枚举。
    • 考虑每根原始木棍的第一根木棍,如果当前枚举的木棍长度无法得出合法解,就不必考虑下一根木棍了,当前木棍一定是作为某根原始木棍的第一根木棍的,现在不行,以后也不可能得出合法解。也就是说每根原始木棍的第一根小木棍一定要成功,否则就返回。
  • 相关阅读:
    nsstring -->nsdictionary
    卫辉市,
    iPhone 手机内存,
    连接错误,等的好漫长,
    BZOJ 1631==USACO 2007== POJ 3268 Cow Party奶牛派对
    POJ 2837 Til the Cows Come Home
    POJ 1285 确定比赛名次
    Codevs 2833 奇怪的梦境
    Codevs 1063 合并果子
    Codevs 1231 最优布线问题
  • 原文地址:https://www.cnblogs.com/xzxl/p/7327212.html
Copyright © 2020-2023  润新知