97. 小明逛公园
题目描述
小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。
给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。
小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。
输入描述
第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。
接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。
接下里的一行包含一个整数 Q,表示观景计划的数量。
接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。
输出描述
对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。
输入示例
7 3 2 3 4 3 6 6 4 7 8 2 2 3 3 4输出示例
4 -1
cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n,m;
cin >> n >> m; // 输入节点数 n 和边数 m
// 初始化邻接矩阵,默认边权很大(表示无穷大)
vector<vector<int>> edges(n+1, vector<int>(n+1, 10005));
// 读入 m 条边信息,并更新邻接矩阵
for(int i = 0; i < m; i++){
int s, t, v;
cin >> s >> t >> v; // s->t 边权为 v
edges[s][t] = v; // 无向图,双向赋值
edges[t][s] = v;
}
// Floyd-Warshall 算法求所有点对最短路
for(int k = 1; k <= n; k++){ // 中间节点
for(int i = 1; i <= n; i++){ // 起点
for(int j = 1; j <= n; j++){ // 终点
// 松弛操作:通过 k 节点的路径更短则更新
edges[i][j] = min(edges[i][j], edges[i][k] + edges[k][j]);
}
}
}
int z;
cin >> z; // 输入查询次数
while(z--){
int s, t;
cin >> s >> t; // 查询 s->t 的最短路
if(edges[s][t] == 10005) cout << -1 << endl; // 无路则输出 -1
else cout << edges[s][t] << endl; // 输出最短路径长度
}
return 0;
}
总结
1. 图的存储 --- 邻接矩阵
- 用二维数组表示权值
- 节点间无边设为大数(10005),便于后续迭代更新
- 对角线默认可视为 0(自己到自己距离)
2. 核心思想 --- 多源最短路
Floyd 算法的动态规划思想:
对任意两点距离
dist[i][j],如果通过中间节点k可以缩短路径,就更新:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
3. 三层循环结构
- 外层循环
k:尝试每个节点作为中转 - 中间与内层循环
i、j:对所有点对执行松弛操作 - 保证每次更新考虑了最多经过
k之前的所有节点
4. 复杂度分析
- 时间复杂度:O(n³),三重循环
- 空间复杂度:O(n²),邻接矩阵存储图
- 适合节点数较少(例如 n ≤ 500)的情况
127. 骑士的攻击
题目描述
在象棋中,马和象的移动规则分别是"马走日"和"象走田"。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。
棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界)
输入描述
第一行包含一个整数 n,表示测试用例的数量,1 <= n <= 100。
接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。
输出描述
输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。
输入示例
6 5 2 5 4 1 1 2 2 1 1 8 8 1 1 8 7 2 1 3 3 4 6 4 6输出示例
2 4 6 5 1 0
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <string.h>
using namespace std;
// 保存每个点的步数
int moves[1001][1001];
// 骑士的 8 种可能移动方向
int dir[8][2] = {
-2,-1, -2,1, 2,-1, 2,1,
-1,2, -1,-2, 1,2, 1,-2
};
int x2, y2; // 目标坐标
struct Knight {
int x, y; // 当前坐标
int g; // 起点到当前的实际代价
int h; // 当前到目标的估算代价(启发式)
int f; // f = g + h
bool operator <(const Knight& k) const {
return k.f < f; // 优先队列按 f 值升序
}
};
// 优先队列,用于 A* 搜索
priority_queue<Knight> que;
// 启发式函数:使用欧几里得距离的平方
int He(const Knight& k) {
return (k.x - x2)*(k.x - x2) + (k.y - y2)*(k.y - y2);
}
// A* 搜索函数
void Astar(const Knight& k) {
que.push(k);
while(!que.empty()) {
Knight cur = que.top();
que.pop();
// 到达目标,结束搜索
if(cur.x == x2 && cur.y == y2) break;
Knight next;
for(int i = 0; i < 8; i++) {
next.x = cur.x + dir[i][0];
next.y = cur.y + dir[i][1];
// 越界判断
if(next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) continue;
// 若该点未访问
if(moves[next.x][next.y] == 0) {
moves[next.x][next.y] = moves[cur.x][cur.y] + 1; // 更新步数
// 更新 A* 参数
next.g = cur.g + 5; // 实际代价 g 增加 5(可视作权重)
next.h = He(next); // 估算代价 h
next.f = next.g + next.h; // f = g + h
que.push(next);
}
}
}
}
int main() {
int n, x1, y1;
cin >> n; // 测试组数
while(n--) {
cin >> x1 >> y1 >> x2 >> y2;
memset(moves, 0, sizeof(moves)); // 清空步数数组
Knight start;
start.x = x1;
start.y = y1;
start.g = 0;
start.h = He(start);
start.f = start.g + start.h;
Astar(start);
while(!que.empty()) que.pop(); // 清空队列,准备下一组测试
cout << moves[x2][y2] << endl; // 输出最少步数
}
return 0;
}
总结
1. 问题背景
- 在 1000×1000 的棋盘上求骑士从
(x1, y1)到(x2, y2)的最少步数。 - 每次骑士可以走 8 种 L 型方向。
核心思想 --- A 搜索*
- 使用
g表示从起点到当前节点的实际代价(步数 × 权重) - 使用
h表示从当前节点到目标的估算代价(欧几里得距离平方) f = g + h作为优先队列排序依据- 每次从优先队列中取
f最小的节点扩展
2. 访问控制
moves[x][y] == 0表示该点未访问- 扩展节点时更新步数
moves[next.x][next.y] = moves[cur.x][cur.y] + 1
3. 复杂度分析
- 空间复杂度:O(N²),N=1000,存储棋盘状态
- 时间复杂度:理论上最坏 O(N² log N²),实际受启发式函数加速
- 对比 BFS,A* 利用启发式可以快速逼近目标点,减少搜索量
4. 使用场景
- 大型棋盘骑士最短路径
- 可改造为八方向网格寻路(机器人、游戏寻路)
- 启发式函数合理时,搜索效率明显高于普通 BFS
最短路算法总结
1. 单源最短路(Single Source)
| 算法 | 特点 | 条件 | 复杂度 |
|---|---|---|---|
| Dijkstra | 贪心 + 松弛 | 边权 ≥ 0 | O(E log V) |
| Bellman‑Ford | 边松弛 V−1 次 | 可负权 | O(V·E) |
| SPFA | Bellman‑Ford 队列优化 | 可负权 | 平均快,最坏 O(V·E) |
| A* | 启发式搜索 | 有明确目标 | 受启发式影响,通常快于 BFS |
2. 多源最短路(All Pairs)
| 算法 | 特点 | 条件 | 复杂度 |
|---|---|---|---|
| Floyd‑Warshall | DP 三重循环 | 可负权但无负环 | O(N³) |
3. 核心概念
- 松弛操作:若通过 u 到 v 的路径更短,则更新距离。
- 选择原则:
- 正权单源 → Dijkstra
- 负权单源 → Bellman‑Ford/SPFA
- 明确目标 → A*
- 全源 → 小图 Floyd,大稀疏图 Johnson
图论总结
1. 图论基础
- 图由 顶点(节点) 和 边(连线) 构成,可分为 有向图/无向图。
- 图可以是 加权/无权。
- 节点有 度数(无向图度;有向图入度和出度)。
- 图的连通性:无向图任意两点连通称 连通图 ,有向图互相可达称 强连通图。
2. 图的存储方式
- 邻接矩阵 :二维数组存边权或连接关系,适合 稠密图,访问快但空间大。
- 邻接表 :链表/数组存每个节点的邻居,适合 稀疏图,空间经济。
3. 图的遍历(基本搜索)
深度优先搜索(DFS)
- 沿一个方向深入直到无法继续,再回溯。
- 适合穷举路径、连通性判断、与递归结合应用。
- 模板有两种写法:处理当前节点 vs 处理子节点。
- 不保证最短路。
广度优先搜索(BFS)
- 从起点一圈一圈向外扩散,层级遍历。
- 适合寻找 最短无权路径 和层次结构问题。
- BFS 第一次到达节点即最短。
4. 并查集(Union‑Find)
- 用于管理集合关系,快速判断两个节点是否在同一个连通集合。
- 典型用例:检测图是否有环、判断是否是树、连通块统计。
- 优化策略包括 路径压缩 和 按秩合并 以提高效率。
5. 最小生成树(MST)
目标:在加权无向图中,选出 所有节点的最小连通子图,总权重最小。
Kruskal 算法
- 以 边集为主。
- 按边权排序,逐条加入,只加入不会成环的边(利用并查集判断)。
- 适合 稀疏图。
- 复杂度约 O(E log E)。
Prim 算法
- 以 节点集合为主。
- 从一个节点开始,每次选最小的边扩展到未加入的节点。
- 适合 稠密图。
- 复杂度可达 O(N²)。
6. 拓扑排序(Topological Sort)
- 适用于 有向无环图(DAG)。
- 把图的节点排成一个线性序列,满足:若存在边 u→v,则 u 在序列中出现在 v 之前。
核心思路(卡恩算法)
- 统计所有节点的 入度。
- 找出 入度为 0 的节点,加入结果。
- 删除该节点及其出边,更新其邻居的入度。
- 重复上述过程,直到没有入度为 0 的节点或全部处理完。
- 若最终处理节点数 < 总节点数,则说明图有环,不存在拓扑序。
性质与应用
- 拓扑排序不仅能 生成线性顺序 ,也能 检测有向图是否有环。
- 应用场景包括依赖关系处理、任务调度等。