为了能到远|

迪杰小凤凰

园龄:1年5个月粉丝:1关注:1

搜索算法合集 - By DijkstraPhoenix

搜索算法合集

By DijkstraPhoenix

深度优先搜索 (DFS)

引入

如果现在有一个迷宫,如何走路径最短?

方法

走迷宫最简单粗暴的方法式什么呢?当然是把所有路都走一遍啦!

如果是手动计算的话,可能会把你手指累得抽筋,但电脑不会,电脑具有强大的算力,这种暴力的事情当然是交给电脑做啦。

深搜的本质:一条路走到底,走到死胡同再往回走,回到上一个岔口继续走,直到找到正确的路

实际上,任何一条路都可以看做是一个只有一个岔口的分岔路,所以不需要把路和岔口分开计算。

那么刚才的例子应该是这么走。实际上岔口走的顺序是任意的,方法不唯一。

概念:从死胡同返回的步骤叫做回溯

由于深搜不能保证第一次找到的路径为最短路径,所以需要统计所有路线

深搜一般使用递归实现,走过的每个位置都要打上标记,同一条路不能再走一遍

四联通和八连通:

有些走迷宫题目是四联通的(即上下左右),也有些是八连通的(即上、下、左、右、左上、左下、右上、右下),需要仔细观察题目要求

四联通位移数组:dx[]={1,0,-1,0}; dy[]={0,1,0,-1};

八连通位移数组:dx[]={1,0,-1,0,1,1,-1,-1}; dy[]={0,1,0,-1,1,-1,1,-1};

主算法代码:

int maze[MAXN][MAXN];//存储迷宫 0表示当前节点可以走,1表示不能走
bool vis[MAXN][MAXN];//打标记
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};//位移数组,分别对应 上右下左(如果是八向移动的话要改成对应的)
int n,m,stx,sty,edx,edy;//地图长宽以及起点和终点的坐标
int ans=0x7f7f7f7f;//最短距离,要初始化为极大值

void dfs(int x,int y,int z)//x和y是当前位置的坐标,z是走过的步数
{
    if(x==edx&&y==edy)//到了终点
    {
        ans=min(ans,z);//更新答案(如果答案还是极大值,说明无法到达终点)
        return;
    }
    vis[x][y]=true;//打标记
    for(int i=0;i<4;i++)//枚举四个方向
    {
        int nx=x+dx[i],ny=y+dy[i];//下一个应该走到的位置
        if(nx<1||nx>n||ny<1||ny>m)continue;//不能走出地图(这个要写在灵魂拷问的最前面,否则访问数组要越界)
        if(maze[nx][ny]==1)continue;//不能卡墙里
        if(vis[nx][ny])continue;//不能走你走过的路
        dfs(nx,ny,z+1);//走到下一个节点
    }
    vis[x][y]=false;//重点!回溯时要清除标记!
}

例题

连通块问题

求1的连通块数量

这是一类重要的题目!

本题可以使用 DFS 从每一个点开始将每一个连通块都标记并计数

上图就是标记出的连通块

我们可以从连通块的任意一个位置开始,遍历整个连通块,并把这个连通块的所有点打上标记(防止重复计算)

这个方法也叫洪水填充法(Flood Fill)

强烈建议在网上找几篇专门讲连通块的博客学一下

#include<bits/stdc++.h>
using namespace std;
int maze[105][105];
bool vis[105][105];
int n,m,ans;
void dfs(int x,int y)
{
    vis[x][y]=true;
    for(int i=0;i<4;i++)
    {
        int nx=x+dx[i],ny=y+dy[i];
        if(nx<1||nx>n||ny<1||ny>m)continue;
        if(maze[nx][ny]!=1)continue;
        if(vis[nx][ny])continue;
        dfs(nx,ny);
    }
    //注意!!!此处不要回溯(标记是给后面的连通块看的)
}
int main(void)
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            cin>>maze[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            if(!vis[i][j]&&maze[i][j]==1)//如果这个点没找过并且是连通块的一部分,那么就是一个新的连通块
            {
                ans++;
                dfs(i,j);//为整个连通块打上标记
            }
        }
    }
    cout<<ans;
    return 0;
}

