代码随想录算法训练营 Day53 | 图论 part11

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:尝试每个节点作为中转
  • 中间与内层循环 ij:对所有点对执行松弛操作
  • 保证每次更新考虑了最多经过 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 之前。
核心思路(卡恩算法)
  1. 统计所有节点的 入度
  2. 找出 入度为 0 的节点,加入结果。
  3. 删除该节点及其出边,更新其邻居的入度。
  4. 重复上述过程,直到没有入度为 0 的节点或全部处理完。
  5. 若最终处理节点数 < 总节点数,则说明图有环,不存在拓扑序。
性质与应用
  • 拓扑排序不仅能 生成线性顺序 ,也能 检测有向图是否有环
  • 应用场景包括依赖关系处理、任务调度等。
相关推荐
呃呃本2 小时前
算法题(图论)
算法·图论
一只数据集2 小时前
商超上货人形机器人全身运控数据集分析——Kuavo 5机器人5W型号夹爪末端执行器操作轨迹数据
人工智能·算法·机器人
谙弆悕博士3 小时前
【附Python源码】基于决策树的信用卡欺诈检测实战
python·学习·算法·决策树·机器学习·数据分析·scikit-learn
MATLAB代码顾问3 小时前
黏菌算法(SMA)原理详解与Python实现
开发语言·python·算法
张赫轩(不重名)3 小时前
图论3:连通性问题(复杂度均为 O(N + M) )
c++·算法·图论·拓扑学
Liangwei Lin3 小时前
LeetCode 238. 除了自身以外数组的乘积
算法
啦啦啦_99993 小时前
2. ID3决策树 & C4.5决策树
算法·决策树·机器学习
技术小黑3 小时前
CNN算法实战系列02 | ResNet50V2算法实战与解析
pytorch·深度学习·算法·cnn