搜索与图论
文章目录
- 搜索与图论
- 深度优先搜索 DFS
- [843. n-皇后问题 - AcWing题库](https://www.acwing.com/problem/content/845/)
- 宽度优先搜索 BFS
- [844. 走迷宫 - AcWing题库](https://www.acwing.com/problem/content/description/846/)
- 树与图的存储
- [846. 树的重心 - AcWing题库](https://www.acwing.com/problem/content/description/848/)
- 树与图的深度优先遍历
- 树与图的宽度优先遍历
- [847. 图中点的层次 - AcWing题库](https://www.acwing.com/problem/content/849/)
- 拓扑排序
- 如何求拓扑序
- 度:入度和出度
- [848. 有向图的拓扑序列 - AcWing题库](https://www.acwing.com/problem/content/850/)
- 题目概述
- 解题思路
- 方法步骤
- 代码解析
- 代码解释
- 复杂度分析
- 示例输入输出
- 最短路
- 建图!!!
- 朴素 Dijkstra
- [849. Dijkstra求最短路 I - AcWing题库](https://www.acwing.com/problem/content/851/)
- 堆优化Dijkstra
- Bellman_Ford
- [853. 有边数限制的最短路 - AcWing题库](https://www.acwing.com/problem/content/855/)
- 题目分析
- 算法思路
- 关键步骤:
- 代码解释
- 复杂度分析
- 注意事项
- SPFA
- [851. spfa求最短路 - AcWing题库](https://www.acwing.com/problem/content/853/)
- [852. spfa判断负环 - AcWing题库](https://www.acwing.com/problem/content/854/)
- Floyd
- [854. Floyd求最短路 - AcWing题库](https://www.acwing.com/problem/content/856/)
搜索与图论
数据结构 | 空间 | |||||
---|---|---|---|---|---|---|
DFS | 回溯、剪枝 | stack | O(h) | 不具有最短性 | 最优性剪枝,不合法剪枝 | |
BFS | queue | O(2^h) | 最短路 |
深度优先搜索 DFS
DFS是一个执着的人会一直尽可能往回走,走到头再回溯,回溯的过程能往前邹尽可能往前走—yxc
顺序,搜索流程是一个树
843. n-皇后问题 - AcWing题库
搜索顺序:
-
全排列思路:枚举每一行皇后可以放在哪个位置,注意剪枝(提前判断,当前方案不合法直接回溯)O(n*n!)
#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; }
-
每一个位置放和不放O(2n2)
#include <iostream>using namespace std; const int N = 20; int n; char g[N][N]; bool col[N], dg[N], udg[N], row[N];void dfs(int x, int y, int s) {if(y == n) y = 0, x ++;if(x == n){if(s == n){for(int i = 0; i < n; i ++) puts(g[i]);puts("");}return;}//不放皇后dfs(x, y + 1, s);//放if(!row[x] && !col[y] && !dg[y + x] && !udg[n + y - x]){g[x][y] = 'Q';row[x] = col[y] = dg[y + x] = udg[n + y - x] = true;dfs(x, y + 1, s + 1);row[x] = col[y] = dg[y + x] = udg[n + y - x] = false;g[x][y] = '.';}}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; }
宽度优先搜索 BFS
稳重的人,每一次扩展一层
dp可以理解为特殊的最短路,即没有环的最短路
框架:
844. 走迷宫 - AcWing题库
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int g[N][N], d[N][N];
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};
int n, m;int bfs()
{queue<PII> q;q.push({0, 0});memset(d, -1, sizeof d);d[0][0] = 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];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});}}}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;
}
prv存储从哪个点过来的
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int g[N][N], d[N][N];
PII prv[N][N];
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};
int n, m;int bfs()
{queue<PII> q;q.push({0, 0});memset(d, -1, sizeof d);d[0][0] = 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];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;prv[x][y] = t;q.push({x, y});}}}int x = n - 1, y = m - 1;while(x || y){cout << x << " " << y << endl;auto t = prv[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;
}
树与图的存储
树是无环连通图
-
有向图
-
无向图(特殊有向图)
- 邻接矩阵 g[a][b]
- 邻接表
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;void add(int a, int b)
{e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}int main()
{memset(h, -1, sizeof h);
}
846. 树的重心 - AcWing题库
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
//dfs(u)返回以u为根的树的点的数量
//sum 记录当前u为根的树的大小
// res存删除当前点之后,每个连通块大小的最大值
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;
bool st[M];
int n;
int ans = N;//全局答案,最大的最小值void add(int a, int b)
{e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
//返回以u为根的树的点的数量
int dfs(int u)
{st[u] = true;//已经被搜int sum = 1, res = 0;//sum 记录当前u为根的树的大小// res存删除当前点之后,每个连通块大小的最大值for(int i = h[u]; i != -1; i = ne[i]){int j = e[i];//j存储当前节点对应图的编号if(!st[j]) {int s = dfs(j);//s表示当前子树的大小res = max(res, s);sum += s;}}res = max(res, n - sum);ans = min(ans, res);return sum;
}int main()
{memset(h, -1, sizeof h);cin >> n;for(int i = 1; i <= n - 1; i ++){int a, b;cin >> a >> b;add(a, b);add(b, a);}dfs(1);//随便挑一个点cout << ans << endl;return 0;
}
树与图的深度优先遍历
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;
bool st[M];void add(int a, int b)
{e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}void dfs(int u)
{st[u] = true;//已经被搜for(int i = h[u]; i != -1; i = ne[i]){int j = e[i];//j存储当前节点对应图的编号if(!st[j]) dfs(j);}}int main()
{memset(h, -1, sizeof h);dfs(1);//随便挑一个点
}
树与图的宽度优先遍历
847. 图中点的层次 - AcWing题库
拓扑排序
有向图才会有拓扑序列,对于每条边,起点在终点前面
有环一定没有拓扑序
可证明有向无环图一定有拓扑图,所以有向无环图也被称为拓扑图
如何求拓扑序
度:入度和出度
所有当前入度为0的点都可以作为起点
入度为零意味着没有任何一条边指向当前点
宽搜过程
848. 有向图的拓扑序列 - AcWing题库
题目概述
给定一个有向图,包含 n n n 个点和 m m m 条边,可能存在重边和自环。要求输出该图的任意一个拓扑序列,如果不存在拓扑序列(即图中存在环),则输出 − 1 -1 −1。
解题思路
拓扑排序是针对有向无环图(DAG)的一种线性排序方法,使得对于图中的每一条有向边 ( u , v ) (u, v) (u,v), u u u 在排序中总是位于 v v v 的前面。如果图中存在环,则无法进行拓扑排序。
方法步骤
- 计算入度:统计每个节点的入度(即有多少条边指向该节点)。
- 初始化队列:将所有入度为0的节点加入队列。这些节点没有前置依赖,可以直接作为拓扑序列的起始点。
- 处理队列:从队列中取出一个节点,将其加入拓扑序列,并移除所有从该节点出发的边(即减少其邻居节点的入度)。如果邻居节点的入度变为0,则将其加入队列。
- 检查结果:如果拓扑序列包含所有节点,则排序成功;否则,说明图中存在环,无法进行拓扑排序。
代码解析
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx; // 邻接表存储图
int q[N], d[N]; // q数组模拟队列,d数组存储入度
int n, m;void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}bool topsort() {int hh = 0, tt = -1;// 将所有入度为0的节点加入队列for(int i = 1; i <= n; i ++) {if(!d[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];d[j] --; // 邻居节点的入度减1if(d[j] == 0) // 如果入度为0,加入队列q[++ tt] = j;}}// 如果队列中的节点数等于n,说明拓扑排序成功return tt == n - 1;
}int main() {memset(h, -1, sizeof h); // 初始化邻接表cin >> n >> m;while(m --) {int a, b;cin >> a >> b;d[b] ++; // 更新节点b的入度add(a, b); // 添加边a->b}if(topsort()) {for(int i = 0; i < n; i ++)cout << q[i] << " \n"[i == n];} else {cout << -1 << endl;}return 0;
}
代码解释
- 邻接表存储图:使用数组模拟邻接表,
h
数组存储每个节点的头指针,e
和ne
数组分别存储边的终点和下一条边的索引。 - 入度数组
d
:记录每个节点的入度。 add
函数:添加一条从a
到b
的边,并更新b
的入度。topsort
函数:- 初始化队列,将所有入度为0的节点加入队列。
- 处理队列中的节点,减少其邻居节点的入度,如果邻居节点入度为0则加入队列。
- 最后检查队列中的节点数是否等于
n
,如果是则说明拓扑排序成功。
- 主函数:读取输入,构建图,调用
topsort
函数并输出结果。
复杂度分析
- 时间复杂度: O ( n + m ) O(n + m) O(n+m),每个节点和每条边各处理一次。
- 空间复杂度: O ( n + m ) O(n + m) O(n+m),存储图结构和队列。
示例输入输出
输入:
3 3
1 2
2 3
1 3
输出:
1 2 3
解释:拓扑序列可以是 1 2 3
或 1 3 2
,代码输出前者。
最短路
建图!!!
考察抽象成图的过程,即建图的能力
朴素 Dijkstra
s表示当前已经确定最短距离的点
-
dist[1] = 0, dist[i] = 0x3f
-
for(int i = 1 ~ n)
-
t <- 不在s中的距离最近的点
-
s <- t
-
用t跟新其他点的距离
-
849. Dijkstra求最短路 I - AcWing题库
堆优化Dijkstra
在一堆数找最小的数,可以用堆 O(1)
在堆中修改一个数,log(n)
Bellman_Ford
如果有负权回路,最短路不一定存在
for n次 (迭代n次)for 所有边a, b, w //松弛操作dist[b] = min(dist[b], dist[a] + w);dist[b] <= dist[a] + w // 三角不等式
bellmanford 可以用来求负环,时间复杂度较高
853. 有边数限制的最短路 - AcWing题库
题目分析
给定一个n个顶点m条边的有向图,图中可能存在重边和自环,边权可能为负数。要求求出从顶点1到顶点n的最多经过k条边的最短路径距离。如果无法到达顶点n,则输出"impossible"。
算法思路
这道题使用Bellman-Ford算法来解决,因为该算法特别适合处理有边数限制的最短路问题。Bellman-Ford算法的基本思想是通过松弛操作逐步逼近最短路径。
关键步骤:
- 初始化距离数组dist,将所有顶点到起点的距离设为无穷大,起点距离设为0。
- 进行k次松弛操作,每次操作遍历所有边,尝试通过该边缩短终点到起点的距离。
- 使用备份数组backup确保每次松弛操作基于上一次迭代的结果,避免"串联更新"。
- 最终检查终点距离,如果仍大于一个较大值(0x3f3f3f3f/2),则认为不可达。
代码解释
#include <bits/stdc++.h>
using namespace std;const int N = 550, M = 10010; // 定义顶点和边的最大数量
int n, m, k; // n顶点数,m边数,k边数限制
int dist[N], backup[N]; // dist存储距离,backup用于备份struct Edge {int a, b, w; // 边的起点、终点和权值
} edges[M];void bellman_ford() {memset(dist, 0x3f, sizeof dist); // 初始化距离为无穷大dist[1] = 0; // 起点距离设为0for(int i = 0; i < k; i++) { // 进行k次松弛memcpy(backup, dist, sizeof dist); // 备份当前距离数组for(int j = 0; j < m; j++) { // 遍历所有边int a = edges[j].a, b = edges[j].b, w = edges[j].w;dist[b] = min(dist[b], backup[a] + w); // 松弛操作}}
}int main() {cin >> n >> m >> k;for(int i = 0; i < m; i++) {int a, b, w;cin >> a >> b >> w;edges[i] = {a, b, w}; // 存储所有边}bellman_ford();// 判断是否可达,注意不是直接比较0x3f3f3f3f因为有负权边if(dist[n] > 0x3f3f3f3f / 2) puts("impossible");else cout << dist[n] << endl;return 0;
}
复杂度分析
- 时间复杂度:O(k*m),其中k是边数限制,m是总边数。
- 空间复杂度:O(n+m),用于存储距离数组和边集。
注意事项
- 必须使用backup数组备份上一次的dist状态,避免在同一轮松弛中多次更新导致的错误。
- 判断不可达的条件是dist[n] > INF/2,而不是dist[n] == INF,因为有负权边可能导致距离略小于INF。
- 该算法可以检测负权环,但本题有边数限制,所以不需要考虑无限松弛的情况。
SPFA
宽搜优化,队列存所有变小的节点,跟新过谁,再拿他跟新(公司业绩提高才可能加工资
851. spfa求最短路 - AcWing题库
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
int h[N], e[N], ne[N], idx, w[N];
int dist[N];
bool st[N];
int n, m;void add(int a, int b, int c)
{e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}void spfa()
{memset(dist, 0x3f, sizeof dist);dist[1] = 0;queue<int> q;q.push(1);st[1] = true;while(q.size()){int t = q.front();q.pop();st[t] = false;for(int i = h[t]; i != -1; i = ne[i]){int j = e[i];if(dist[j] > dist[t] + w[i]){dist[j] = dist[t] + w[i];if(!st[j]){q.push(j);st[j] = true;}}}}
}int main()
{cin >> n >> m;memset(h, -1, sizeof h);while (m --){int a, b, c;cin >> a >> b >> c;add(a, b, c);}spfa();if(dist[n] == 0x3f3f3f3f) puts("impossible");else cout << dist[n] << endl;return 0;
}
852. spfa判断负环 - AcWing题库
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 2e5 + 10;
int h[N], e[N], ne[N], idx, w[N];
int dist[N];
bool st[N];
int cnt[N];
int n, m;void add(int a, int b, int c)
{e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}bool spfa()
{memset(dist, 0x3f, sizeof dist);dist[1] = 0;queue<int> q;for(int i = 1; i <= n; i ++){q.push(i);st[i] = true;}while(q.size()){int t = q.front();q.pop();st[t] = false;for(int i = h[t]; i != -1; i = ne[i]){int j = e[i];if(dist[j] > dist[t] + w[i]){dist[j] = dist[t] + w[i];cnt[j] = cnt[t] + 1;if(cnt[j] >= n) return true;if(!st[j]){q.push(j);st[j] = true;}}}}return false;
}int main()
{cin >> n >> m;memset(h, -1, sizeof h);while (m --){int a, b, c;cin >> a >> b >> c;add(a, b, c);}if(spfa()) puts("Yes");else puts("No");return 0;
}
Floyd
求多源汇最短路
初始化:邻接矩阵存储图 d[i, j]
for(int k = 1; k <= n; k ++)for(int i = 1; i <= n; i ++)for(int j = 1; j <= n; j ++)d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
854. Floyd求最短路 - AcWing题库
#include <bits/stdc++.h>
using namespace std;
const int N = 220;
int d[N][N];
int n, m, k;void floyd()
{for(int k = 1; k <= n; k ++){for(int i = 1; i <= n; i ++){for(int j = 1; j <= n; j ++){d[i][j] = min(d[i][j], d[i][k] + d[k][j]);}}}
}int main()
{cin >> n >> m >> k;memset(d, 0x3f, sizeof d);for(int i = 1; i <= n; i ++){for(int j = 1; j <= n; j ++){if(i == j) d[i][j] = 0;}}for(int i = 1; i <= m; i ++){int a, b, w;cin >> a >> b >> w;d[a][b] = min(d[a][b], w);}floyd();while(k --){int a, b;cin >> a >> b;if(d[a][b] > 0x3f3f3f3f / 2) puts("impossible");else cout << d[a][b] << endl;}return 0;
}