八皇后问题

洛谷 P1219

本题的每一步都决定一个皇后的位置,由输出格式就可以看出,我们可以按每一列的顺序计算。一个皇后会独占一行、一列、两斜线,因为是按列计算的,不需要给列打标记,则需要 3 个标记数组。

(其实可以看一下洛谷上的题解)

#include<bits/stdc++.h>
using namespace std;
bool vis[15],vis1[35],vis2[35];
int n;
int nod[15];
int sum=0;
void dfs(int k)
{
    if(k>n)
    {
        sum++;
        if(sum<=3)//前3个要输出方案
        {
            for(int i=1;i<=n;i++)cout<<nod[i]<<" ";
            cout<<endl;
        }
        return;
    }
    for(int i=1;i<=n;i++)
    {
        if(vis[i])continue;
        if(vis1[i+k-1])continue;
        if(vis2[i-k+13])continue;
        vis[i]=true;
        vis1[i+k-1]=true;
        vis2[i-k+13]=true;//可以手动模拟一下行列坐标和斜坐标的关系,加13是防止计算出负数
        nod[k]=i;//保存方案
        dfs(k+1);
        vis[i]=false;
        vis1[i+k-1]=false;
        vis2[i-k+13]=false;
    }
}
int main(void)
{
    cin>>n;
    dfs(1);
    cout<<sum;
    return 0;
}

全排列问题

这是一个重要的题目!

洛谷 P1706

按照题意模拟搜索即可

#include<iostream>
#include<cstdio>
using namespace std;
int n,a[1000],vis[1000];
void dfs(int step)
{
    if(step==n+1)
    {
        for(int i=1;i<=n;i++)
        {
            printf("%5d",a[i]);//题目要求格式化输出
        }
        cout<<endl;
    }
    for(int i=1;i<=n;i++)
    {
        if(vis[i]==1)continue;
        a[step]=i;
        vis[i]=1;
        dfs(step+1);
        vis[i]=0;
    }
}
int main(void)
{
    cin>>n;
    dfs(1);
    return 0;
}

一些建议练习的题

求细胞数量
提示:联通块问题,不要清除标记,从每个未标记且是细胞的块出发,将整个块打上标记

小猫爬山

选数

单词接龙

广度优先搜索 (BFS)

引入

还是刚才的迷宫问题

方法

除了把每一条路走一遍,其实还可以用水把迷宫给淹了。这样可以顺着水找到路径。

BFS 模拟洪水往外扩散的样子,按层遍历。

这样做有一个好处:如果像迷宫这样,每两个相邻的方块之间的距离都一样的话,第一次找到的路径就是最短路径(每一层走过的路程都一样),如果没有这个条件就不保证最短

以下是 BFS 和 DFS 策略上的对比:

放到迷宫里就是这个样子:

(图中深绿色代表路径,浅绿色代表访问过的点,线表示遍历的每一层)

重点!因为同一层的点无法直接像 DFS 那样转移,所以会把应该访问的节点放到一个队列里,挨个处理

主要代码:

struct node
{
    int x,y,step;//节点坐标和已经走过的步数
};
queue<node>Q; //队列
bool vis[MAXN][MAXN];//标记数组
int maze[MAXN][MAXN];//存储地图,0可以走,1不能走
int stx,sty,edx,edy;
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
int n,m;//地图长宽

