算法基础课—搜索与图论(一)DFS、BFS

深度优先搜索 DFS

深度优先搜索DFS
一直往深的搜,直到碰到叶节点就回溯到上一节点进行搜索
在这里插入图片描述

核心——回溯和剪枝

DFS ——递归实现——回溯和剪枝

回溯

回溯——找到多个解
因为要回溯所以在算法中有很重要的一步——记录现场、恢复现场
回溯——递归实现

剪枝

一般用在对于有限制条件和寻找最优情况的判断
剪枝——边走边判断是否符合条件或者是否是最优解,如果不符合,可以直接不考虑这条分支

算法思想

1、循环子节点
2、结束条件判断
比方说如果u==n,则输出。。。
3、条件判断——子节点情况
(1)基础条件——没有访问过
(2)外部条件——题目所给的限制条件——剪枝
4、记录状态
(1)表示当前位置的
我现在是遍历到哪一个,下一个要遍历哪一个——一般作为形参来传递,遍历下一个一般+1
比方说最经常用的 u,u表示序列中第几个,遍历下一个u就加一
也有对于坐标的记录,记录x,y,表示当前点的位置,如果遍历下一个x,y更新
(2)表示是否访问过的
表示visit的数组,visit[i] 即第i个节点是否访问过
(3)恢复现场
5、 恢复现场——恢复子节点的状态——在递归回溯之后,一般在递归语句后面
(1)改成没有访问过——visit数组改成false
(2)在递归之前对其进行的操作的恢复

整体结构

整体结构
void dfs(....表示节点位置....)
for(。。多个子节点。。){
	if(是否满足结束条件){
	。。。。。
	return;
	}
	if(条件判断){
		visit[i] = true;
		......执行相应操作,比方说状态更改或者是访问子节点更改子节点状态等等
		dfs(....位置更新,更新到下一位置....)
		visit[i] = false;
		.....恢复状态.....
	}
}

算法模板——来自于排列数的题目

void dfs(int u){
    int i;
    for(i = 1; i <= n; i ++){
        if(u == n){
            for(i = 0; i < n; i ++ ){//这里要从0开始因为u一开始从0开始存的path
                cout<<path[i]<<" ";
            }
            cout<<endl;
            return;
        }
        if(!st[i]){//如果没有用过
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            st[i] = false;
        }
    }
}

排列数字

题目

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式
共一行,包含一个整数 n。

输出格式
按字典序输出所有排列方案,每个方案占一行。

数据范围
1≤n≤7
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

算法思想

算法思想:
dfs要知道下一个遍历到哪里,也要知道当前遍历的情况
于是用path存储当前遍历的情况,用u来表示遍历到哪一个位置,st来表示是否用过区分每一个节点
循环for(i=1;i<=n;i++)其实是对各种情况的分发,先在一个情况下进行遍历,如果说遍历到n时,则输出返回,回溯的时候要恢复父节点的状态
然后再继续到另一种情况下
深度优先
记录状态,遍历下一状态,遍历终止,回溯,恢复状态,继续遍历
遍历下一状态要明白怎么遍历到下一状态,如这里,就是用u来表示,我到第几个位置了。
记录状态,记录当前状态的情况,这里是st[]和path,同时要便于之后恢复状态,st,path

代码

#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int n;
bool st[N];//用于判断是否用过
int path[N]; //记录数组
void dfs(int u){
    int i;
    for(i = 1; i <= n; i ++){
        if(u == n){
            for(i = 0; i < n; i ++ ){//这里要从0开始因为u一开始从0开始存的path
                cout<<path[i]<<" ";
            }
            cout<<endl;
            return;
        }
        if(!st[i]){//如果没有用过
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            st[i] = false;
        }
    }
}
int main(){
    int i;
    cin>>n;
    dfs(0);
}

n皇后问题

题目

n− 皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

在这里插入图片描述
现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式
共一行,包含整数 n。

输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围
1≤n≤9
输入样例:
4
输出样例:
.Q…
…Q
Q…
…Q.

…Q.
Q…
…Q
.Q…

方法一

