DFS 中重要的两个概念:回溯、剪枝
BFS 一般和最短路径有关系,DFS 没有
回溯注意点:记得恢复现场
模板:
void dfs (int k)
{
if (到目的地)
{
输出解;
return;
}
for (int i = 1; i <= 算法种数; i++)
{
if (满足条件)
{
保存结果,保存现场状态;
dfs(k + 1);
恢复保存结果之前的状态; // 回溯一步
}
}
}
全排列的 DFS 过程:

#include
using namespace std;
const int N = 10;
int n;
int path[N]; // 路径
bool vis[N]; // 是否访问过
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i++) cout << path[i] << " ";
puts("");
return;
}
for (int i = 1; i <= n; i++)
{
if (!vis[i])
{
// 标记已访问,并添加到路径
path[u] = i;
vis[i] = true;
// 继续搜索
dfs(u + 1);
// 回溯
vis[i] = false;
}
}
}
int main()
{
cin >> n;
dfs(0); // 从 path[0] 开始
return 0;
}
题目:AcWing 843. n-皇后问题 - AcWing

解法1:按行枚举
#include <iostream>
using namespace std;
const int N = 20;
int n;
char g[N][N]; // 存储路径
// bool 数组用来判断搜索的下一个位置是否可行
// col 列, dg 对角线(左上->右下), udg 反对角线(左下->右上)
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;
}
// 枚举 u 这一行,搜索合法的列
int x = u;
for (int y = 0; y < n; y++)
{
// 剪枝(对于不满足要求的点,不再继续往下搜索)
if (!col[y] && !dg[y - x + n] && !udg[y + x])
{
// 标记已访问,并加入路径
col[y] = dg[y - x + n] = udg[y + x] = true;
g[u][y] = 'Q';
// 向下搜索
dfs(u + 1);
// 回溯
g[u][y] = '.';
col[y] = dg[y - x + n] = udg[y + x] = false;
}
}
}
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:按格子枚举
#include
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];
// 比较原始的 dfs 搜索,每一格都搜,s 表示皇后数量
void dfs(int x, int y, int s)
{
// 换行
if (y == n) y = 0, x++;
if (x == n)
{
// 摆好了 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[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); // 递归下一层
g[x][y] = '.';
row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
}
}
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;
}
只有当所有边的权重都是 1 时,才可以用 BFS 求最短路。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 110;
typedef pair<int, int> PII; // 二元对
// 方向向量
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int g[N][N], d[N][N];
int n, m; // n x m 的矩阵
void bfs()
{
// 距离初始化为 —1
memset(d, -1, sizeof d);
d[0][0] = 0; // 起点初始化为 A
queue<PII> q;
q.push({0, 0});
while (!q.empty())
{
PII p = q.front();
q.pop();
for (int i = 0; i < 4; i++)
{
int x = p.first + dx[i], y = p.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
{
d[x][y] = d[p.first][p.second] + 1;
q.push({x, y});
}
}
}
cout << d[n - 1][m - 1] << endl;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
bfs();
return 0;
}
输出路径:
#include
#include
#include
using namespace std;
const int N = 110;
typedef pair<int, int> PII;
int g[N][N], d[N][N]; // g 地图, d 距离
PII Prev[N][N]; // 记录前驱,用于输出路径
int n, m; // n x m 的矩阵
void bfs()
{
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 方向向量
queue<PII> q;
q.push({0, 0});
memset(d, -1, sizeof d); // 初始化距离为 -1
d[0][0] = 0; // 起点为 0
while (!q.empty())
{
PII p = q.front();
q.pop();
for (int i = 0; i < 4; i++)
{
int x = p.first + dx[i], y = p.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
{
d[x][y] = d[p.first][p.second] + 1; // 更新距离
q.push({x, y}); // 新坐标入队
Prev[x][y] = p; // 更新前驱
}
}
}
// 输出路径
int x = n - 1, y = m - 1;
while (x || y) {
cout << x << " " << y << endl;
// Prev[x][y] 存储的是能到达当前位置的位置
PII p = Prev[x][y];
x = p.first, y = p.second;
}
cout << d[n - 1][m - 1] << endl;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
bfs();
return 0;
}
树是一种特殊的图(无环连通图)。
无向图是特殊的有向图,主要学习有向图。
有向图:(两个点之间有固定的方向)
a ---> b
无向图:(实际上就是每个方向都能走)
a ---> b
b ---> a
有向图的存储:

