DFS与BFS
使用的数据结构 | 空间 | 特点 | |
---|---|---|---|
DFS | 栈 | O(h) | 不具有最短路 |
BFS | 队列 | O( 2 n 2^n 2n) | 最短路 |
DFS: O(h) 会记录路径上的所有点, 与高度成正比, 因此有多深, 就会存多少
842. 排列数字
分析
DFS最重要的是顺序, 该以怎么样的顺序去搜索
这里就是假设有3个空位, 顺序就是从第1位开始填写 _ _ _ , 从前往后1位1位填写
比如现在已经填了 2 _ _ , 只要保证每次填的时候不能和前面一样
最开始3个都是空
要注意恢复现场
code
#include <iostream>
using namespace std;
const int N = 10;
bool st[N];
int n;
int path[N]; // 保存每个位置上存了哪些数
void dfs(int u){ // 表示搜到第几个空位
if (u == n) { // 因为第0层考虑第1个空位, 到第n层的时候, 前面n个空位都考虑完了, 所以直接输出答案
for (int i = 0; i < n; i ++ ) cout << path[i] << ' ';
cout << endl;
return ;
}
for (int i = 1; i <= n; i ++ ){
if (!st[i]){
st[i] = true;
path[u] = i;
dfs(u + 1);
st[i] = false;
}
}
}
int main(){
cin >> n;
dfs(0);
return 0;
}
843. n-皇后问题
分析1(像全排列那样搜)
因为同一行同一列只能放一个皇后
从第1行开始看, 看皇后可以放在哪一列,
再递归到下一层, 看第2行皇后可以放到哪
再枚举第3层皇后可以放在哪
可以在枚举的时候, 判断往当前位置放皇后是否冲突, 如果冲突, 直接回溯, 下面就不用搜了, 这个过程叫剪枝
时间复杂度O(n* n!)
因为每行都有n个分支, 每放一行, 下一层递归就会-1, n, n - 1, n - 2, …
有n行 所以是n * n!
code
#include <iostream>
using namespace std;
const int N = 10;
char g[N][N];
int n;
int col[N], dg[N], udg[N];
void dfs(int u){
if (u == n) {
for (int i = 0; i < n; i ++ ) cout << g[i] << endl;
cout << endl;
return ;
}
for (int i = 0; i < n; i ++ ){
if (!col[i] && !dg[i + u] && !udg[i - u + n]){ // 一定要加
col[i] = dg[i + u] = udg[i - u + n] = 1;
g[u][i] = 'Q';
dfs(u + 1);
g[u][i] = '.';
col[i] = dg[i + u] = udg[i - u + n] = 0;
}
}
}
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;
}
分析2
可以用一种更原始的方式来枚举八皇后问题
现在写一个最原始的暴力方法:对于 n 2 n^2 n2 个格子,枚举这个格子放或者不放皇后,时间复杂度KaTeX parse error: Double superscript at position 7: O(2^ n^̲2)
code
#include <iostream>
using namespace std;
const int N = 10;
char g[N][N];
int n;
int row[N], col[N], dg[N], udg[N];
void dfs(int x, int y, int s){ // x, y表示当前枚举的位置, s表示当前已经放了多少个皇后
if (y == n) y = 0, x ++ ;
if (x == n){
if (s == n){ // 还需要满足已经放了n个皇后, s有可能小于n
// 因为我们有可能1个皇后都没摆, 也有可能只摆了2-3个皇后
for (int i = 0; i < n; i ++ ) puts(g[i]);
puts("");
}
return ;
}
// 不放皇后
dfs(x, y + 1, s);
// 放皇后
if (!row[x] && !col[y] && !dg[x + y] && !udg[y - x + n]){
g[x][y] = 'Q';
row[x] = col[y] = dg[x + y] = udg[y - x + n] = 1;
dfs(x, y + 1, s + 1);
g[x][y] = '.';
row[x] = col[y] = dg[x + y] = udg[y - x + n] = 0;
}
}
int main(){
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
g[i][j] = '.';
dfs(0, 0, 0);
return 0;
}
844. 走迷宫
分析
每个位置上的数表示在bfs中第几层被扩展到
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int g[N][N];
int d[N][N];
PII q[N * N]; // N * N
int n, m;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
void bfs(){
q[0] = {0, 0};
memset(d, -1, sizeof d);
d[0][0] = 0;
int hh = 0, tt = 0; // 因为0当前放了数, 所以++ tt才是下一个要放的位置
while (hh <= tt){
auto t = q[hh ++ ];
int x = t.first, y = t.second;
for (int i = 0; i < 4; i ++ ){
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] == 0 && d[a][b] == -1){
d[a][b] = d[x][y] + 1;
q[++ tt ] = {a, b};
}
}
}
}
int main(){
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
cin >> g[i][j];
bfs();
cout << d[n - 1][m - 1] << endl;
return 0;
}
输出路径
开个PII Prev[N][N]
if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] == 0 && d[a][b] == -1)
...
Prev[a][b] = t
...
}
int x = n - 1, y = m - 1;
while (x || y){
cout << x << ' ' << y << endl;
auto t = Prev[x][y];
x = t.first, y = t.second;
}
code(输出路径)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int g[N][N];
int d[N][N];
PII q[N * N], Prev[N][N]; // N * N
int n, m;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
void bfs(){
q[0] = {0, 0};
memset(d, -1, sizeof d);
d[0][0] = 0;
int hh = 0, tt = 0; // 因为0当前放了数, 所以++ tt才是下一个要放的位置
while (hh <= tt){
auto t = q[hh ++ ];
int x = t.first, y = t.second;
for (int i = 0; i < 4; i ++ ){
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] == 0 && d[a][b] == -1){
d[a][b] = d[x][y] + 1;
Prev[a][b] = t;
q[++ tt ] = {a, b};
}
}
}
int x = n - 1, y = m - 1;
while (x || y){
cout << x << ' ' << y << endl;
auto t = Prev[x][y];
x = t.first, y = t.second;
}
}
int main(){
cin >> n >> m;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
cin >> g[i][j];
bfs();
cout << d[n - 1][m - 1] << endl;
return 0;
}
八数码(留到习题课)
树与图的存储
树是一种特殊的图(无环连通图),所以只讲图的存储
图:有向图,无向图
有向图存储方式:
(1)邻接矩阵(就是一个二维数组),用的很少,因为浪费空间 O(n^2), 比较适合存储稠密图(
边
数
∼
=
点
数
2
边数 \sim= 点数^2
边数∼=点数2)
(2)邻接表,类似于哈希表的拉链法,新节点插入该链的时候采用头插法(
边
数
∼
=
点
数
边数 \sim= 点数
边数∼=点数)
树与图的遍历
yxc: 深度优先遍历和宽度优先遍历,每个点只用遍历1次, 因此需要开个boot st[N]
数组来表示哪些点已经遍历过了
深度优先遍历(图)
code
846. 树的重心
分析
删除1号点后, 其余连通块点数最大值是4
删除2号点的话, 最多6个点连通
删除4号点的话, 连通块最大个数是5
对于这个题, 我们只要能够 求出, 删掉每个点后, 求出其余连通块的的点数的最大值, 然后在所有点里找一个最小的, 就可以了
问题—> 如何快速每个点删除掉后, 连通块点数的最大值
dfs可以算出子树的大小
比如要算4的子树的大小, 递归的处理完3和6, 因为我们是从上面1下来的, 所以不会走回到1, 4往下走的过程中, 就可以统计出3和6的点数了.
3的点数+ 6的点数 + 4一个点, 就可以知道总共的点数了
因此在dfs中可以求出每个子树的数量
假设想看下, 把4删掉最大值怎么算
首先, 看下把4删掉后, 会分成哪些部分, 每个儿子都是一部分, 还有从父节点往上是另外一部分
子节点的size都可以返回回来
惊奇的发现, 上面父节点连通块的点数 =
n
−
s
i
z
e
4
n - size_4
n−size4
数据范围
1
0
5
10^5
105, 因为每个点都只会被遍历1次, 所以树与图的遍历, 不管bfs还是dfs, 时间复杂度都是O(n + m)
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;
int n;
bool st[N];
int ans = 1e8;
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int dfs(int u){ // 计算以u为根的子树中(包含u)所有点的数量
st[u] = true;
int sum = 1, res = 0; // 当前这个点也算1个点, sum计算当前u子树中点的数量, res表示把u删除后, 子树的数量
for (int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if (!st[j]) {
int s = dfs(j); // 子树的数量
res = max(res, s); // 与res取max
sum += s;
}
}
res = max(res, n - sum); // 统计父节点连通快与res的最大值
ans = min(ans, res); // 计算答案
return sum; // 返回以u为根节点子树的数量
}
int main(){
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ ){
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
dfs(1);
cout << ans << endl;
return 0;
}
宽度优先搜索(图)
847. 图中点的层次
分析
计算从1->n的最短路
因为这个图里所有边权都是1, 所以可以用宽搜求最短距离
第1次发现这个点的距离, 就是到这个点的最短路径
code
注意宽搜中用d[N]
距离数组判重
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = N * 2;
int h[N], e[M], ne[M], idx;
int n, m;
int d[N]; // 宽搜中用距离数组判重
int q[N];
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int bfs(){
memset(d, -1, sizeof d);
int hh = 0, tt = 0;
q[0] = 1, d[1] = 0;
while (hh <= tt){
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
if (d[j] == -1){
d[j] = d[t] + 1;
q[++ tt] = j;
}
}
}
return d[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
cout << bfs() << endl;
return 0;
}
01:55:34
开始
拓扑排序
图的宽搜的经典应用—> 求拓扑序列
图的拓扑序列是针对有向图来说的
有向无环图---->拓扑图
度数:
入度: 一个点有几条边进来
出度: 一个点有几条边出去
入度为0的点可以作为起点(因为没有任何点指向我)
queue <--- 所有入度为0的点
while (queue 不空) {
t<--队头
枚举t的所有出边t->j;
删掉t->j边, d[j] --; // 为什么要删掉t->j呢,
if (d[j] == 0) queue <- j
}
如果图中有个环的话, 那么我们找不到任何突破口(突破口, 就是入度为0的点)
命题:一个有向无环图—> 一定至少存在一个入度为0的点
证明: 反证法
假设一个有向无环图, 所有点的入度都不是0, 那么可以随便挑一个点, 由于这个点的入度不是0, 可以找到它的上一个点, 同样上一个点的入度不是0, 可以找到上一个点, 由于每一个点的入度都不是0, 因此可以无穷无尽的往回找下去
假设点数是n, 当我们沿着往回的路径走了n + 1个点的时候, 由抽屉原理, 我们一共找了n + 1个不同的点, 但是总共只有n个点, 所以路径里必然存在两个点相同, 因此必然存在一个环
因此我们可以从这个入度为0的点作为突破口, 把这个点和这个点关联的边全部删掉, 本来是有向无环图, 删掉一个点还是有向无环图, 所以说只要是有向无环图, 不断的去突破, 各个击破, 那么各个击破的序列, 就是拓扑序列了
848. 有向图的拓扑序列
分析
模板
code
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N];
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool topsort(){
int hh = 0, tt = 0; // tt指向当前点需要放的位置
// 因为当前队列没放任何物品, 所以tt ++, 表示往当前位置放, 放了之后,
// tt + 1
for (int i = 1; i <= n; i ++ )
if (d[i] == 0) q[tt ++ ] = i; // 入度为0的点假如到队列
while (hh != tt){
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i]){
int j = e[i];
d[j] --;
if (d[j] == 0) q[tt ++ ] = j;
}
}
if (tt == n) return true;
return false;
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ){
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
d[b] ++;
}
if (topsort()){
for (int i = 0; i < n; i ++ ) cout << q[i] << ' ';
cout << endl;
}else puts("-1");
return 0;
}