1、如何表示当前位置
由于一行只可能有一个皇后,则可以将皇后位置转换为类似“1324”序列进行思考,其中数字则为每行皇后在的位置,则只需u来记录现在到第几行。
2、如何进行状态更新 u + 1 即到下一行,即到序列的下一个位置
3、条件判断,设置col[]数组,如果该行有,则col[i] =true;
正对角线 dg[]数组,横纵坐标相加的值,类似于y = -x + b,这个b值,如果对应dg[b] = true 则该对角线上存在数
反对角线 udg[]数组 y= x + b,b = y - x ,由于b小于0 ,所以加上一个n 所以dg[n + y - x] = true ,则。。。
x,y坐标移动——上下左右,设置dx,dy数组
x 即为u ,y即为循环里的子节点序号
4、当前位置什么时候到达下一状态
循环子节点的情况,如果情况满足条件,进入下一状态

代码
#include <iostream>

using namespace std;

const int N = 20;

int n;
char g[N][N];
bool col[N], dg[N], udg[N];

void dfs(int u)
{
    if (u == n)
    {
        for (int i = 0; i < n; i ++ ) puts(g[i]);
        puts("");
        return;
    }

    for (int i = 0; i < n; i ++ )
        if (!col[i] && !dg[u + i] && !udg[n - u + i])
        {
            g[u][i] = 'Q';
            col[i] = dg[u + i] = udg[n - u + i] = true;
            dfs(u + 1);
            col[i] = dg[u + i] = udg[n - u + i] = false;
            g[u][i] = '.';
        }
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';

    dfs(0);

    return 0;
}

方法二

1、当前位置的表示——x,y,s s表示有几个皇后
2、更新——x,y+1
3、结束标志s==n
4、条件判断与方法一类似

代码
#include <iostream>

using namespace std;

const int N = 10;

int n;
bool row[N], col[N], dg[N * 2], udg[N * 2];
char g[N][N];

void dfs(int x, int y, int s)
{
    if (s > n) return;
    if (y == n) y = 0, x ++ ;

    if (x == n)
    {
        if (s == n)
        {
            for (int i = 0; i < n; i ++ ) puts(g[i]);
            puts("");
        }
        return;
    }

    g[x][y] = '.';
    dfs(x, y + 1, s);

    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n])
    {
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
        g[x][y] = 'Q';
        dfs(x, y + 1, s + 1);
        g[x][y] = '.';
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
    }
}

int main()
{
    cin >> n;

    dfs(0, 0, 0);

    return 0;
}

宽度优先搜索 BFS

第一层搜完搜第二层,一层一层搜索

在这里插入图片描述

最短性

BFS 搜到的点具有最短性,也就是他搜到的第一点一定是最短路径的点,而DFS不具有这个性质。所以凡是权值为1的最短路径的都是用BFS来做,而比较没那么明显思路的用DFS来做

空间复杂度的对比

  ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210524202053320.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1l0dHR0dHR0dHR0dHR0dHQ=,size_16,color_FFFFFF,t_70)

适用情况

BFS ——距离、层数的搜索——适用于许多最短的情况
BFS与最短路,只有权重都为1,才可以用BFS求最短路
如果是深度搜索,能保证搜索到终点,但是不一定能保证搜到的是最短的

层可以是
(1)树、图的层
(2)表示相同状态
(3)到某点的距离

核心

1、状态的表示
可以是位置,可以是数字,可以是字符串,用来表示当前节点内容
2、层数——可以是具体的层、距离、某种情况
3、记录层数——对应状态下的对应层数
可以用二维数组、一位数组、map等记录

BFS模板

将初始情况压入队列
while(队列非空){
取出队头元素
对取出的队头元素进行条件判断(没有访问过+题目给的条件)
满足条件的进行扩展
将扩展的元素压入堆栈
}

代码模板——走迷宫的宽度有限搜索

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        for (int i = 0; i < 4; i ++ )
        {
            int x = t.first + dx[i], y = t.second + dy[i];

            if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
            {
                d[x][y] = d[t.first][t.second] + 1;
                q.push({x, y});
            }
        }
    }

走迷宫

题目

给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。

输入格式
第一行包含两个整数 n 和 m。

接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。

输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围
1≤n,m≤100
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8

算法思想

算法思想:
1、表示状态
用x,y的位置表示状态——pair<int ,int>
2、更新状态——如何更新状态,进入到下一个
3、层——这里是距离,到起点的距离
4、记录层数——d[][]数组,对应xy下的d
5、条件判断——在对子节点扩展的时候,要判断是否访问过+题目所给的条件
6、结束状态判断——题目所给的
如果说没有从这个结束状态判断中出来,说明可能不存在题目说的可能性
宽度有限搜索——队列,先把第一层的进队列,再把第二层的进队列,第二层就是与上一层距离为1的位置,如果该位置没有被访问且符合范围且不存在障碍物,则插入队列最为第二层
然后将队头出栈,再依次进行扩展。
题目类似于要我们求一条最短路径,权值为1的最短路径问题就可以用BFS,相当于每次进行距离为1的往下搜索,看最快如何能找到终点