void bfs(void)//bfs其实不需要封装在函数里,它不需要递归
{
    Q.push(node{stx,sty,0});
    vis[stx][sty]=true;//初始状态
    while(!Q.empty())//直到所有的状态都处理完毕
    {
        int x=Q.front().x,y=Q.front().y,st=Q.front().step;//取出队首元素
        Q.pop();//很重要!不要忘了出队列
        for(int i=0;i<4;i++)
        {
            int nx=x+dx[i],ny=y+dy[i];
            if(nx<1||nx>n||ny<1||ny>m)continue;
            if(vis[nx][ny])continue;
            if(maze[nx][ny]==1)continue;//灵魂三问
            Q.push(node{nx,ny,st+1});//加入处理队列
            vis[nx][ny]=true;//打标记  注意!BFS没有回溯!
            if(nx==edx&&ny==edy)//到终点了
            {
                cout<<st+1;
                exit(0);//封装在cstdlib库里,直接退出整个程序
            }
        }
    }
}

例题

Catch That Cow S

洛谷 P1588

本题的转移有三种:前进1步,后退1步,乘2

其实本题代码很简单(别看有点长)实际上是三个转移

#include<bits/stdc++.h>
using namespace std;
int t,x,y;
bool vis[100005];
queue<int>Q,step;
int main(void)
{
    cin>>t;
    for(int test(1);test<=t;test++)
    {
        cin>>x>>y;
        Q.push(x); //入队
        step.push(0); //初始他一步也没走,step入队0
        vis[x]=true; //标记
        while(!Q.empty()) //step和Q是同步的,不需要额外判断
        {
            //取队首元素
            int s=Q.front();
            int st=step.front();
            Q.pop(); //出队
            step.pop();
            int nst=st+1;
            int ns;
            //找可以的情况
            ns=s+1;//前进1步
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //条件成立
            {
                vis[ns]=true; //标记
                Q.push(ns); //入队
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因为BFS第一个找到的一定是最短的路径,直接输出
                    cout<<nst;
                    break;
                }
            }
            ns=s-1;//后退1步
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //条件成立
            {
                vis[ns]=true; //标记
                Q.push(ns); //入队
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因为BFS第一个找到的一定是最短的路径,直接输出
                    cout<<nst;
                    break;
                }
            }
            ns=s*2;//乘2
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //条件成立
            {
                vis[ns]=true; //标记
                Q.push(ns); //入队
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因为BFS第一个找到的一定是最短的路径,直接输出
                    cout<<nst;
                    break;
                }
            }
        }
    }
}

字串变换

洛谷 P1032

本题需要寻找可以变换的部分进行转移。字符串长度不长,可以暴力配对

#include<bits/stdc++.h>
using namespace std;
#define int long long

string ap[25],bp[25];
int le;
queue<string>q;
queue<int>step;//本代码没有使用结构体而是使用两个队列
map<string,bool>vis;

signed main(void)
{
    string a,b;
    string ia,ib;
    cin>>a>>b;
    while(cin>>ia)
    {
        cin>>ib;
        ap[++le]=ia;
        bp[le]=ib;
    }
    q.push(a);
    step.push(0);
    vis[a]=true;
    while(!q.empty())
    {
        string s=q.front();q.pop();
        int st=step.front();step.pop();
        if(st==10)continue;
        for(int i=1;i<=le;i++)
        {
            int start=0;
            while(true)
            {
                int fd=s.find(ap[i],start);//寻找匹配字符串
                if(fd==string::npos)break;
                start=fd+1;
                string tmp=s.substr(0,fd);
                tmp+=bp[i];
                tmp+=s.substr(fd+ap[i].length());
                if(vis[tmp])continue;
                q.push(tmp);//转移
                step.push(st+1);
                if(tmp==b)//找到了
                {
                    cout<<st+1;
                    return 0;
                }
            }
        }
    }
    cout<<"NO ANSWER!";
    return 0;
} 

一些建议的题

奇怪的电梯

填涂颜色

棋盘

最后的迷宫

迭代加深搜索 (IDDFS 或 IDS)

引入

