DFS提高——DFS常见模型

一、概述

DFS只能求出来是否能从起点到终点,不能求出来最短距离。
DFS一般代码比BFS要短一些因为不用维护队列,缺点是递归太深容易爆栈,栈空间应该是1MB左右


1、DFS——连通型

包括在BFS中见到的Flood Fill类型、图和树的遍历
一般题上是问能否从A点走到B点,或者从这个点出发能到达多少个符合要求的点
这类题一般是从棋盘上一个点到另一个点,每个点只需要走一遍,所以不需要恢复现场

例题:1113. 红与黑 - AcWing题库

代码模板:

#include<iostream>
#include<cstring>

using namespace std;

const int N=25;

char g[N][N];
bool st[N][N];
int n,m,cnt=1,bex,bey,T;

int dx[4]={0,-1,0,1};//左上右下
int dy[4]={-1,0,1,0};

void dfs(int x,int y)
{
    for(int i=0;i<4;i++)
    {
        int tx=x+dx[i];
        int ty=y+dy[i];
        if(tx<0 || tx>=n || ty<0 || ty>=m)//出界
            continue;
        if(st[tx][ty] || g[tx][ty]=='#')//用过了或者不能走
            continue;
        st[tx][ty]=1;//标记
        cnt++;//总数+1
        dfs(tx,ty);//遍历下一个
    }
    return ;
}

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    int a,b;
    while(cin>>m>>n,n||m)//n,m=0时结束
    {
        memset(st,0,sizeof st);//别忘了多组数据时要重置st的
        cnt=1;
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<m;j++)
            {
                cin>>g[i][j];
                if(g[i][j]=='@')//记下终点坐标
                {
                    bex=i;
                    bey=j;
                }
            }
        }
        st[bex][bey]=1;
        dfs(bex,bey);
        cout<<cnt<<endl;
    }
    return 0;
}


2、DFS——搜索顺序

搜索顺序类型的题,一般要考虑把所有顺序全部枚举到。
而且需要考虑恢复现场的问题,因为它是把当前棋盘看成一个整体,把棋盘当前的状态,转移成目标状态。
所以我们每次在搜索过程中,标记一个点往下DFS,回来的时候要把这个标记取消掉,方便这一层其他点进行DFS。


例题:1116. 马走日 - AcWing题库

代码模板:


#include<iostream>
#include<cstring>

using namespace std;

const int N=30;

int n,m,bx,by,T,res;
bool st[N][N];

int dx[8]={-1,-2,-2,-1,1,2,2,1};//从左上顺时针到左下
int dy[8]={-2,-1,1,2,-2,-1,1,2};

void dfs(int x,int y,int cnt)
{
    if(cnt==n*m)//结束条件
    {
        res++;
        return ;
    }
    for(int i=0;i<8;i++)//
    {
        int tx=x+dx[i];
        int ty=y+dy[i];
        if(tx<0 || tx>=n || ty<0 || ty>=m)//出界
            continue;
        if(st[tx][ty])//遍历过了
            continue;
        st[tx][ty]=1;//标记遍历过了
        dfs(tx,ty,cnt+1);
        st[tx][ty]=0;//恢复现场
    }
    return ;
}

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    cin>>T;
    while(T--)
    {
        memset(st,0,sizeof st);//每次要重置st数组
        res=0;
        cin>>n>>m>>bx>>by;
        st[bx][by]=1;//起点标记
        dfs(bx,by,1);
        cout<<res<<endl;
    }
    return 0;
}


3、DFS优化——剪枝与优化

常用5个方法:
1、优化搜索顺序
    大部分情况下我们应该先搜分支较小的节点,一般通过对传入的数据先排个序
2、排除等效冗余
    有些题在DFS并不需要考虑顺序,搜1 3 2 和搜1 2 3 是一个结果,一般我们用一个start变量记录当前搜到哪个点就行,下次从这个点往后,之前的不再搜索。
3、可行性剪枝
    最常见的剪枝方式,如果不符合条件直接跳过
4、最优性剪枝
    一般可以初始化一个变量记录当前最优解,如果递归时还没结束就已经超过了最优解,那么明显就不是答案,直接结束。
5、记忆化搜索(DP)
    DP问题了,在这里不多讲

例题:167. 木棒 - AcWing题库

代码模板:


#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 70;

int n;
int w[N];//小木棍长度
int sum,length;//小木棍长度总和,当前每根木棍长度
bool st[N];//标记哪些小木棍用过了,1代表用过了