代码

#include <iostream>
using namespace std;
typedef pair<int,int> PII;
const int N = 1000;
int a[N][N],d[N][N];//用来记录所在层数
int hh = 0, tt = 0;
PII q[N*N];//注意得是n*n才够大
int main(){
    int n, m;
    int i,j;
    cin>>n>>m;
    for(i = 1; i <= n; i ++ ){
        for(j = 1; j <= m; j ++){
            cin>>a[i][j];
        }
    }
    q[0] = {1,1};
    d[1][1] = 1;
    int dx[] = {-1, 1, 0, 0},dy[] = {0, 0, -1, 1};
    while(hh <= tt){
        PII t = q[hh ++ ];
        for(i = 0; i < 4; i ++){
            int x = t.first + dx[i];
            int y = t.second +dy[i];
            if(x >= 1 && x <= n && y >= 1 && y <= m && !a[x][y] && !d[x][y]){//注意是要没有被访问过,没有被访问过用d[][] = 0 表示,d[][]这个数组用来表示所在层数
                q[++ tt ] = {x, y};
                d[x][y] = d[t.first][t.second] + 1;
            }
        }
    }
    int result = d[n][m] - 1;
    cout<<result<<endl;
}

八数码问题

题目

在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。

例如:

1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。

我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。

交换过程如下:

1 2 3 1 2 3 1 2 3 1 2 3
x 4 6 4 x 6 4 5 6 4 5 6
7 5 8 7 5 8 7 x 8 7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

输入格式
输入占一行,将 3×3 的初始网格描绘出来。

例如,如果初始网格如下所示:

1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8

输出格式
输出占一行,包含一个整数,表示最少交换次数。

如果不存在解决方案,则输出 −1。

输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19

算法思想

1、状态的表示
string ——字符串
2、层数——x移动的距离
3、记录层数——unordered_map<string, int> d;
4、结束标志判断
end = “12345678x”
if(now_str == end)

算法思想:
和走迷宫不同的是,走迷宫的标志使用点来表示的,如果到达终点就结束了,但是这道题的思路应该用字符串来表示,如果字符串与结束字符串相同,则说明到达终点。
同时用unordered_map 来记录d即距离
思路仍然还是
一层一层的搜索,如果之前没有出现过,则对其d进行+1,同时将字符串push到队列中
由于求得是最短路,所以用宽度优先搜索比较合适

宽度优先搜索
1、队列,记录当前状态
2、距离d——记录当前状态的所在层数
3、结束标志
将初始状态push到队列中
while 队列非空
取出队头元素
扩展状态
判断是否出现过,是否符合条件,符合则push进队列
如果到达结束的地方,则跳出循环。

代码


#include <iostream>
#include <unordered_map>
#include <queue>
#include <string>
using namespace std;
int n = 3;
typedef pair<string,int> PSI;
queue<string> q;
unordered_map<string, int> d;
int main(){
    int t = 0, i;
    int result = -1;
    string q0;
    char s[2];
    for(i = 0; i < 9; i ++){
        cin>>s;
        q0 += s;
    }
    d[q0] = 0;//注意unordered_map的赋值方式,有点类似于python的字典
    q.push(q0);
    int dx[] = {-1, 1, 0, 0},dy[] = {0, 0, -1, 1};
    string end = "12345678x";
    while(q.size()){
        auto now_str = q.front();
        q.pop();
        int now_distance = d[now_str];
        int k = now_str.find('x');
        if(now_str == end) {
            result = d[now_str];
            break;
        }
        for(i = 0; i < 4; i ++){
            int x = k / 3 + dx[i];
            int y = k % 3 + dy[i];
            if(x >= 0 && x < n && y >= 0 && y < n){
                swap(now_str[k], now_str[x * 3 + y]);
                if(!d.count(now_str)){
                    d[now_str] = now_distance + 1;
                    //q[++ tt] = {now_f,}
                    q.push(now_str);
                }
                swap(now_str[k], now_str[x * 3 + y]);//恢复现场,因为还要进行下一次循环。
            }
        }
    }
    cout<<result<<endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值