前言
算法的过渡点,也就是从这里开始真正的代码强度开始展现,DFS和BFS必须掌握,才能有进一步发展的空间
- DFS,深度遍历,也就是一口气走到头不撞南墙不回头,同时它很讲究递归,也就是兜兜转转终是你的小tip,疯狂安利。它更像是栈,递归搜索树。
- BFS,广度遍历,是很稳重的少年,可以理解为层层遍历,用队列进行存放,可以理解为头出,将下一步的情况再排进队伍里,很对的点与边,攻城略地,走地图最短路径,走迷宫之类。
下面进入模板图解
排列数字
按字典序输出所有排列方案,每个方案占一行。
#include<bits/stdc++.h>
using namespace std;
const int N=8;
int n,q[N];
bool str[N];
void dfs(int v){
if(v==n){
for(int i=0;i<n;i++) printf("%d ",q[i]);
puts("");
return ;
}
for(int i=1;i<=n;i++){
if(!str[i]){
str[i]=1;
q[v]=i;
dfs(v+1);
str[i]=0;
}
}
}
int main(){
scanf("%d ",&n);
dfs(0);
return 0;
}
在这里详细推解一下模板
void dfs(int u){
if (u == n) // 一个排列填充完成{
for (int i = 0; i < n; i ++) printf("%d ",path[i]); // 注意格式 别忘了每两个数字间用空格隔开
puts(""); // 相当于输出一个回车
return;
}
for (int i = 1; i <= n; i ++){
if (!state[i])
{
path[u] = i; // 把 i 填入数字排列的位置上
state[i] = true; // 表示该数字用过了 不能再用
dfs(u + 1); // 这个位置的数填好 递归到右面一个位置
state[i] = false; // 恢复现场 该数字后续可用
}
}
// for 循环全部结束了 dfs(u)才全部完成 回溯
return; // 可写可不写
}
皇后问题
最常见的皇后问题,有八皇后也有好几个皇后,这是一个后宫佳丽三千的题
第一种解法,暴力解法
先讲原始方法,好理解一点,DFS按每个元素枚举 时间复杂度O(2的n2次幂)因为每个位置都有两种情况,总共有 n2 个位置先看代码主要是dfs 。
#include <iostream>
using namespace std;
const int N = 20;//对角线元素 2n-1 取20防止越界
int n;
char g[N][N]; //存储图
bool row[N],col[N], dg[N], udg[N]; //udg 副对角线 /
//英语单词 column 列 diagonal 主角线 \
//row 行
void dfs (int x,int y,int s) { //xy为坐标 (x,y) s为 n皇后放置个数
if (y == n) { //当x走到行末尾的时候
y = 0; //转到下一行的第一个
x++;
}
if (x == n) { //走到最后一行 且n皇后都放好的时候
if (s == n) { // 如果找到方案的话
for (int i = 0; i < n; i ++) {
puts(g[i]);//puts输出二维数组 输出每一行如何就会自动换行
}//puts遍历字符串这个语法不懂看下
puts("");
}
return; //返回调用函数进行执行
}
dfs(x, y + 1, s);//不放皇后 并且访问右节点
// 判断皇后能否放在这格
if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) {
g[x][y] = 'Q';//放皇后 然后把
row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
dfs(x , y + 1, s + 1);//放置皇后,找下一层的
//回溯的时候 记得恢复现场 ↓
row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
g[x][y] = '.';
}
}
第二种解法
dg[x + y] = udg[x - y + n]的理解
dg[x + y] = udg[x - y + n] 有的兄弟可能不理解其中x+y到底是什么意思?
以反对角线举例,x-y其实就是该点在对角线上的截距 由中学知识可知对角线方程为y = x + b,其中b表示截距也就是b = x - y(数组下标里面的东西),如果在不同行,但再同一对角线,经过方程计算得到的截距都是一样的,不懂就拿纸自己写一下,+n是为了防止负数产生, 因为数组下标是不可能为负数的,因为每个数都+n,他们映射到结果是一样的,不信你就换个比n大的数试试。
这个用的是y=ax+b的函数解决的,N=20的原因是,最多可由8皇后,所以结合对于y轴的截距,(8+1)*2就行,如果设计到更大的境界,直接0x3f
#include<bits/stdc++.h>
using namespace std;
const int N=20;
bool col[N],dg[N],udg[N];
int q[N][N];
bool
void dfs(int v){
if(v==n){
for(int i=0;i<n;i++){
for(int j=0;j<n;ij++){
printf("c",q[i][j]);
}
cout<<endl;
}
cout<<endl;
return ;
}
for(int i=0;i<n;i++){
if(!col[i]&&)
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++)printf("%c",".");
dfs(1);
return 0;
}
BFS走迷宫
比如算法书上经常见的老鼠
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
由于任意两个相邻的点的距离权值都为1, 所以d[][]存储的是其他点到源点的最短距离,也即用BFS求出的是最少移动次数
bfs 的模板解析,也就是队列,设置为空,然后依次进入,比较容易和题目中设定的数据情况相配合进行一些数据操作和修改,比较全面,重点解析一下模板,之后使用的这样的框架
int bfs() {
queue<PII> q; //定义一个坐标队列
q.push({0,0}); //源点进队
memset(d, -1, sizeof d); //初始化各个点到源点的距离为-1
d[0][0] = 0; //源点到自己的距离为0
int dx[4] = {0,1,0,-1}, dy[4] = {1,0,-1,0}; //向四个方向扩展的坐标数组(个人按照[上右下左]的顺序)
while(!q.empty()) {
auto t = q.front(); //取队头元素
q.pop(); //队头元素出队
for(int i = 0; i < 4; i++) { //分别向四个方向扩展
int x = t.first + dx[i], y = t.second + dy[i]; //扩展后的坐标
//首先(x,y)不能越界, 然后g[x][y] == 0说明可以走(g[x][y] == 1说明是障碍物)
//最后是只更新未被访问的点到源点的距离 (要求d[x][y] == -1)
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}); //(x,y)进队
}
}
}
return d[n-1][m-1]; //返回右下角元素到源点的距离
}
题目的解析代码
#include<bits/stdc++.h>
using namespace std;
const int N=110;
pair<int,int> q[N*N];
int n,m;
int g[N][N];
int distance[N][N];
int bfs(){
int head=0,tail=0;
q[0]={0,0};
memset(distance,-1,sizeof(distance));
d[0][0]=0;
d[x]={-1,0,1,0};
d[y]={0,1,0,-1};
while(head<=tail){
pair<int,int> road=q[head++];
for(int i=0;i<4;i++){
int x=road.first+dx[i],y=road.second+dy[i];
if(x>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&distance[x][y]==-1){
distance[x][y]=distance[road.first][road.second]+1;
q[++tail]={x,y};
}
}
}
return distance[n-1][m-1];
}
八数码
邻接链表
对于任意图 G = (V, E) 的某个节点 u
Adj[u] 表示节点 u 的邻接节点组成的一个链表
图的属性
对于任意图 G = (V, E) 的某个节点 u
u.d 表示从源节点 s 到节点 u 的距离
最短路径
对于任意图 G = (V, E) 的某两个节点 u, v
u 到 v 的最短路径为 u 到 v 的路径中边数最短的路径
最短路径距离为
δ(u,v)=min边数u→⋯→v
δ(u,v)=min边数u→⋯→v
若 u 到 v 没有路径, 则 δ(u,v)=∞
add函数是邻接表的数据基础
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
算法以源节点 s 为中心, 以广度优先的策略向四周探索节点, 直到探索完所有节点为止
广度优先搜索算法的详细思路
算法将节点分为三类
未探索的节点, 用白色表示
已探索的节点, 用黑色表示
边界节点(其实就是加入到队列里的节点), 用灰色表示
算法的核心代码依次重复执行下列操作
- 从队列中取出节点 u
- 扫描 Adj[u]
- 对于扫描到的白色节点 v, v 设为灰色, v.d = u.d + 1 后加入队列
- 扫描到灰色或黑色节点就跳过, 不进行任何操作
- u 设为黑色
BFS(G, s)伪代码
初始化
s.color = 灰色
s.d = 无穷大
s.π = NIL
for u in G.V - {s}
u.color = 白色
u.d = 无穷大
u.π = NIL // 一般用不到, 构建广度优先搜索树的时候会用到, 需要构建从 s 到某一结点的最短路径时也会用到
Q = ф
s 入队
while Q != ф
u = Q 出队元素
for v in G.Adj[u]
if v.color == 白色
v.color = 灰色
v.d = u.d + 1
v.π = u
v 入队
u.color = 黑色
八数码的核心解题代码,BFS详解
vector<pair<int,int>> des = {{0,-1},{1,0},{0,1},{-1,0}};
int a[4] = {-1, 3, 1, -3};
// 节点的 v.d 和 v.π 属性
// 最好用 unordered_map 实现, 底层原理是哈希表, 查询操作的时间复杂度是 O(1)
unordered_map<string,int> d;
unordered_map<string,string> pi;
int BFS(string st)
{
queue<string> q; // 队列 Q
string ed = "12345678x"; // 设置搜索的终止节点 ed
// 源节点 st 初始化
d[st] = 0; // 设置 st.d = 0
pi[st] = ""; // 设置 st.π = NIL
q.push(st); // st 入队
// 只要队列不空, 就执行 while 循环
while(!q.empty())
{
// 出队操作
string s = q.front();
q.pop();
// 判断是否到达终止节点
if(s == ed)
return d[ed]; // 输出 ed.d
// 临时保存一下 s 的属性
// 用 distance 保存 s.d
// 用 parent 保存 s
int distance = d[s];
string parent(s);
// 用 index 存放 'x' 在 s 中的下标
// 用 x 和 y 存在 'x' 在 3*3 矩阵中的坐标
int index = s.find('x');
int x = index / 3, y = index - 3 * x;
// 遍历 Adj[s], 看看是否有未探索的节点
for(int i = 0; i < 4; i++)
{
// 计算邻接节点中 'x' 的坐标
int nx = x + des[i].first;
int ny = y + des[i].second;
if(nx >= 0 && nx <=2 && ny >= 0 && ny <= 2 ) // 判断坐标是否有效
{
swap(s[index],s[index+a[i]]); // 生成邻接节点, 此时的 s 是原来 s 的邻接节点
if(!d.count(s)) // 判断邻接节点是否探索过
{ // 如果未探索过
d[s] = distance + 1; // 设置 v.d = u.d + 1
pi[s] = parent; // 设置 v.π = u
q.push(s); // v 入队
}
swap(s[index],s[index+a[i]]) ; // 恢复 s 为原来的 s
}
}
}
return -1; // 如果探索完所有节点都没有找到 ed 节点, 就返回 -1
}
题解全码
#include<iostream>
#include<unordered_map>
#include<queue>
using namespace std;
queue<string> q;//队列记录待分析的结点
unordered_map<string, int> d;//无需映射记录到达各个结点的最短步数
int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0};
const string ed = "12345678x";
int bfs(string state) {
q.push(state);
d[state] = 0;
while (q.size() > 0) {
auto t = q.front();
q.pop();
int idx = t.find('x');
int a = idx / 3, b = idx % 3;
int distance = d[t];
if (t == ed)
return d[t];
for (int i = 0; i < 4; i++) {
int x = a + dx[i], y = b + dy[i];//进行移动时需要二维化
swap(t[x * 3 + y], t[idx]);
if (x >= 0 && x < 3 && y >= 0 && y < 3 && !d[t]) {
d[t] = distance + 1;
q.push(t);
}
swap(t[x * 3 + y], t[idx]);
}
}
return -1;
}
int main() {
string state;
char c;
for (int i = 0; i < 9; i++) {//二维->一维
cin >> c;
state += c;
}
cout << bfs(state);
}
树与图的深度优先遍历 树的重心
这道题涉及到DFS和之前学到的find 搜索树,
加入一个图解
using namespace std;
const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目
bool st[N]; //记录节点是否被访问过,访问过则标记为true
//a所对应的单链表中插入b a作为根
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// dfs 框架
/*
void dfs(int u){
st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]) {
dfs(j);
}
}
}
*/
//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
st[u] = true; //标记访问过u节点
int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点
//访问u的每个子节点
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
//因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
if (!st[j]) {
int s = dfs(j); // u节点的单棵子树节点数 如图中的size值
res = max(res, s); // 记录最大联通子图的节点数
sum += s; //以j为根的树 的节点数
}
}
//n-sum 如图中的n-size值,不包括根节点4;
res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
return sum;
}
int main() {
memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
cin >> n; //表示树的结点数
// 题目接下来会输入,n-1行数据,
// 树中是不存在环的,对于有n个节点的树,必定是n-1条边
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
add(a, b), add(b, a); //无向图
}
dfs(1); //可以任意选定一个节点开始 u<=n
cout << ans << endl;
return 0;
}
树与图的深度优先遍历
有向图才会涉及到重边和闭环问题,也就是最短路径问题中需要注意的踩雷点,计算好出度和入度
本题最大的难点就在于如何进行层次遍历,我们来看这样一个例子,有这样一幅图,我已经将其转换成了邻接表进行存储。(不知道如何转换成邻接表存储的同学,建议先划到下方,4. 可能有帮助的前置习题,在给出的链接中复习一下如何用邻接表存储图)
虽然放在了bfs算法里,但是他本质是dijkstra算法,用queue的形式表现出来有一种BFS的逻辑,也可以用根优化等等,或者理解成topsort走到头也可以。
有向图的拓扑
划重点,只有有向图也有拓扑,也正是有了方向,才解决了闭环
解题思路
-
首先记录各个点的入度
-
然后将入度为 0 的点放入队列
-
将队列里的点依次出队列,然后找出所有出队列这个点发出的边,删除边,同事边的另一侧的点的入度 -1。
-
如果所有点都进过队列,则可以拓扑排序,输出所有顶点。否则输出-1,代表不可以进行拓扑排序。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx;//邻接矩阵存储图
int h[N];
int q[N], hh = 0, tt = -1;//队列保存入度为0的点,也就是能够输出的点,
int n, m;//保存图的点数和边数
int d[N];保存各个点的入度
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort(){
for(int i = 1; i <= n; i++){//遍历一遍顶点的入度。
if(d[i] == 0)//如果入度为 0, 则可以入队列
q[++tt] = i;
}
while(tt >= hh){//循环处理队列中点的
int a = q[hh++];
for(int i = h[a]; i != -1; i = ne[i]){//循环删除 a 发出的边
int b = e[i];//a 有一条边指向b
d[b]--;//删除边后,b的入度减1
if(d[b] == 0)//如果b的入度减为 0,则 b 可以输出,入队列
q[++tt] = b;
}
}
if(tt == n - 1){//如果队列中的点的个数与图中点的个数相同,则可以进行拓扑排序
for(int i = 0; i < n; i++){//队列中保存了所有入度为0的点,依次输出
cout << q[i] << " ";
}
}
else//如果队列中的点的个数与图中点的个数不相同,则可以进行拓扑排序
cout << -1;//输出-1,代表错误
}
int main(){
cin >> n >> m;//保存点的个数和边的个数
memset(h, -1, sizeof h);//初始化邻接矩阵
while (m -- ){//依次读入边
int a, b;
cin >> a >> b;
d[b]++;//顶点b的入度+1
add(a, b);//添加到邻接矩阵
}
topsort();//进行拓扑排序
return 0;
}
这个就是很直观的拓扑排序
福利板子
数据结构与算法比赛常用的套题板子,做了汇总,主要是DFS BFS 各种花式树与图
自己上传的资源,如果有打比赛需要的可以下载一下
https://download.csdn.net/download/qq_43621422/85174186