bool dfs(int u,int cur,int start)//u是大棍个数,cur是当前大棍长度,start记录从哪开始遍历
{
    if(u*length==sum)//如果大棍个数*大棍长度=总和
        return true;//成功
    if(cur==length) //如果当前大棍的长度已经等于应该的长度,开一个新的大棍
        return dfs(u+1,0,0);
    for(int i=start;i<n;i++)//遍历小棍
    {
        if(st[i]||cur+w[i]>length) //如果当前小棍已经遍历过 或者 加上这个小棍大棍长度超了
            continue;//跳过
        st[i]=true;//标记小棍
        if(dfs(u,cur+w[i],i+1))//递归 
            return true;
        st[i]=false;//恢复现场
        if(!cur||cur+w[i]==length)//如果当前大棍长度为0 或者加上小棍刚好为length
            return false;//剪枝优化2,如果一个大棍的开头或者结尾失败,这层就失败
        int j=i;
        while(j<n&&w[j]==w[i])//剪枝优化3,如果当前小棍不行,和它长度相同的都不行
            j ++ ;
        i=j-1;
    }
    return false;
}

int main()
{
    while(cin>>n,n)//读入,n==0时结束
    {
        memset(st,0,sizeof st);//每次重置状态数组
        sum=0;//记录小棍总和
        for(int i=0;i<n;i++)
        {
            cin>>w[i];//读入小棍长度
            sum+=w[i];//计算总和
        }
        sort(w,w+n,greater<int>());//从大到小排序,搜索顺序的优化
        length=1;//初始假设每根大棍长度应该为1,我觉得可以初始为最长的小棍长度.
        while(true)//剪枝优化1,大棍长度一定是总长度的约数
        {
            if(sum%length==0 && dfs(0,0,0))//能整除而且可以让全部大棍长度为length
            {
                cout<<length<<endl;//就是最优解,因为length从小到大枚举
                break;
            }
            length++;//如果不行长度+1
        }
    }
    return 0;
}


4、DFS优化——迭代加深优化

一般是部分情况DFS会搜到很深的地方,但是答案在另一个很浅的分支里,
而迭代加深的思想,就是我每次规定一个最深搜索层数depth,如果每找到答案,就把depth++
如果有一个10层的树,
考虑一个一般情况,
如果答案在第3层(如下面图左上角的树),我们要是用迭代加深优化只用搜到第3层就结束了,如果是一般DFS我们会将第一个分支搜完,把第二个分支搜完,一直搜到第三个分支才能搜到答案
乍一看没差多少,但是由于DFS中每层的点按照指数级增长,每一个分支你搜到最深处会很费时间,只要我们“远远的看见是个死胡同”,就不用进去了,迭代就是让我们“远远的看”。
再看一个最坏情况,我们假设是一个满二叉树(除了最后一层每个点都有两个儿子)
如果有一个10层的树,答案就在在第10层,对于第10层我们要搜2^10,前九层加起来一共2^10-1,所以即使再最坏情况下,也不会低于正常DFS,而且这还是只有两个儿子,当儿子数更多时,迭代的效果会非常明显。



例题:170. 加成序列 - AcWing题库

代码模板:


#include<iostream>
#include<cstring>

using namespace std;

const int N=110;

int n,p[N];//p记录路径

bool dfs(int u,int depth)//u为当前深度,depth为限制的最深深度
{
    if(u==depth)//当当前深度已经到达限制
        return p[u-1]==n;//判断当前最后一个数是否等于n
    bool st[N]={0};//将st所有初始化为0,用来记录用过的数字
    for(int i=u-1;i>=0;i--)//二重遍历,遍历u前所有可能的两个数的和
    {
        for(int j=i;j>=0;j--)
        {
            int tem=p[i]+p[j];
            if(tem<=p[u-1] || st[tem] || tem>n)//如果和小于上一位 或 大于n 或 已经用过了
                continue;//跳过
            st[tem]=1;//标记
            p[u]=tem;//记下路径,不用恢复现场的原因是后面会覆盖当前值
            if(dfs(u+1,depth))//如果后面成功了
                return true;
        }
    }
    return false;
}

int main()
{
    p[0]=1;//第一个点一定是1
    while(cin>>n,n)
    {
        int depth=1;//深度为1,尽管每次会重复搜索,但把每层加起来的和也小于答案层的点的个数,当每个点分支越多时,就可以忽略掉
        while(!dfs(1,depth))//当当前深度没有答案时,深度+1
            depth++;    
        for(int i=0;i<depth;i++)//输出答案
            printf("%d ",p[i]);
        printf("\n");
    }
    return 0;
}








5、DFS优化——双向DFS

原理和双向BFS相类似,双向搜索可以减少中间指数级增长的搜索量。



例题:171. 送礼物 - AcWing题库

代码模板:


#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

typedef long long LL;

const int N=1<<24;
int n,m,k;//n是物品个数,m是最大重量,k是前半暴搜数量
int g[50],weights[N];//g是每个物品重量,weight是每种可能的和
int res,cnt;//res是答案,cnt是计数