规定:n - 图的点数,m - 图的边数
图的邻接表存储:适用于稀疏图( m 和 n 是一个量级)
int h[N]; // 链表头
int e[M]; // 节点的值
int ne[M]; // 下一个节点
int idx; // 当前节点的索引
// 插入一条 a 指向 b 的边
// 在 a 对应的单链表中插入一个节点 b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 对于有权图,需要一个 w[] 存储权值
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
图的邻接矩阵存储:适用于稠密图(m 和 n^2 一个量级)
// g[i][j] = k 表示 i 指向 j 边长为 k
int g[N][N];
void add(int a, int b, int c)
{
g[a][b] = min(g[a][b], c); // 处理重边,只需要记录最短的边
}
Bellman Ford 算法中使用的结构体进行存储边:
// a 指向 b 权重为 w 的边
struct Edge {
int a, b, w;
} edges[M];
DFS 模板:
void dfs(int u)
{
st[u] = true; // 标记已访问
int sum = 1, res = 0;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
DFS 有个很好的特点,遍历的同时可以求出每个子树的大小。
#include
#include
#include
using namespace std;
const int N = 100010, M = N * 2;
// 图的邻接表表示
int h[N]; // 链表头
int e[M]; // 节点的值
int ne[M]; // 下一个节点
int idx; // 当前节点的索引
bool st[N]; // 标记访问
int n;
int ans = N;
// 插入一条 a 指向 b 的边
// 在 a 对应的单链表中插入一个节点 b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 以 u 为根的子树点的数量
int dfs(int u)
{
st[u] = true; // 标记已访问
// sum - 当前子树点的数量, res - 删除当前点后连通块的最大值
int sum = 1, res = 0;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) {
int s = dfs(j); // 当前子树的大小
sum += s;
res = max(res, s);
};
}
// 树的重心:删除当前点后,剩余各个连通块中点数的最大值最小
res = max(res, n - sum);
ans = min(ans, res);
return sum;
}
int main()
{
// 初始化链表
memset(h, -1, sizeof h);
cin >> n;
for (int i = 0; i < n; i++)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
dfs(1);
cout << ans << endl;
return 0;
}
BFS 模板:
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (!q.empty())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点 j 已经被遍历过
q.push(j);
}
}
}
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;
int d[N]; // 存储每个节点距离起点的距离 d[1] = 0
int n, m; // n 个点 m 条边
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int bfs()
{
memset(d, -1, sizeof d);
d[1] = 0;
queue<int> q;
q.push(1);
while (!q.empty())
{
int t = q.front();
q.pop();
// 遍历 t 的每个邻边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] == -1)
{
d[j] = d[t] + 1; // 存储 j 节点距离起点的举例,并标记已访问
q.push(j);
}
}
}
return d[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
}
cout << bfs() << endl;
return 0;
}
一个有向无环图,一定至少存在一个入度为 0 的点,一定存在拓扑序列。
可利用反证法很快的得到结论,如果每个点都前驱节点,那么这个图将无穷无尽。
入度与出度:
入度为 0 表示没有任何一个点要求在前面,所有入度为 0 的点都可以排在前面位置。
拓扑排序的思路:
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;
int q[N], hh = 0, tt = -1; // 队列Ω
int d[N]; // 入度
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool topsort()
{
// 将入度为 0 的点入队
for (int i = 1; i <= n; i++)
if (d[i] == 0) q[++tt] = i;
// 不停的将入度为 0 的点指向的边删除,删除后入度为 0 则入队
while (hh <= tt)
{
int t = q[hh++];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
d[j] --; // 删除 t->j 的边
// 如果 j 的入度为 0 则入队
if (d[j] == 0) q[++tt] = j;
}
}
// n 个点的都入队则为拓扑图
return tt == n - 1;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
add(a, b); // 加边 a -> b
d[b]++; // 维护 b 入度
}
if (topsort())
{
for (int i = 0; i < n; i++) cout << q[i] << " ";
puts("");
} else puts("-1");
return 0;
}
图论中的 源点 == 起点,汇点 == 终点
规定:n - 图的点数,m - 图的边数
单源,只有一个起点的最短路。
多源,多个不同起点到其他点的最短路。
稠密图,指边数很多,m 和 n^2 是一个量级。
稀疏图,指边数不多,m 和 n 是一个量级。
Dijkstra 基于贪心,当有负权边时,局部最优不一定全局最优。
题目:849. Dijkstra求最短路 I - AcWing题库
整体思路:进行 n - 1 次迭代去确定每个点到起点的最小值。
时间复杂度分析:
使用邻接矩阵的写法:
#include
#include
using namespace std;
const int N = 510;
int g[N][N]; // 邻接矩阵(稠密图)
int dist[N]; // 到源点的距离
bool st[N]; // 记录是否已经找到最短路
int n, m; // n 个点, m 条边
int dijkstra()
{
// 距离初始化成无穷大
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 1 号点距离为 0
for (int i = 0; i < n; i++)
{
// 在没有确定最短路的点中,距离源点最近的点
int t = -1;
for (int j = 1; j <= n; j++)
// 当前点未确定最短路 && 当前路不是最短的
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true; // 标记已经确定最短路
// 使用 t 更新其他点距离
// 遍历所有 t 可以达到的点 jd
for (int j = 1; j <= n; j++)
// 比较 1--> j 和 1--> t --> j 的距离
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
// 默认边长初始化成无穷大
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
// 处理重边,只需要记录最短的边
g[a][b] = min(g[a][b], c);
}
cout << dijkstra() << endl;
return 0;
}
邻接表的写法:
#include
#include
using namespace std;
const int N = 1e6 + 10;
int h[N], w[N], e[N], ne[N], idx; // 邻接表
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++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n; i++)
{
int t = -1;
// 找到未确定最短路的点中,距离源点最近的点 t
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
st[t] = true;
// 使用 t 更新邻点的最短路径
for (int j = h[t]; j != -1; j = ne[j])
{
int k = e[j]; // 遍历 t 的邻点
dist[k] = min(dist[k], dist[t] + w[j]);
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra() << endl;
}
题目:AcWing 850. Dijkstra求最短路 II - AcWing
堆优化版的 Dijkstra 使用 最小堆 优化朴素版中的寻找距离最短的点。
C++ 中的 pair 二元组默认支持比较,以 first 为第一关键字,second 为第二关键字,进行字典序比较。
堆中存储的是一个 二元组 {距离, 节点编号},从而优化 找出距离最短的点 这个过程。
时间复杂度: O ( m l o g n ) O(mlogn) O(mlogn),遍历所有点的出边,相当于遍历所有边,所以是 m 次
邻接表:这题最合适的解法
#include
#include
#include
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII; // 堆里存储 { 距离, 节点编号 }
int h[N], w[N], e[N], ne[N], idx; // 邻接表(稀疏图)
int dist[N]; // 距离源点的距离
bool st[N]; // 是否已经找到最短路
int n, m;
// 添加 a 指向 b 边长为 c 的边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); // 距离初始化为无穷大
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆
heap.push({0, 1}); // 1 号点距离源点距离为 1
while (heap.size())
{
PII t = heap.top(); // 距离源点最近的点
heap.pop();
int ver = t.second; // 节点编号
// int distance = t.first; // 源点距离 ver 的距离
if (st[ver]) continue; // 如果距离已经确定,则跳过该点
// 更新 ver 所指向的节点距离
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
// 比较 1-->j 和 1-->ver-->j 的距离
if (dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j}); // 距离变小,则入堆
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); // 邻接表不需要考虑重边
}
cout << dijkstra() << endl;
return 0;
}
邻接矩阵:内存开辟过大,会报错
#include
#include
#include
#include
using namespace std;
const int N = 5500;
typedef pair<int, int> PII; // {距离, 节点编号}
int g[N][N]; // 邻接矩阵
int dist[N], st[N];
int n, m;
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆
heap.push({0, 1});
while (heap.size())
{
PII t = heap.top();
heap.pop();
int ver = t.second; // 距离源点最近的点
if (st[ver]) continue;
// 利用 t 更新它的邻点
st[ver] = true;
for (int i = 1; i <= n; i++)
{
// 比较 1->i 和 1->ver->i
if (dist[i] > dist[ver] + g[ver][i])
{
dist[i] = dist[ver] + g[ver][i];
heap.push({dist[i], i});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = min(g[a][b], c); // 处理重边
}
cout << dijkstra() << endl;
return 0;
}
bellman-ford 算法擅长解决有边数限制的最短路问题。
此题边权可能为负数,所以不能使用 Dijkstra。
参考:AcWing 853. 有边数限制的最短路 - AcWing
Dijkstra 不能解决负权边是因为 Dijkstra 要求每个点被确定后st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了。
思路:连续进行松弛,在每次松弛时把每条边更新一下,若在 n - 1 次松弛后还能更新,说明图中有负环,无法得出结果,否则完成。
步骤:
// back 数组是上一次迭代后 dist 数组的备份。
for n 次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b], back[a] + w)
时间复杂度: O ( n m ) O(nm) O(nm)
为什么需要 back 数组?
为了避免如下的串联情况, 在边数限制为一条的情况下,节点 3 的距离应该是 3,但是由于串联情况,利用本轮更新的节点 2 更新了节点 3 的距离,所以现在节点 3 的距离是 2。

