搜索:BFS、DFS、剪枝

搜索问题,首先需要表示出题目的状态空间,即初始状态、可达状态和终结状态,并确定它们之间的转移条件。之后需要确定合适的搜索方式,这个搜索方式需要保证可以到达所有的可达情况而且没有死循环。最后,估计复杂度酌情剪枝或改变搜索方式。

关键在于:表示出完整的状态空间,根据状态空间大小估计时间复杂度,选择合适的解题模型


 

BFS是树的按层遍历的过程。题目要找到最少的操作数,可以考虑用BFS来搜索。因为BFS按层搜索的特性,在队列中发现第一个符合条件的位置,一定是最优解之一。BFS在图上以邻接矩阵的方式遍历,复杂度O(n2),邻接表为O(n+e)

较难的BFS问题,状态转移会有其他的附加条件。这种情况下,原来简单的BFS找队列中出现的第一个元素是不适用的。考虑新添一些状态,以此来覆盖完整的状态空间。例洛谷P3659,构造出完整的状态空间后遍历即可。

有一类问题需要来考虑分层图。关于分层图的具体思路和做法,见博客【常用技巧:分层图】。将题目提供的多种选择构造为新的边,这样状态空间就变得完整同时简单。在新图上遍历即可。例洛谷P1073

还有一类问题,需要结合状态压缩。基本模型是去往指定地点,但是图上有几种锁,需要找到特定的钥匙才能打开锁继续前进。这种问题,首先构造一个函数,可以求出将存放钥匙的单元格的钥匙状态。原本的图再添加一个钥匙的状态,得到完整的状态空间。当遇到锁时,看当前的钥匙状态是否可以和锁匹配,以这样的方式进行BFS即可解决问题。例洛谷P4011

#include<bits/stdc++.h>
using namespace std;
int n,m,p,k,v,maze[15][15][15][15],go[4][2]={0,1,0,-1,1,0,-1,0},cnt[15][15],key[15][15][15];
bool mark[15][15][2000];
struct node
{
    int x,y,k,d;
};
queue<node> q;

int getkey(int x,int y)
{
    int ans=0;
    for(int i=1;i<=cnt[x][y];i++){
        ans|=(1<<(key[x][y][i]-1));
    }
    return ans;
}
int BFS()
{
    int startx=1,starty=1,startk;
    startk=getkey(startx,starty);
    q.push((node){startx,starty,startk,0});
    mark[startx][starty][startk]=true;
    while(!q.empty()){
        node now=q.front();
        q.pop();
        if(now.x==n&&now.y==m){
            return now.d;
        }
        for(int i=0;i<4;i++){
            int x=now.x+go[i][0];
            int y=now.y+go[i][1];
            int pass=maze[now.x][now.y][x][y];
            if(x<=0||x>n||y<=0||y>m||pass<0||(pass&&(!(now.k&(1<<(pass-1)))))) continue;
            int nextk=now.k|getkey(x,y);
            if(mark[x][y][nextk]) continue;
            q.push((node){x,y,nextk,now.d+1});
            mark[x][y][nextk]=true;
        }
    }
    return -1;
}
int main()
{
    scanf("%d%d%d",&n,&m,&p);
    scanf("%d",&k);
    for(int i=1;i<=k;i++){
        int a,b,c,d,e;
        scanf("%d%d%d%d%d",&a,&b,&c,&d,&e);
        if(e==0) maze[a][b][c][d]=maze[c][d][a][b]=-1;
        else maze[a][b][c][d]=maze[c][d][a][b]=e;
    }
    scanf("%d",&v);
    for(int i=1;i<=v;i++){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        cnt[a][b]++;
        key[a][b][cnt[a][b]]=c;
    }
    printf("%d\n",BFS());
    return 0;
}

DFS与树的先根遍历类似,是一个递归的过程。DFS相对于BFS代码量更小而且无需维护其他数据结构,所以可选择的情况下优先选择DFS。对于“能否到达”“是否存在”这样的简单搜索问题,DFS会更好些。

DFS常与枚举相结合。枚举可能得不到想要的结果,这时需要回溯,与DFS递归的过程完美契合。对于数独问题,枚举+DFS,或者是舞蹈链(听说的)。洛谷P1784

#include<bits/stdc++.h>
using namespace std;
int maze[15][15],x[100],y[100],num;
bool flag;

bool judge(int k,int n)
{
    for(int i=1;i<=9;i++){
        if(maze[x[n]][i]==k||maze[i][y[n]]==k) return false;
    }
    int a=(x[n]-1)/3*3;
    int b=(y[n]-1)/3*3;
    for(int i=1;i<=3;i++){
        for(int j=1;j<=3;j++){
            if(maze[i+a][j+b]==k) return false;
        }
    }
    return true;
}
void dfs(int n)
{
    if(n==num){
        for(int i=1;i<=9;i++){
            for(int j=1;j<=9;j++){
                if(j!=1) printf(" ");
                printf("%d",maze[i][j]);
            }
            printf("\n");
        }
        flag=true;
        return;
    }
    else{
        for(int i=1;i<=9;i++){
            if(judge(i,n)&&!flag){
                maze[x[n]][y[n]]=i;
                dfs(n+1);
                maze[x[n]][y[n]]=0;
            }
        }
    }
    return;
}
int main()
{
    for(int i=1;i<=9;i++){
        for(int j=1;j<=9;j++){
            scanf("%d",&maze[i][j]);
            if(maze[i][j]==0){
                x[num]=i;
                y[num]=j;
                num++;
            }
        }
    }
    dfs(0);
    return 0;
}

难度更大一点,DFS用于实现记忆化搜索。记忆化搜索是一种动态优化思想,可以避免重复的搜索和访问。


 剪枝:通过预先判断,放弃搜索不可能是解的点,来节省时间

(1)可行性剪枝,目前的状态不满足题目的某些要求,并且其子节点也一定不满足要求。dfs下走一条没有重复节点的路,如果当前点左右走过上下没走过或是上下走过左右没走过,可以直接剪掉,洛谷P1585。

(2)最优性剪枝,目前搜索到的节点的后续最优情况也不比当前的最优解好,就停止对当前节点的搜索,回溯到父节点搜索其他情况。常见的比如求最优解,如果发现当前情况下其他点就算取到最优值也不如目前的最优解,就可以直接剪掉了,洛谷P1559。

(3)交换搜索顺序,严格来说是对搜索方式的优化。改变搜索顺序可能会减少搜索的总状态数,有选择的挑选分支小的子树优先搜索。比如数独问题,优先选择空缺少的行上的点进行搜索,洛谷P1074。

对于DFS的题目,如果提交之后MLE,大概率是dfs爆栈,需要对深搜过程进行剪枝;TLE也是类似。

 

posted @ 2020-10-07 18:40  太山多桢  阅读(271)  评论(0)    收藏  举报