众所周知所有的搜索算法都是 DFS 或 BFS 的变形,迭代加深搜索是 DFS 的一种变形,它在进行深搜时限定了搜索深度

这看起来是没事找事,对吧?

如果我们遇到了一道搜索题每一层的宽度无限但深度有限,可以用 DFS 解决;如果深度无限宽度有限,可以使用 BFS。那么深度和宽度都是无限的呢?

于是就有了迭代加深搜索

思想

迭代加深搜索限定搜索深度其实就是将无限的深度转化为有限的深度

迭代加深搜索的核心就在于进行多次搜索,每次搜索依次放宽深度限定,这样第一次找到的必定是步数最少的解(这里的步数指搜索的层数而非贡献值总和)

就像这样:

核心代码如下:

int lmt;//深度限制
bool flg;//是否成功
void dfs(int dep/*深度*/,/*一些别的东西*/)
{
    if(dep>lmt)return;//超过限制直接返回
    if(/*找到了*/)
    {
        flg=true;
        return;
    }
    /*干一些事*/
}

int main(void)
{
    /*输入以及初始化*/

    for(lmt=1;lmt<=20/*最大深度,一般在20左右,具体按题意来看*/;lmt++)//循环进行多次搜索
    {
        dfs(1,/*一些别的参数*/);
        if(flg)//成功了
        {
            /*输出答案*/
        }
    }

    /*没找到的情况*/
    return 0;
}

例题

埃及分数

洛谷 P1763

解法

这道题很明显搜索深度和宽度都是无限的(题目中的限制每个分数不小于1/1e7,但由于数量巨大,无法直接计算,近似看做无限),需要迭代加深搜索求解

每次搜索限制分数的个数,本题一定有解,所以可以无限循环减少限制

这种算法无法通过 HACK 数据,满分解法会在后续章节中讲解

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int deep=1;//深度限制
bool flag=false;
int tmp[15],ans[15];
int gcd(int x,int y)
{
    if(y==0)return x;
    return gcd(y,x%y);
}
void dfs(int k,int a,int b)
{
    if(k>deep)//超过最大深度
        return;
    if(a==1&&b>tmp[k-1])//更优解
    {
        tmp[k]=b;
        if(!flag||tmp[k]<ans[k])
        {
            for(int i=1;i<=deep;i++)
                ans[i]=tmp[i];
        }
        flag=true;
        return;
    }
    int l=max(b/a,tmp[k-1]+1);
    int r=(deep-k+1)*b/a;
    if(flag&&r>=ans[deep])
        r=ans[deep]-1;
    for(int i=l;i<r;i++)
    {
        tmp[k]=i;
        int gc=gcd(a*i-b,b*i);
        dfs(k+1,(a*i-b)/gc,b*i/gc);
    }
}
signed main(void)
{
    int a,b;
    cin>>a>>b;
    int gc=gcd(a,b);
    a/=gc;
    b/=gc;//化简(这一步好像不需要,最好加上)
    tmp[0]=1;
    for(deep=1;;deep++)
    {
        dfs(1,a,b);
        if(flag)//找到答案
        {
            for(int i=1;i<=deep;i++)
            {
                cout<<ans[i]<<" ";
            }
            break;
        }
    }
    return 0;
}

一些建议的题

洛谷 P1189 SEARCH

UVA11214 守卫键盘

启发式搜索 (A*)

引入

对于一个有很多条可行的路径的迷宫,如何能够最快速地找到最短路径?

虽然对于搜索,他的本质是暴力,也就是把所有的路径走一遍,但是,这不代表搜索不能变得智能。

那么能不能让搜索算法每次寻找最优的路径呢?

接下来介绍启发式搜索,这是一种相对智能的搜索算法。

原理

启发式搜索 (A*) 是一种的 BFS 的改良,它拥有一个估价函数 f(x)=g(x)+h(x),其中 g(x) 是已走的步数,h(x) 是预计还有多少步到达终点(在某些题目中,估价函数不一定只真实的最短路),f(x) 为估计从起点到终点的步数。