void dfs(int u,int s)//u是当前遍历到的下标,s是当前的和,先暴搜前一半,打表
{
    if(u==k)//当下标遍历完时
    {
        weights[cnt++]=s;//记录一下和
        return ;
    }
    if((LL)s+g[u]<=m)//当 当前的和 与 当前下标对应的值 的和 小于最大重量
        dfs(u+1,s+g[u]);//选择这个物品,继续递归
    dfs(u+1,s);//不选这个物品
}

void dfs2(int u,int s)//u是当前遍历到的下标,s是当前的和,暴搜后一半
{
    if(u==n)//当搜索到最后一个位置了,二分查找
    {
        int l=0,r=cnt-1;
        while(l<r)
        {
            int mid=l+r+1>>1;
            if(weights[mid]+(LL)s<=m)//当当前中间值+当前的和<=最大重量时
                l=mid;
            else 
                r=mid-1;
        }
        if(weights[l]+(LL)s<=m)//当找出来的大于等于差值的最小值符合条件
            res=max(res,weights[l]+s);//更新最优解
        return;
    }
    if((LL)s+g[u]<=m) //如果当前的和+当前下标对应的物品的重量没有超过限制
        dfs2(u+1,s+g[u]);//选这个物品
    dfs2(u+1,s);//不选这个物品
}

int main()
{
    cin>>m>>n;
    for(int i=0;i<n;i++)//读入物品重量 
        cin>>g[i];
    sort(g,g+n,greater<int>());//从大到小排序,优化搜索顺序
    k=n/2;// 不+2是为了防止 n = 1时,出现死循环
    dfs(0,0);//对前半数据打表
    sort(weights,weights+cnt);//把表从小到大排序方便二分
    int t=1;//手动去重
    for(int i=1;i<cnt;i++)
        if(weights[i]!=weights[i-1])
            weights[t++]=weights[i];
    cnt=t;
    dfs2(k,0);//从k开始
    cout<<res<<endl;
    return 0;
}




6、DFS优化——IDA*

一般来说配合迭代加深一起。思想上和A*算法差不多,都是找一个估计函数。

例题:180. 排书 - AcWing题库

代码模板:


#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 15;

int n;
int q[N];
int w[5][N];

int f()//估计函数
{
    int cnt=0;
    for(int i=1;i<n-1;i++)
        if(q[i]!=q[i-1]+1)//如果当前位的数字不是上一位数字+1
            cnt++;//不符合顺序的个数+1
    return (cnt+2)/3;//一次操作最多修正3个顺序,向上取整计算估计距离
}

bool check()//检查是否全部有序
{
    for(int i=0;i<n-1;i++)
        if(q[i+1]!=q[i]+1)
            return false;
    return true;
}

bool dfs(int depth,int max_depth)//depth是当前深度,max_depth是最大深度的限制
{
    if(depth+f()>max_depth)//如果当前深度加上预估函数值大于最大深度,跳出
        return false;
    if(check())//如果当前满足题目要求,跳出
        return true;
    for(int len=1;len<=n;len++)//遍历长度
    {
        for(int l=0;l+len-1<n;l++)//遍历起点位置
        {
            int r=l+len-1;//r为端的末尾
            for(int k=r+1;k<n;k++)//从r+1开始遍历所有点
            {
                memcpy(w[depth],q,sizeof q);//w暂存每层的状态,方便恢复现场
                int x,y;//
                for(x=r+1,y=l;x<=k;x++,y++)//x从r+1走到k,先把r+1~k之间的字母移动到前面位置
                    q[y]=w[depth][x];
                for(x=l;x<=r;x++,y++)//再把l~r即要插入的字段放到上面字段的后面
                    q[y]=w[depth][x];
                if(dfs(depth+1,max_depth))//如果递归得到了答案,就结束
                    return true;
                memcpy(q,w[depth],sizeof q);//恢复现场,把之前的状态再恢复回来
            }
        }
    }
    return false;//递归全部还是没找到答案,失败
}

int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        cin>>n;
        for(int i=0;i<n;i++)
            cin>>q[i];
        int depth=0;
        while(depth<5 && !dfs(0,depth))//迭代加深
            depth++;
        if(depth>=5)
            puts("5 or more");
        else
            cout<<depth<<endl;
    }
    return 0;
}















全部评论

相关推荐

05-12 17:28
已编辑
门头沟学院 硬件开发
ldf李鑫:不说公司名祝你以后天天遇到这样的公司
点赞 评论 收藏
分享
秋盈丶:后续:我在宿舍群里和大学同学分享了这事儿,我好兄弟气不过把他挂到某脉上了,10w+阅读量几百条评论,直接干成精品贴子,爽
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务