正确做法是用上轮节点 2 更新的距离 – 无穷大,来更新节点 3, 再取最小值,所以节点 3 离起点的距离是 3。

#include
#include
using namespace std;
const int N = 510, M = 10010;
int dist[N]; // 到源点的距离
int backup[N]; // 备份数组防止串联
int n, m, k;
// a 指向 b 权重为 w 的边
struct Edge {
int a, b, w;
} edges[M];
void bellman_ford()
{
// 初始化距离为无穷大
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 源点到源点距离为 0
for (int i = 0; i < k; i++)
{
// 备份上一次迭代的结果,防止出现串联(用本次更新的点去更新其他点)
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;
// 比较 1->b 和 1->a->b 的路径长度
dist[b] = min(dist[b], backup[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else cout << dist[n] << endl;
}
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();
return 0;
}
SPFA 的限制很小,只要图中没有负环,就可以用。 SPFA 不仅可以处理负权图,也可以处理正权图。
SPFA 可以看作对 Bellman_ford 的优化:
SPFA 和 Dijkstra 中的
st[]数组:
- Dijkstra 中的
st[]中保存的是当前确定了到源点距离最短的点,确定以后不可逆。- SPFA 中的
st[]仅仅表示当前发生过更新的点,是可逆的。SPFA 和 Dijksra 的思想:
- Dijkstra 是基于贪心的思想,每次选择最近的点去更新其它点,过后就不再访问。
- SPFA 中,只要有某个点的距离更新了,就加入到队列中,去更新其他点,所有每个点有可能重复入队。
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储
int dist[N]; // 距离源点的距离
bool st[N]; // 每个点是否在队列中
int n, m;
// 添加一条 a 指向 b 权重为 c 的边
void add (int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 求 1 号点到 n 号点的最短路距离
void spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
int a = q.front();
q.pop();
st[a] = false;
// 遍历 a 的出边指向的点
for (int i = h[a]; i != -1; i = ne[i])
{
int b = e[i], c = w[i];
if (dist[b] > dist[a] + c)
{
dist[b] = dist[a] + c;
if (!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) puts("impossible");
else cout << dist[n] << endl;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
add (a, b, c);
}
spfa();
return 0;
}
求负环的常用方法,基于 SPFA,一般都用方法 2(该题也是用方法 2):
方法 1:统计每个点入队的次数,如果某个点入队 n 次,则说明存在负环
方法 2:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于 n,则也说明存在环
#include
#include
#include
using namespace std;
const int N = 1e5 + 10;
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储
int dist[N]; // 每个点距离源点的距离
int cnt[N]; // 每个点到源点的边数
bool st[N]; // 每个点是否在队列中
int n, m;
// 添加一条 a 指向 b 权重为 c 的边
void add (int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 求 1 号点到 n 号点的最短路距离
bool spfa()
{
queue<int> q;
// 判断负环,只从一个点出发,有可能到达不了负环那里
// 需要一开始就把所有结点放入队列,且标记进入了队列提高效率
for (int i = 1; i <= n; i++)
{
q.push(i);
st[i] = true;
}
while (q.size())
{
int a = q.front();
q.pop();
st[a] = false;
// 遍历 a 的出边指向的点
for (int i = h[a]; i != -1; i = ne[i])
{
int b = e[i], c = w[i];
if (dist[b] > dist[a] + c)
{
dist[b] = dist[a] + c;
cnt[b] = cnt[a] + 1;
if (cnt[b] >= n) return true;
if (!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
add (a, b, c);
}
puts(spfa() ? "Yes" : "No");
return 0;
}
Floyd 属于多源最短路径,能够求出任意 2 个顶点之间的最短路径,支持负权边(不能有负权回路)。
时间复杂度: O ( n 3 ) O(n^3) O(n3),效率比执行 n 次 Dijkstra 算法好。
单源最短路径算法,对每个顶点求一次,同样可以求出任意 2 个顶点之间的最短路径。
算法过程:
d[][] 数组,注意处理 自环 和 重边 的情况d[][] 数组原理:动态规划
f [ k ] [ i ] [ j ] f[k][i][j] f[k][i][j] 代表(k 的取值范围是从 1 到 n),考虑从 1 到 k 的节点作为中间经过的节点时,从 i 到 j 的最短路径。
状态转移方程: f [ k ] [ i ] [ j ] = m i n ( f [ k − 1 ] [ i ] [ j ] , f [ k − 1 ] [ i ] [ k ] + f [ k − 1 ] [ k ] [ j ] ) f[k][i][j] = min(f[k−1][i][j], f[k−1][i][k] + f[k−1][k][j]) f[k][i][j]=min(f[k−1][i][j],f[k−1][i][k]+f[k−1][k][j])
题目:AcWing 854. Floyd求最短路 - AcWing
#include
#include
using namespace std;
const int N = 210, M = 20010, INF = 0x3f3f3f3f;
// d[i][j] 表示从 i 到 j 的最短路径
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;
// 初始化
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i == j) d[i][j] = 0; // 处理自环
else d[i][j] = INF;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c); // 处理重边
}
floyd();
while (k --)
{
int a, b;
cin >> a >> b;
if (d[a][b] > INF / 2) puts("impossible");
else cout << d[a][b] << endl;
}
return 0;
}
m - 边的数量,n - 点的数量
稠密图:m 和 n^2 一个量级
稀疏图:m 和 n 一个量级
Kruskal 比 堆优化的 Prim 简单易用,所以堆优化的 Prim 并不常用。
给定一个无向图,在图中选择若干条边把图的所有节点连起来,要求边长之和最小,就是最小生成树。
整体思路:
dijkstra 是使用 t 更新其他点到 源点 的距离

题目:858. Prim算法求最小生成树 - AcWing题库
#include
#include
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int g[N][N]; // 稠密图
int dist[N]; // 节点到生成树的距离
bool st[N]; // 节点是否在生成树中
int n, m;
// 返回最小生成树中边权之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 从 1 号点开始生成
int res = 0;
// 每次循环选出一个点加入到生成树中
for (int i = 0; i < n; i++)
{
// 找到集合外,距离集合最小的点
int t = -1;
for (int j = 1; j <= n; j++) // 遍历所有节点
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 当前图是不连通的,不存在生成树
if (dist[t] == INF) return INF;
res += dist[t]; // 先更新最小生成树的边权和,防止有自环
// 更新生成树外的点到生成树的距离
for (int j = 1; j <= n; j++)
// t->j 距离小于原来的距离,则更新
dist[j] = min(dist[j], g[t][j]);
st[t] = true; // 标记该点已经在生成树中
}
return res;
}
int main()
{
// 各个点之间的距离初始化成无穷
memset(g, 0x3f, sizeof g);
cin >> n >> m;
while (m --)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c); // 无向图,有重边
}
int t = prim(); // 求最小生成树
if (t == INF) puts("impossible");
else cout << t << endl;
return 0;
}
此部分略过,原因如下:
Kruskal 算法:
判断是否会产生回路的方法为:使用并查集。
题目:859. Kruskal算法求最小生成树 - AcWing题库
图解:AcWing 859. 思路详解 + 代码注释 + 图解 \color{green}{思路详解+代码注释+图解} 思路详解+代码注释+图解 - AcWing
#include
#include
#include
using namespace std;
const int N = 2e5 + 10, INF = 0x3f3f3f3f;
int p[N]; // 并查集
int n, m;
struct Edge {
int a, b, w;
// 重载 < 符号,方便调用排序函数
bool operator < (const Edge &W) const
{
return w < W.w;
}
} edges[N];
// 并查集,找祖宗节点
int find(int x)
{
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
int cnt = 0; // 全部加入到树的集合中边的数量(可能有多个集合)
int res = 0; // 最小生成树的边权重之和
// 按边权从小到大顺序遍历所有边
for (int i = 0; i < m; i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
int pa = find(a), pb = find(b);
if (pa != pb) // a b 不在一个集合中
{
p[pa] = p[pb]; // 合并 a b
res += w; // 计算边权和
cnt ++; // 全部集合中的边数量 + 1
}
}
// 树中有 n 个节点便有 n-1 条边,如果 cnt 不等于 n-1,说明无法生成有 n 个节点的树
if (cnt < n - 1) return INF; // 无法生成最小生成树
return res;
}
int main()
{
cin >> n >> m;
// 初始化并查集
for (int i = 0; i < n; i++) p[i] = i;
// 建图
for (int i = 0; i < m; i++)
{
int a, b, w;
cin >> a >> b >> w;
edges[i] = {a, b, w};
}
// 按照边权升序排序
sort(edges, edges + m);
int t = kruskal();
if (t == INF) puts("impossible");
else cout << t;
return 0;
}
二分图,当且仅当图中不含奇数环。由于图中不含奇数环,所以染色过程中一定没有矛盾。
二分图:一定不含有奇数环,可能包含长度为偶数的环, 不一定是连通图
通俗易懂的定义:图中点通过移动能分成左右两部分,左侧的点只和右侧的点相连,右侧的点只和左侧的点相连。
下图是个二分图:

下图不是二分图:

DFS 代码思路:
1 和 2 表示不同颜色,0 表示 未染色由于某个点染色成功不代表整个图就是二分图,因此只有某个点染色失败才能立刻
break / return
- 染色失败相当于存在相邻的 2 个点染了相同的颜色
#include
#include
using namespace std;
// 由于是无向图,顶点数最大是 N,那么边数最大是顶点数的 2 倍
const int N = 1e5 + 10, M = N + N;
int h[N], e[M], ne[M], idx;
// 记录节点被染成哪种颜色, 0 表示未染色, 1, 2 是两种不同的颜色
int color[N];
int n, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 深度优先遍历对 u 及其邻点进行染色,并返回是否能够完成染色操作
bool dfs(int u, int c)
{
color[u] = c; // 对 u 染色
// 遍历 u 所有邻点并染色
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
// 邻点没有颜色,则递归处理这个邻点(1 染成 2,2 染成 1)
if (!color[j] && !dfs(j, 3 - c))
return false;
// 已经染色,且颜色不是 3 - c,则冲突
else if (color[j] && color[j] != 3 - c)
return false;
}
return true;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
// 读入边
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a); // 无向图
}
for (int i = 1; i <= n; i++) // 遍历所有点
if (!color[i]) // 没有染色
{
if (!dfs(i, 1)) // 染色该点,并递归处理和它相邻的点
{
puts("No");
return 0;
}
}
puts("Yes");
return 0;
}
BFS 代码思路:
1 和 2 表示不同颜色,0 表示 未染色queue 存 PII,表示 <点编号, 颜色>i 个点入队(默认颜色可以是 1 或 2)
while (队列不空)t,并遍历队头 t 的所有邻边
t 相反的颜色,并添加到队列t 的颜色相同, 则返回 false#include
#include
#include
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
int e[M], ne[M], h[N], idx;
int n, m;
int color[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 对 u 进行染色,并对其邻边进行染色
bool bfs(int u)
{
queue<int> q;
q.push(u);
color[u] = 1;
while (q.size())
{
int t = q.front();
q.pop();
int c = color[t]; // 颜色
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
// 未染色
if (!color[j])
{
color[j] = 3 - c;
q.push(j);
}
// 已染色且与 t 颜色相同
else if (color[j] && color[j] == c)
return false;
}
}
return true;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
bool flag = true;
for (int i = 1; i <= n; i++)
{
if (!color[i])
{
if (!bfs(i))
{
flag = false;
break;
}
}
}
puts(flag ? "Yes" : "No");
return 0;
}
有趣的示例: 匈牙利算法(简单易懂)_一只大秀逗的博客-CSDN博客
#include
#include
using namespace std;
const int N = 510, M = 1e5 + 10;
int h[N], e[M], ne[M], idx;
// st[j] = a 表示一轮模拟匹配中,女孩 j 被男孩 a 预定了
bool st[N];
// match[j] = a 表示女孩 j 现有配对男友是 a
int match[N];
// n1 n2 分别是两个点集的个数
int n1, n2, m;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 如果 x 参与模拟配对,会不会使匹配数增多
bool find(int x)
{
// 遍历 x 喜欢的女孩
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i]; // x 喜欢的女孩 j
if (!st[j]) // 如果这轮匹配中,女孩 j 未被预定
{
st[j] = true; // x 预定女孩 j
// 如果女孩 j 没有男朋友,或她原来的男朋友能够预定其他喜欢的女孩,则配对成功
if (!match[j] || find(match[j]))
{
match[j] = x;
return true;
}
}
}
// 自己喜欢的女孩全部被预定了,配对失败
return false;
}
int main()
{
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b); // 存边只存一条边即可,虽然是无向图
}
int res = 0;
for (int i = 1; i <= n1; i++)
{
// 每次模拟匹配的预定情况都不一样,每轮都需要初始化
memset(st, false, sizeof st);
if (find(i)) res++;
}
cout << res << endl;
return 0;
}