这样,A* 每次都找最有可能成为最优解的一步,用优先队列维护。

那么此时维护的就是全局最优解,所以启发式搜索第一次找到的一定是最优解,第 k 次找到的一定是第 k 优解。

以下是 A* 和 BFS 在没有障碍的地图中寻路的对比(深绿是最终路径,浅绿是访问过的节点)

A*

BFS

可以发现 A* 比 BFS 访问的这些无用节点少了很多。

例题

八数码

洛谷 P1379

这道题使用朴素的 BFS 明显过不了,可以考虑 A*。(本题还有双向搜索的解法,会在后面的章节内讲解)

这里我们的 h(x) 定义为当前状态和目标状态中,不同的数码个数,那么首次到达即为最优解。

本题可以使用康托展开将矩阵转化为一个整数(相当于哈希),这里的代码不考虑。代码使用结构体 + set 存储。

#include <bits/stdc++.h>
using namespace std;
const int dx[] = {1, -1, 0, 0};
const int dy[] = {0, 0, 1, -1};
struct ary //存储状态
{
    int s[5][5];
    bool operator<(ary x) const
    {
        for (int i = 1; i <= 3; i++)
        {
            for (int j = 1; j <= 3; j++)
            {
                if (s[i][j] != x.s[i][j])
                    return s[i][j] < x.s[i][j];
            }
        }
        return false;
    }
} inp, ed;

int h(ary x)//h()函数
{
    int sum = 0;
    for (int i = 1; i <= 3; i++)
    {
        for (int j = 1; j <= 3; j++)
        {
            if(x.s[i][j]!=ed.s[i][j])sum++;
        }
    }
    return sum;
}

struct node //这个结构体是用于自定义优先队列的判断的
{
    ary ar;
    int t;
    bool operator<(node x) const
    {
        return h(ar) + t > h(x.ar) + x.t;
    }
};
string sed = "123804765";
priority_queue<node> heap;
set<ary> vis;
int main(void)
{
    for (int i = 1; i <= 3; i++)
    {
        for (int j = 1; j <= 3; j++)
        {
            ed.s[i][j] = sed[(i - 1) * 3 + j - 1] - '0';
            char s;
            cin >> s;
            inp.s[i][j] = s - '0';
        }
    }
    heap.push((node){inp, 0});
    while (!heap.empty())//A*算法主体(其实就是优先队列优化BFS)
    {
        node fa = heap.top();
        heap.pop();
        if (!h(fa.ar))//首次找到即为最优解
        {
            cout << fa.t;
            return 0;
        }
        int sx, sy;
        for (int i = 1; i <= 3; i++)
        {
            for (int j = 1; j <= 3; j++)
            {
                if (fa.ar.s[i][j] == 0)
                {
                    sx = i;
                    sy = j;
                }
            }
        }
        for (int i = 0; i < 4; i++)
        {
            int nx = sx + dx[i];
            int ny = sy + dy[i];
            if (1<=nx&&nx<=3&&1<=ny&&ny<=3)//移动数码
            {
                swap(fa.ar.s[sx][sy], fa.ar.s[nx][ny]);
                if (!vis.count(fa.ar))
                {
                    vis.insert(fa.ar);
                    heap.push((node){fa.ar, fa.t + 1});
                }    
                swap(fa.ar.s[sx][sy], fa.ar.s[nx][ny]);
            }
        }
    }
    return 0;
}

一些建议的题

洛谷 P4467,洛谷 P2483 K短路
提示:可以用最短路算法预处理 h(x),P2483 使用 A* 无法得到满分

--还没写完呢--

本文作者:迪杰小凤凰

本文链接:https://www.cnblogs.com/dijkstraphoenix/p/18449170

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   迪杰小凤凰  阅读(97)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起