目录
深度优先搜索dfs:
实现数据结构:stack栈
特点:不撞南墙不回头
空间 :O(h)(h表示树的高度)
不具备最短路
例题:排列数字
问题:
给定一个整数n,将数字1~n排成一排,将会有很多种排列方法,请你按照字典序将所有的排列方法输出
输入样例:3
输出样例:
0 1 2
0 2 1
1 0 2
1 2 0
2 0 1
2 1 0
模拟过程:
注意模拟不撞南墙不回头这个思想:
我们从第0层开始向下搜索:即dfs(0),一开始的st[N]数组都是为0的(全局变量不初始化,默认为0),那么我们的第0层就按照for循环的顺序填上path[0]=0,并将0标记为1,表示已经用过了,后面不能再使用了,接下来,我们搜索下一层:即dfs(u+1),此时要注意并不是马上就执行st[i]=0这个操作,而是重新开始执行函数,此时参数为u+1,同样的,还是进入for循环,此时因为已经标记了0,所以0不满足条件,下一个则是1,所以在第一层填上1,至此path已经填好了path[0]=0 path[1]=1 ,接下来重复操作则有path[2]=2
path[2]=2的下一步时dfs(u+1),注意此时u==n,可以执行第一个if语句了,将我们的path数组n大小之前的数输出【0 1 2】并且return回溯
回溯:返回到调用这次递归前的那一层,也就是我们填入path[2]=2的那一层,即第二层:也就是说那一层我们已经执行完了,dfs(u+1)这个操作,按照顺序我们要执行st[i]=0,此时的i很明显是2,相当于我们用了2,在搜索下一层时,我们先标记2不能用,在它搜索回来时,我们将2取消标记
再次for循环:按照for循环的顺序,由于2已经用了,虽然它回来时解除了标记,但是我们已经用不到了,因为i++,导致现在i的值是3,然后我们将3填入path,即此时为path[2]=3,意味着我们的第二层又增加了一个组合:【0,1,3】,相当于碰到底后返回,并且向另外一个分支走去,这就是dfs!
#include<iostream>
using namespace std;
const int N=10;
int n;
int path[N];//存放答案
bool st[N];//标记该元素是否被使用过
void dfs(int u)
{
if(u==n)//当遍历到最后一层时,输出该层
{
for(int i=0;i<n;i++) printf("%d ",path[i]);
printf("\n");
return;//回溯
}
for(int i=0;i<n;i++)
{
if(!st[i])//如果该元素没有被使用过,那么就使用该元素
{
path[u]=i;//存储
st[i]=1;//true标记为已经用过了
dfs(u+1);//向下一层进行搜索
st[i]=0;//回复现场
}
}
}
int main()
{
cin>>n;
dfs(0);
}
例题:n-皇后问题
问题:
n-皇后问题是指将 n 个皇后放在 n∗n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数n,请你输出所有的满足条件的棋子摆法。
输入样例:4
输出样例:
.Q..
...Q
Q...
..Q.
..Q.
Q...
...Q
.Q..
思路解析:
因为每行每列每一斜线都只能放置一个皇后,那么我么就需要三个数组来判断这个位置是否能放置数组:即列数组,正对角线数组,反对角线数组
问题:为什么不是用四个数组来判断?为什么没有行数组?
答:像上题一样,我们是一层层的判断,每一层的一种情况,我们只放置一个数,那么就确保了:每一行只会有一个元素,所以不需要再开一个行数组进行判断
模拟过程:
整体与上面的排列数字一样,多加了一个mp[u][i]='.'的操作,也就是说我回溯回来的时候,证明了这个点的放置是与其他点有冲突的,所以需要重新将已排好的点再次赋值成原始的模样,有一种天然的对称关系^ ^
#include<iostream>
using namespace std;
const int N = 20;
int n;
char mp[N][N];
bool col[N], dg[N], udg[N];
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i++) puts(mp[i]);
puts("");
return;
}
for (int i = 0; i < n; i++) {
if (!col[i] && !dg[u + i] && !udg[n - u + i])
{
mp[u][i] = 'Q';
col[i] = dg[u + i] = udg[n - u + i] = 1;
/*u+i n-u+i可以分别看作x+y=b,y-x=b的截距
一条对角线对应一个截距,可以将这个截距映射到数组下标中
*/
dfs(u + 1);
col[i] = dg[u + i] = udg[n - u + i] = 0;
mp[u][i] = '.';
}
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
mp[i][j] = '.';
}
}
dfs(0);
return 0;
}
广度优先搜索bfs:
实现数据结构:queue队列
特点:一层一层地走完
空间 :O(2^h)(h表示树的高度)因为每一层最坏的情况是满二叉树
具备最短路
模板:
初始状态入队
while queue不空:
t<----队头
扩展队头
例题:走迷宫
问题:
给定一个n*m的二维整数数组,用来表示一个迷宫,数组中只包含0或1,其中0表示可以走的路,1表示不可通过的墙壁。
最初,有一个人位于左上角(1, 1)处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角(n, m)处,至少需要移动多少次。
数据保证(1, 1)处和(n, m)处的数字为0,且一定至少存在一条通路
模拟过程:
首先需要两个数组:一个是地图数组(用来初始化) g[N][N]:又称邻接矩阵
一个是距离数组(用来记录这个点到起点地距离) d[N][N]
此外我们需要用队列来记录方位:此时套用模板
(实际上任何数据结构都可)但习惯队列
迭代条件:当队列不为空时(即队尾大于队头)
迭代过程:一开始是将起点位置放入队列中,然后遍历起点的四个方位,并用x,y分别记录可能到达的方位,如果满足条件:(在迷宫内且该位置未走过(d数组的值为-1))那么就将该位置入队,并在距离数组中将其标记+1,意味着从上一个点到这个点的距离为1,也就是距离起点的位置为1,此后再以这个点的四个方位进行扩展
返回:距离数组中的终点坐标
#include<iostream>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N];//地图
int d[N][N];//距离
PII q[N * N];//模拟队列
int bfs()
{
int hh = 0, tt = 0;//hh指向队头元素,tt指向队尾元素
q[0] = { 0,0 };
memset(d, -1, sizeof(d));//没有经历到这个点时,初始化均为-1
d[0][0] = 0;//开始元素距离起点地距离为0
int dx[4] = { -1,0,1,0 },
int dy[4] = { 0,1,0,-1 };//移动方位
while (hh <= tt)
{
auto t = q[hh++];
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]==-1代表该点没有更新过距离
也就是没有走过
*/
d[x][y] = d[t.first][t.second] + 1;
q[++tt] = { x,y };
}
}
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
升级版:路径追踪
如果想要知道从起点到终点的最短路径经过的坐标,那我们还需要一个数组path来记录经过的坐标
代码实现:
#include<iostream>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N];//地图
int d[N][N];//距离
PII q[N * N];//模拟队列
PII Path[N][N];
int bfs()
{
int hh = 0, tt = 0;//hh指向队头元素,tt指向队尾元素
q[0] = { 0,0 };
memset(d, -1, sizeof(d));
d[0][0] = 0;
int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
while (hh <= tt) {
auto t = q[hh++];
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]==-1代表该点没有更新过距离
也就是没有走过
*/
d[x][y] = d[t.first][t.second] + 1;
//下面是重点!!!
Path[x][y] = t;
q[++tt] = { x,y };
}
}
}
//求路径的过程
int x = n - 1, y = m - 1;
while (x || y)
{//只要x,y不同时为0,就继续向前转移
cout << x << " " << y << endl;
auto t = Path[x][y];
x = t.first, y = t.second;
}
return d[n - 1][m - 1];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
实质:
我们需要用Path数组来记录上一个被放入队列中的元素,那么我们需要对它开为pair,因为要存储坐标,但是如果只是存储坐标的话,会有大量没有用的坐标也存入Path数组里,所以我们需要从后往前找,即我是由哪个推出来的,那么我再变成“哪个推出来的”从而推出“哪个推出来的”
树和图的遍历:
树和图的存储:
有向图:
(1)邻接矩阵~~~~~二维数组
(2)邻接表~~~~~~单链表
邻接矩适合稠密图:稠密图:边的数量是点的数量的2~3倍
邻接表适合稀疏图:稀疏图:边的数量小于等于点的数量
邻接表:
#include<iostream>
using namespace std;
const int N=100010,M=N*2;
int h[N],e[M],ne[M],idx;
//h为每一个结点,e为该条边的权值,ne代表该条边指向的结点的下一条边
/*插入一条由a指向b的边*/
void add(int a,int b)
{
//头插法
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
树和图的遍历:
可以想象成树的节点存储(下一个节点的地址和节点本身所存储的值)
或者单链表的的存储结构
#include<iostream>
using namespace std;
const int N=100010,M=N*2;
int h[N],e[M],ne[M],idx;
bool st[N];
/*
进入dfs函数后,先标记u点已经走过了,然后遍历u的邻接点
*/
void dfs(int u)
{
st[u]=1;
for(int i=h[u];i!=-1;i=ne[i])//头节点初始化为-1
{
int j=e[i];
if(!st[j]) dfs(j);
}
}
例题:树的重心
给定一颗树,树中包含n个结点(编号1~n)和n-1条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
#include <iostream>
#include <algorithm>
#include <cstring>
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++;//头++
}
//返回以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]) //如果没有访问过
{//dfs:向下寻找节点数
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;
}
例题:图中点的层次
给定一个n个点m条边的有向图,图中可能存在重边和自环。
所有边的长度都是1,点的编号为1~n。
请你求出1号点到n号点的最短距离,如果从1号点无法走到n号点,输出-1
模拟过程:类似于bfs,用新的元素入队,去寻找更新的元素,只不过这里的方位存储在链表中,需要另一种遍历方式而已
#include<iostream>
using namespace std;
const int N = 1e5 + 5, M = N * 2;
int n, m;
int h[N], e[M], ne[M], idx;
int dis[N];
void add(int a, int b)//单链表头插法
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int bfs()
{
memset(dis, -1, sizeof(dis));//初始化距离为-1
int q[N], hh = 0, tt = 0;//队头,队尾
q[hh] = 1;//队头元素标记为1
dis[1] = 0;//1本身到自身的距离为0
while (hh <= tt) //当队列不为空
{
int u = q[hh++];//取下队头
for (int i = h[u]; i != -1; i = ne[i])//图的遍历
{
int v = e[i];//取下节点数
if (dis[v] == -1) //节点数为-1,证明未走过
{
dis[v] = dis[u] + 1;//从u->v的距离为1:因为边长为1
q[++tt] = v;//将新的元素入队->更新新的节点
}
}
}
return dis[n];//返回距离的终点即可
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof(h));
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
}
cout << bfs();
}
拓扑序列
有向无环图一定存在拓扑序列,也被称为拓扑图
若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列。
一个有向无环图,一定至少存在一个入度为0的点
简单来说:就是a->b->c就是一个拓扑序列
而:a->b->c->a就不是一个拓扑序列
例题:有向图的拓扑序列
给定一个n个点m条边的有向图,点的编号是1到n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出-1。
若一个由图中所有点构成的序列A满足:对于图中的每条边(x, y),x在A中都出现在y之前,则称A是该图的一个拓扑序列
代码实现:(待修改)
#include<iostream>
using namespace std;
const int N = 1e5 + 5;
int h[N], e[N], ne[N], idx;
int q[N], hh, tt = -1, n, m;
int deg[N];
void add(int a, int b)//头插
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
bool bfs()
{
for (int i = 1; i <= n; i++)
{
if (!deg[i])
{
q[++tt] = i;//将所有的点入队
}
}
while (hh <= tt)//当队列不为空
{
int t = q[hh++];//取出队头元素
for (int i = h[t]; i != -1; i = ne[i])//图的遍历
{
int j = e[i];//记录节点数
deg[j]--;//
if (deg[j] == 0) q[++tt] = j;
}
}
return tt == n - 1;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof(h));
while (m--)
{
int x, y;
cin >> x >> y;
deg[y]++;
add(x, y);
}
if (bfs())
{
for (int i = 0; i < n; i++)
{
cout << q[i] << " ";
}
}
else
{
cout << -1;
}
return 0;
}