图论理论基础(5)

文章目录

  • [题型:最短路径算法 - Dijkstra算法](#题型:最短路径算法 - Dijkstra算法)
    • [1. 核心思路](#1. 核心思路)
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 核心思想](#1.2 核心思想)
      • [1.3 与Prim算法的区别](#1.3 与Prim算法的区别)
    • [2. 基础版本(邻接矩阵)](#2. 基础版本(邻接矩阵))
      • [2.1 算法模板](#2.1 算法模板)
      • [2.2 时间复杂度分析](#2.2 时间复杂度分析)
    • [3. 堆优化版本(邻接表)](#3. 堆优化版本(邻接表))
      • [3.1 核心思想](#3.1 核心思想)
      • [3.2 算法模板](#3.2 算法模板)
      • [3.3 时间复杂度分析](#3.3 时间复杂度分析)
    • [4. 常见变形](#4. 常见变形)
      • [4.1 求源点到所有节点的最短路径](#4.1 求源点到所有节点的最短路径)
      • [4.2 记录最短路径(路径还原)](#4.2 记录最短路径(路径还原))
      • [4.3 多条最短路径](#4.3 多条最短路径)
      • [4.4 限制边数的最短路径](#4.4 限制边数的最短路径)
    • [5. 典型应用场景](#5. 典型应用场景)
    • [6. 算法对比总结](#6. 算法对比总结)
    • [7. 注意事项](#7. 注意事项)
  • [题型:最短路径算法 - Bellman-Ford算法](#题型:最短路径算法 - Bellman-Ford算法)
    • [1. 核心思路](#1. 核心思路)
      • [2. 基础版本(边列表)](#2. 基础版本(边列表))
      • [3. SPFA算法(队列优化的Bellman-Ford)](#3. SPFA算法(队列优化的Bellman-Ford))
      • [4. 时间复杂度分析](#4. 时间复杂度分析)
    • [5. 常见变形](#5. 常见变形)
      • [5.1 检测负权回路](#5.1 检测负权回路)
        • [5.1.1 Bellman-Ford检测负权回路](#5.1.1 Bellman-Ford检测负权回路)
        • [5.1.2 SPFA检测负权回路](#5.1.2 SPFA检测负权回路)
      • [5.2 限制边数的最短路径](#5.2 限制边数的最短路径)
    • [6. 典型应用场景](#6. 典型应用场景)
    • [7. 算法对比总结](#7. 算法对比总结)
    • [8. 注意事项](#8. 注意事项)
  • [题型:多源最短路 - Floyd算法](#题型:多源最短路 - Floyd算法)
    • [1. 核心思路](#1. 核心思路)
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 核心思想](#1.2 核心思想)
    • [2. 动态规划五部曲](#2. 动态规划五部曲)
      • [2.1 确定dp数组及下标含义](#2.1 确定dp数组及下标含义)
      • [2.2 确定递推公式](#2.2 确定递推公式)
      • [2.3 dp数组初始化](#2.3 dp数组初始化)
      • [2.4 确定遍历顺序](#2.4 确定遍历顺序)
      • [2.5 举例推导数组](#2.5 举例推导数组)
    • [3. 算法模板](#3. 算法模板)
      • [3.1 三维版本(便于理解)](#3.1 三维版本(便于理解))
      • [3.2 空间优化版本(推荐)](#3.2 空间优化版本(推荐))
      • [3.3 时间复杂度分析](#3.3 时间复杂度分析)
    • [4. 常见变形](#4. 常见变形)
      • [4.1 记录最短路径](#4.1 记录最短路径)
      • [4.2 检测负权回路](#4.2 检测负权回路)
      • [4.3 传递闭包](#4.3 传递闭包)
    • [5. 典型应用场景](#5. 典型应用场景)
    • [6. 算法对比总结](#6. 算法对比总结)
    • [7. 注意事项](#7. 注意事项)
  • 题型:A*算法(A-Star)
    • [1. 核心思路](#1. 核心思路)
      • [1.1 基本概念](#1.1 基本概念)
      • [1.2 评估函数](#1.2 评估函数)
      • [1.3 算法特点](#1.3 算法特点)
    • [2. 算法模板](#2. 算法模板)
      • [2.1 基础版本(网格地图)](#2.1 基础版本(网格地图))
      • [2.2 通用版本(图结构)](#2.2 通用版本(图结构))
      • [2.3 时间复杂度分析](#2.3 时间复杂度分析)
    • [3. 常见变形](#3. 常见变形)
      • [3.1 不同的启发式函数](#3.1 不同的启发式函数)
        • [3.1.1 曼哈顿距离(Manhattan Distance)](#3.1.1 曼哈顿距离(Manhattan Distance))
        • [3.1.2 欧氏距离(Euclidean Distance)](#3.1.2 欧氏距离(Euclidean Distance))
        • [3.1.3 切比雪夫距离(Chebyshev Distance)](#3.1.3 切比雪夫距离(Chebyshev Distance))
      • [3.2 路径还原](#3.2 路径还原)
      • [3.3 加权A*(Weighted A*)](#3.3 加权A*(Weighted A*))
    • [4. 典型应用场景](#4. 典型应用场景)
    • [5. 算法对比总结](#5. 算法对比总结)
    • [6. 注意事项](#6. 注意事项)

题型:最短路径算法 - Dijkstra算法

1. 核心思路

Dijkstra算法用于在**有权图(权值非负)**中求从起点到其他所有节点的最短路径。

1.1 基本概念

  • 单源最短路径:从一个源点出发,到图中所有其他节点的最短路径
  • 权值限制权值必须非负(因为访问过的节点不能再访问,负权值会导致错过真正的最短路)
  • 应用场景
    • 网络路由:找到数据包传输的最短路径
    • 地图导航:找到两点间的最短距离
    • 资源分配:找到最优分配路径

示例

复制代码
有向图:       从A到各点的最短路径:
A--3-->B       A → A: 0
|      |       A → B: 3
1      2       A → C: 1
|      |       A → D: 5 (A→C→D)
C--4-->D       A → E: 6 (A→B→E)
       |
       5
       |
       E

1.2 核心思想

采用贪心策略,每次选择距离源点最近且未访问过的节点,更新其邻居节点的最短距离。

算法流程(三部曲):

  1. 选节点:从所有未访问节点中,选择距离源点最近的节点
  2. 标记访问:将该节点标记为已访问
  3. 更新距离:更新该节点的所有邻居节点到源点的最短距离(通过minDist数组)

关键数据结构

  • minDist[i]:记录源点到节点i的最短距离
  • visited[i]:标记节点i是否已被访问

1.3 与Prim算法的区别

比较项 Prim算法 Dijkstra算法
目标 最小生成树(所有节点连通) 最短路径(源点到各点)
更新规则 minDist[j] = min(minDist[j], grid[cur][j]) minDist[v] = min(minDist[v], minDist[cur] + grid[cur][v])
含义 节点j到生成树的最小距离 源点到节点v的最短距离
计算方式 直接使用边的权值 源点到cur的距离 + cur到v的距离

核心区别

  • Primgrid[cur][j] 表示 cur 加入生成树后,生成树到节点j的距离
  • DijkstraminDist[cur] + grid[cur][v] 表示源点到cur的距离 + cur到v的距离 = 源点到v的距离

2. 基础版本(邻接矩阵)

2.1 算法模板

cpp 复制代码
#include<iostream>
#include<vector>
#include<climits>
using namespace std;

int main(){
    int n, m, p1, p2, val;  // n是节点数,m是边数
    cin >> n >> m;

    // 邻接矩阵(适合稠密图)
    // 标记数组序号和节点一致,都是1到n,不用0
    // 初始化为最大值,表示不连通
    vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));

    // 读入边
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2] = val;  // 有向图
        // 如果是无向图,需要添加:grid[p2][p1] = val;
    }

    int start = 1;  // 起点
    int end = n;    // 终点

    // 存储从源节点到每个节点的最短距离
    vector<int> minDist(n + 1, INT_MAX);

    // 记录顶点是否被访问过
    vector<bool> visited(n + 1, false);

    minDist[start] = 0;  // 起点到自身的距离为0

    // 遍历所有节点
    for(int i = 0; i < n; i++){
        int minVal = INT_MAX;
        int cur = -1;

        // 1. 选距离源节点最近且从未访问过的节点
        for(int v = 1; v <= n; v++){
            if(!visited[v] && minDist[v] < minVal){
                minVal = minDist[v];
                cur = v;
            }
        }

        // 如果没有找到未访问的节点,说明图不连通
        if(cur == -1) break;

        // 2. 标记该节点已经访问
        visited[cur] = true;

        // 3. 更新非访问节点到源节点距离(minDist数组)
        for(int v = 1; v <= n; v++){
            // 条件:未访问 && 有边连接 && 距离更短
            if(!visited[v] && grid[cur][v] != INT_MAX && 
               minDist[cur] + grid[cur][v] < minDist[v]){
                minDist[v] = minDist[cur] + grid[cur][v];
            }
        }
    }

    // 输出结果
    if(minDist[end] == INT_MAX) {
        cout << -1 << endl;  // 不能到达终点
    } else {
        cout << minDist[end] << endl;
    }

    return 0;
}

2.2 时间复杂度分析

  • 时间复杂度 :O(V²)
    • 外层循环:V次
    • 内层循环:每次遍历V个节点找最小值,再遍历V个节点更新距离
  • 空间复杂度:O(V²)(邻接矩阵)
  • 适用场景:稠密图(边数接近V²)

3. 堆优化版本(邻接表)

3.1 核心思想

类似Prim和Kruskal的思想

  • 稠密图:用邻接矩阵方便
  • 稀疏图:用邻接表较为方便

优化思路

  • 基础版本用两层循环遍历,一层是遍历所有节点找最近节点,第二层是更新minDist
  • 堆优化版本从边的角度 出发,利用小顶堆对边的权值进行排序,这样取出来的就是离源节点最近的节点

3.2 算法模板

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
#include<queue>
#include<climits>

using namespace std;

// 小顶堆比较器
class mycomparison{
    public:
    bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs){
        return lhs.second > rhs.second;  // 按距离从小到大排序
    }
};

// 定义一个结构体来表示带权重的边
struct Edge{
    int to;    // 目标节点
    int val;   // 边的权重

    Edge(int t, int w) : to(t), val(w) {}
};

int main(){
    int n, m, p1, p2, val;
    cin >> n >> m;

    // 邻接表(适合稀疏图)
    vector<list<Edge>> grid(n + 1);

    // 读入边
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        // p1指向p2,权值为val
        grid[p1].push_back(Edge(p2, val));
    }

    int start = 1;  // 起点
    int end = n;    // 终点

    // 存储从源节点到每个节点的最短距离
    vector<int> minDist(n + 1, INT_MAX);

    // 记录顶点是否被访问过
    vector<bool> visited(n + 1, false);

    // 优先级队列存放 pair<节点,源点到该节点的距离>
    //priority_queue<元素类型, 存储容器, 比较器>
    priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;

    // 初始化队列,源点到源点的距离是0
    pq.push(pair<int, int>(start, 0));
    minDist[start] = 0;  // 起始点到自身的距离为0

    while(!pq.empty()){
        // 1. 通过优先级队列,选源节点到哪个节点近且该节点未被访问过
        pair<int, int> cur = pq.top();
        pq.pop();

        // 如果该节点已经访问过,跳过
        if(visited[cur.first]) continue;

        // 2. 标记被访问过
        visited[cur.first] = true;

        // 3. 更新非访问节点到源点的距离(minDist数组)
        // 遍历cur指向的节点
        for(Edge edge : grid[cur.first]){
            // cur指向节点edge.to,这条边的权值为edge.val
            if(!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]){
                minDist[edge.to] = minDist[cur.first] + edge.val;
                pq.push(pair<int, int>(edge.to, minDist[edge.to]));
            }
        }
    }

    // 输出结果
    if(minDist[end] == INT_MAX) {
        cout << -1 << endl;
    } else {
        cout << minDist[end] << endl;
    }

    return 0;
}

3.3 时间复杂度分析

  • 时间复杂度 :O((V + E)logV)
    • 每个节点最多入队一次:O(V)
    • 每条边最多被访问一次:O(E)
    • 堆操作:O(logV)
  • 空间复杂度:O(V + E)(邻接表 + 堆)
  • 适用场景:稀疏图(边数远小于V²)

4. 常见变形

4.1 求源点到所有节点的最短路径

cpp 复制代码
// 在算法结束后,minDist数组存储的就是源点到所有节点的最短距离
// 遍历minDist数组即可得到所有最短路径
for(int i = 1; i <= n; i++){
    if(minDist[i] == INT_MAX) {
        cout << "节点" << i << "不可达" << endl;
    } else {
        cout << "源点到节点" << i << "的最短距离: " << minDist[i] << endl;
    }
}

4.2 记录最短路径(路径还原)

cpp 复制代码
// 添加parent数组记录路径
vector<int> parent(n + 1, -1);

// 在更新距离时记录父节点
if(minDist[cur] + grid[cur][v] < minDist[v]){
    minDist[v] = minDist[cur] + grid[cur][v];
    parent[v] = cur;  // 记录父节点
}

// 路径还原
vector<int> path;
int cur = end;
while(cur != -1){
    path.push_back(cur);
    cur = parent[cur];
}
reverse(path.begin(), path.end());

4.3 多条最短路径

cpp 复制代码
// 使用vector存储多条路径
vector<vector<int>> paths;
vector<int> path;

// 使用DFS回溯找到所有最短路径
void dfs(int node, int target, vector<int>& path, 
         vector<vector<int>>& graph, vector<int>& minDist){
    if(node == target){
        paths.push_back(path);
        return;
    }
    
    for(int next : graph[node]){
        if(minDist[next] == minDist[node] + 1){  // 假设边权为1
            path.push_back(next);
            dfs(next, target, path, graph, minDist);
            path.pop_back();
        }
    }
}

4.4 限制边数的最短路径

cpp 复制代码
// 使用动态规划:dp[k][v] 表示经过k条边到达v的最短距离
vector<vector<int>> dp(k + 1, vector<int>(n + 1, INT_MAX));
dp[0][start] = 0;

for(int step = 1; step <= k; step++){
    for(int u = 1; u <= n; u++){
        if(dp[step - 1][u] != INT_MAX){
            for(Edge edge : graph[u]){
                dp[step][edge.to] = min(dp[step][edge.to], 
                                        dp[step - 1][u] + edge.val);
            }
        }
    }
}

5. 典型应用场景

  1. 网络路由问题

    • LeetCode 743. 网络延迟时间
    • 找到数据包传输的最短路径
  2. 地图导航问题

    • 找到两点间的最短距离
    • 计算最优路线
  3. 资源分配问题

    • 找到最优分配路径
    • 最小成本路径
  4. 社交网络问题

    • 找到两个人之间的最短关系链
    • 六度分隔理论

6. 算法对比总结

特性 基础版本 堆优化版本
数据结构 邻接矩阵 邻接表 + 优先队列
时间复杂度 O(V²) O((V+E)logV)
空间复杂度 O(V²) O(V+E)
适用图类型 稠密图 稀疏图
实现难度 简单 中等
推荐 稠密图使用 稀疏图使用

选择建议

  • 稠密图 (E ≈ V²)→ 使用 基础版本(邻接矩阵)
  • 稀疏图 (E << V²)→ 使用 堆优化版本(邻接表)
  • 一般情况堆优化版本更通用,推荐使用

7. 注意事项

  1. 权值必须非负

    • 如果图中有负权边,Dijkstra算法无法得到正确结果
    • 需要使用Bellman-Ford或SPFA算法
  2. 有向图 vs 无向图

    • 有向图:只添加一条边 grid[p1][p2] = val
    • 无向图:需要添加两条边 grid[p1][p2] = val; grid[p2][p1] = val;
  3. 不可达节点

    • 如果 minDist[i] == INT_MAX,说明源点无法到达节点i
    • 需要在输出时进行判断
  4. 堆优化版本的重复入队

    • 同一个节点可能多次入队(因为距离可能被多次更新)
    • 使用 visited 数组避免重复处理
  5. 初始化问题

    • 起点距离必须初始化为0:minDist[start] = 0
    • 其他节点初始化为最大值:minDist[i] = INT_MAX

题型:最短路径算法 - Bellman-Ford算法

1. 核心思路

Bellman-Ford算法用于在**有权图(可以包含负权边)**中求从起点到其他所有节点的最短路径。

关键特性

  • 可以处理负权边:这是与Dijkstra算法的主要区别
  • 可以检测负权回路:如果图中存在负权回路,算法可以检测出来
  • 单源最短路径:从一个源点出发,到图中所有其他节点的最短路径

核心思想

对所有边进行松弛操作minDist[B] = min(minDist[A] + value, minDist[B]))n-1次(n为节点数量),从而求得最短路。

松弛操作的含义

  • 松弛1次:可以得到起点到达与起点一条边相连的节点的最短距离
  • 松弛2次:可以得到起点到达与起点两条边相连的节点的最短距离
  • ...
  • 松弛n-1次:可以得到起点到达所有节点的最短距离(因为n个节点最多需要n-1条边连接)

为什么需要n-1次松弛

  • 节点数量为n,起点到终点最多是n-1条边相连接
  • 对所有边松弛n-1次就一定能得到起点到达终点的最短距离

2. 基础版本(边列表)

算法模板

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
#include<climits>
using namespace std;

int main(){
    int n,m,p1,p2,val;
    cin>>n>>m;

    vector<vector<int>>grid;

    //将所有边保存起来
    for(int i=0;i<m;i++){
        cin>>p1>>p2>>val;
        //p1指向p2,权值为val
        grid.push_back({p1,p2,val});//行列表
        //grid[p1][p2]=val;//地图,注意不能混用
    }

    int start=1;
    int end=n;

    vector<int> minDist(n+1,INT_MAX);
    minDist[start]=0;
    //因为 Bellman-Ford 依赖"重复松弛"来逐渐传播最短路径信息,所以不需要visited数组
    for(int i=1;i<n;i++){//对所有边松弛n-1次
        for(vector<int> &side:grid){//每一次松弛都是对所有边进行松弛
            int from=side[0];//边的出发点
            int to=side[1];//边的到达点
            int price=side[2];//边的权值

            //松弛
            //防止从未计算的节点出发
            if(minDist[from]!=INT_MAX&&minDist[to]>minDist[from]+price){
                minDist[to]=minDist[from]+price;
            }
        }

    }

    if(minDist[end]==INT_MAX)cout<<-1<<endl;
    else cout<<minDist[end]<<endl;
}

3. SPFA算法(队列优化的Bellman-Ford)

核心思想

初始的Bellman-Ford算法对每条边都做了松弛操作,但实际上没有必要。只需要对上一次松弛时更新过的节点(用队列记录)作为出发节点所链接的边进行松弛操作即可。

优化原理

  • 使用队列记录需要松弛的节点
  • 只有距离被更新的节点才可能影响其他节点
  • 避免了大量无效的松弛操作

算法模板

cpp 复制代码
#include<iostream>
#include<vector>
#include<queue>
#include<list>
#include<climits>
using namespace std;

struct Edge{
    int to;//链接到节点
    int val;//边的权重

    Edge(int t,int w):to(t),val(w){}
};

int main(){
    int n,m,p1,p2,val;
    cin>>n>>m;

    vector<list<Edge>>grid(n+1);

    vector<bool> isInQueue(n+1);//优化一下,已经在队列里面的元素不用重复添加

    //记录输入
    for(int i=0;i<m;i++){
        cin>>p1>>p2>>val;
        grid[p1].push_back(Edge(p2,val));
    }

    int start=1;
    int end=n;

    vector<int> minDist(n+1,INT_MAX);
    minDist[start]=0;

    queue<int>que;
    que.push(start);

    while(!que.empty()){
        int node=que.front();que.pop();
        isInQueue[node]=false;//从队列取出来时候要取消标记,i因为只是保证已经在队列里面的元素不用重复加入

        for(Edge edge:grid[node]){
            int from=node;
            int to=edge.to;
            int val=edge.val;
            if(minDist[to]>minDist[from]+val){
                //开始松弛
                minDist[to]=minDist[from]+val;
                if(isInQueue[to]==false){//已在队列中的元素不用重复添加
                    que.push(to);
                    isInQueue[to]=true;
                }
            }
        }

    }
    if(minDist[end]==INT_MAX) cout<<"unconnected"<<endl;
    else cout<<minDist[end]<<endl;
}

为什么不会死循环

  • 如果没有负权回路(负权回路是指一个环,整个环上的权值和是负的),队列不会造成死循环
  • 即使有多个路径值一样也没有关系,不会死循环
  • 节点再加入队列需要有松弛的行为,但是每个节点已经计算出起点到该节点的最短路径后,就不会进入if判断,即不会有新的节点加入队列

4. 时间复杂度分析

  • Bellman-Ford基础版本
    • 时间复杂度:O(V × E)
    • 空间复杂度:O(V)
  • SPFA算法
    • 时间复杂度:平均O(E),最坏O(V × E)(存在负权回路时)
    • 空间复杂度:O(V + E)

5. 常见变形

5.1 检测负权回路

负权回路:一个环,整个环上的权值和是负的。如果存在负权回路,最短路径可能不存在(可以在这个环中一直转,minDist会一直变小)。

5.1.1 Bellman-Ford检测负权回路

在松弛n-1次已经得到结果后,可以进行第n次松弛,如果结果有变化,就代表存在负权回路:

cpp 复制代码
// 在Bellman-Ford算法后添加检测
bool hasNegativeCycle = false;
for(vector<int> &side : grid){
    int from = side[0];
    int to = side[1];
    int price = side[2];
    // 如果还能继续松弛,说明存在负权回路
    if(minDist[from] != INT_MAX && minDist[to] > minDist[from] + price){
        hasNegativeCycle = true;
        break;
    }
}
5.1.2 SPFA检测负权回路

在极端情况下,所有节点都与其他节点相连,每个点都入度为n-1,所以每个节点最多加入n-1次队列。如果某个节点加入队列次数超过了n-1次,那么该图一定有负权回路:

cpp 复制代码
vector<int> count(n + 1, 0);  // 记录每个节点入队次数

while(!que.empty()){
    int node = que.front();
    que.pop();
    isInQueue[node] = false;
    
    for(Edge edge : grid[node]){
        if(minDist[edge.to] > minDist[node] + edge.val){
            minDist[edge.to] = minDist[node] + edge.val;
            if(!isInQueue[edge.to]){
                count[edge.to]++;
                if(count[edge.to] > n - 1){
                    // 存在负权回路
                    return true;
                }
                que.push(edge.to);
                isInQueue[edge.to] = true;
            }
        }
    }
}

5.2 限制边数的最短路径

问题描述 :从起点到终点,最多经过k个中间城市(不是一定经过k个城市,可以经过的城市数量比k少),求最短路径。

核心思想

  • 标准的Bellman-Ford算法是松弛n-1次
  • 限制边数版本:最多经过k个中间城市,算上起点和终点就是k+1条边,所以只需要松弛k+1次

关键点 :需要使用上一轮的minDist(minDist_copy)来计算,保证每轮的更新是严格+1条边。

为什么需要复制

如果不复制,在同一轮松弛中,可能会使用本轮已经更新过的值,导致一条边被计算了多次。

示例说明

复制代码
图:1 -> 2  权重 1  
    2 -> 3  权重 1
要求:最多经过 1 条边(k=1)
正确答案:1 -> 2 OK,1 -> 3 不行(需要两条边)

如果不复制会怎么样?
第一轮(使用 minDist):
1 -> 2 得到距离 1
再用更新后的 minDist[2] = 1 继续松弛
→ 2 -> 3 得到距离 2 (错误!)
这等于是让"第一轮"用了两条边,违反了"最多1条边"的要求。

正确做法(复制 minDist 到 minDist_copy):
第一轮:
minDist_copy = {dist using 0 条边}
用 minDist_copy 松弛
只能更新 1 个边:1 -> 2
不会更新 3
这样保证每轮的更新是严格 +1 条边。

算法模板

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <climits>
using namespace std;

int main() {
    int src, dst,k ,p1, p2, val ,m , n;
    
    cin >> n >> m;

    vector<vector<int>> grid;

    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid.push_back({p1, p2, val});
    }

    cin >> src >> dst >> k;

    vector<int> minDist(n + 1 , INT_MAX);
    minDist[src] = 0;
    vector<int> minDist_copy(n + 1); // 用来记录上一次遍历的结果
    for (int i = 1; i <= k + 1; i++) {
        minDist_copy = minDist; // 获取上一次计算的结果
        for (vector<int> &side : grid) {
            int from = side[0];
            int to = side[1];
            int price = side[2];
            // 注意使用 minDist_copy 来计算 minDist 
            if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) {  
                minDist[to] = minDist_copy[from] + price;
            }
        }
    }
    if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点
    else cout << minDist[dst] << endl; // 到达终点最短路径

}

6. 典型应用场景

  1. 包含负权边的最短路径问题

    • 金融系统中的套利检测
    • 网络延迟可能为负的情况
  2. 负权回路检测

    • 检测图中是否存在负权回路
    • 套利机会检测
  3. 限制边数的最短路径

    • 航班中转次数限制
    • 网络跳数限制
  4. 动态图最短路径

    • 边权值可能变化的情况
    • 实时路径规划

7. 算法对比总结

特性 Dijkstra Bellman-Ford SPFA
权值限制 非负权 可负权 可负权
负权回路检测 不支持 支持 支持
时间复杂度 O((V+E)logV) O(V×E) 平均O(E),最坏O(V×E)
空间复杂度 O(V+E) O(V) O(V+E)
适用场景 非负权图 负权图、检测负权回路 负权图(优化版)
推荐 非负权图使用 需要检测负权回路 负权图优化版本

选择建议

  • 非负权图 → 使用 Dijkstra算法
  • 负权图,需要检测负权回路 → 使用 Bellman-Ford算法
  • 负权图,不需要检测负权回路 → 使用 SPFA算法(更高效)

8. 注意事项

  1. Bellman-Ford不需要visited数组

    • 因为依赖"重复松弛"来逐渐传播最短路径信息
    • 同一个节点可能被多次访问
  2. 边列表 vs 邻接矩阵

    • Bellman-Ford使用边列表存储所有边
    • 不能混用邻接矩阵和边列表
  3. 防止从未计算的节点出发

    • 松弛时需要判断 minDist[from] != INT_MAX
    • 避免从未访问的节点出发进行松弛
  4. SPFA的isInQueue数组

    • 从队列取出时要取消标记:isInQueue[node] = false
    • 只是保证已经在队列中的元素不用重复加入
    • 同一个节点可能多次入队和出队
  5. 限制边数时需要使用minDist_copy

    • 必须使用上一轮的结果进行计算
    • 保证每轮的更新是严格+1条边
    • 避免在同一轮中使用本轮已更新的值

题型:多源最短路 - Floyd算法

1. 核心思路

Floyd算法用于求解所有节点对之间的最短路径(多源最短路),而Dijkstra和Bellman-Ford算法都是单源最短路(只能有一个起点)。

1.1 基本概念

  • 多源最短路:一次性求出所有节点对之间的最短路径
  • 权值限制对边权值没有要求,可以处理正权、负权、零权
  • 核心算法 :基于动态规划思想
  • 应用场景
    • 任意两点间最短距离
    • 网络中心节点选择
    • 图的传递闭包

示例

复制代码
图:        Floyd结果(所有节点对的最短距离):
A--3--B     A→A: 0, A→B: 3, A→C: 1, A→D: 5
|     |     B→A: 3, B→B: 0, B→C: 4, B→D: 2
1     2     C→A: 1, C→B: 4, C→C: 0, C→D: 4
|     |     D→A: 5, D→B: 2, D→C: 4, D→D: 0
C--4--D

1.2 核心思想

动态规划思想:逐步允许使用更多节点作为中间节点,更新所有节点对之间的最短距离。

状态定义

  • grid[i][j][k]:表示节点i到节点j,允许使用节点1到k作为中间节点的最短距离

状态转移

  • 如果最短路径经过节点kgrid[i][j][k] = grid[i][k][k-1] + grid[k][j][k-1]
  • 如果最短路径不经过节点kgrid[i][j][k] = grid[i][j][k-1]
  • 取两者最小值:grid[i][j][k] = min(grid[i][k][k-1] + grid[k][j][k-1], grid[i][j][k-1])

空间优化

  • 由于只依赖k-1层,可以使用二维数组:grid[i][j]
  • 状态转移:grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j])

2. 动态规划五部曲

2.1 确定dp数组及下标含义

三维版本(便于理解):

  • grid[i][j][k]:节点i到节点j,允许使用节点1到k作为中间节点的最短距离

二维版本(空间优化):

  • grid[i][j]:节点i到节点j的最短距离(逐步更新)

2.2 确定递推公式

核心递推式

cpp 复制代码
grid[i][j][k] = min(
    grid[i][j][k-1],                    // 不经过节点k
    grid[i][k][k-1] + grid[k][j][k-1]   // 经过节点k
)

空间优化版本

cpp 复制代码
grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j])

2.3 dp数组初始化

初始化规则

  • grid[i][i][0] = 0:节点到自身的距离为0
  • grid[i][j][0] = edge[i][j]:直接相连的边,权值为边的权值
  • grid[i][j][0] = INF:不直接相连的节点,初始化为最大值

形象理解

  • 可以想象成初始化一个长方体的底层(k=0层)
  • 其他元素初始化为最大值

2.4 确定遍历顺序

关键k必须是最外层循环,i和j的顺序可以任意。

原因

  • k表示允许使用的中间节点集合,需要从小到大逐步扩展
  • i和j形成的平面初始值都是初始化好的
  • 如果k不是最外层,会导致使用未计算的值

遍历顺序

cpp 复制代码
for(int k = 1; k <= n; k++) {        // k必须最外层
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            // 状态转移
        }
    }
}

2.5 举例推导数组

(可以添加一个具体例子来说明)

3. 算法模板

3.1 三维版本(便于理解)

cpp 复制代码
#include<iostream>
#include<vector>
#include<climits>
using namespace std;

int main(){
    int n, m, p1, p2, val;
    cin >> n >> m;

    // 三维版本:grid[i][j][k] 表示节点i到j,允许使用节点1到k作为中间节点
    vector<vector<vector<int>>> grid(n + 1, 
        vector<vector<int>>(n + 1, 
            vector<int>(n + 1, INT_MAX)));

    // 初始化:k=0层(不使用任何中间节点)
    for(int i = 1; i <= n; i++){
        grid[i][i][0] = 0;  // 节点到自身距离为0
    }

    // 读入边
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2][0] = val;
        grid[p2][p1][0] = val;  // 无向图
    }

    // Floyd算法:k必须是最外层
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                // 状态转移:取经过k和不经过k的最小值
                grid[i][j][k] = min(
                    grid[i][j][k-1],                    // 不经过节点k
                    grid[i][k][k-1] + grid[k][j][k-1]  // 经过节点k
                );
            }
        }
    }

    // 输出:查询所有节点对的最短距离
    int z, start, end;
    cin >> z;
    while(z--){
        cin >> start >> end;
        if(grid[start][end][n] == INT_MAX) {
            cout << -1 << endl;  // 不可达
        } else {
            cout << grid[start][end][n] << endl;
        }
    }

    return 0;
}

3.2 空间优化版本(推荐)

由于只依赖k-1层,可以使用二维数组进行空间优化:

cpp 复制代码
#include<iostream>
#include<vector>
#include<climits>
using namespace std;

int main(){
    int n, m, p1, p2, val;
    cin >> n >> m;

    // 二维版本:grid[i][j] 表示节点i到j的最短距离
    vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));

    // 初始化
    for(int i = 1; i <= n; i++){
        grid[i][i] = 0;  // 节点到自身距离为0
    }

    // 读入边
    for(int i = 0; i < m; i++){
        cin >> p1 >> p2 >> val;
        grid[p1][p2] = val;
        grid[p2][p1] = val;  // 无向图
    }

    // Floyd算法:k必须是最外层
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                // 防止溢出:需要判断是否可达
                if(grid[i][k] != INT_MAX && grid[k][j] != INT_MAX){
                    grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]);
                }
            }
        }
    }

    // 输出
    int z, start, end;
    cin >> z;
    while(z--){
        cin >> start >> end;
        if(grid[start][end] == INT_MAX) {
            cout << -1 << endl;
        } else {
            cout << grid[start][end] << endl;
        }
    }

    return 0;
}

3.3 时间复杂度分析

  • 时间复杂度 :O(V³)
    • 三层循环,每层都是V次
  • 空间复杂度
    • 三维版本:O(V³)
    • 二维版本:O(V²)(推荐)
  • 适用场景:适合稠密图,节点数较少的情况

4. 常见变形

4.1 记录最短路径

使用parent数组记录路径:

cpp 复制代码
vector<vector<int>> parent(n + 1, vector<int>(n + 1, -1));

// 初始化
for(int i = 1; i <= n; i++){
    for(int j = 1; j <= n; j++){
        if(grid[i][j] != INT_MAX && i != j){
            parent[i][j] = i;  // j的前驱是i
        }
    }
}

// Floyd算法中更新
if(grid[i][j] > grid[i][k] + grid[k][j]){
    grid[i][j] = grid[i][k] + grid[k][j];
    parent[i][j] = parent[k][j];  // 更新路径
}

// 路径还原
void printPath(int i, int j){
    if(parent[i][j] == -1) return;
    printPath(i, parent[i][j]);
    cout << j << " ";
}

4.2 检测负权回路

如果存在负权回路,grid[i][i] < 0(节点到自身的距离为负):

cpp 复制代码
bool hasNegativeCycle(vector<vector<int>>& grid, int n){
    for(int i = 1; i <= n; i++){
        if(grid[i][i] < 0){
            return true;  // 存在负权回路
        }
    }
    return false;
}

4.3 传递闭包

判断节点i是否能到达节点j(无权图):

cpp 复制代码
vector<vector<bool>> reachable(n + 1, vector<bool>(n + 1, false));

// 初始化
for(int i = 1; i <= n; i++){
    reachable[i][i] = true;  // 节点到自身可达
}

// 读入边
for(int i = 0; i < m; i++){
    cin >> p1 >> p2;
    reachable[p1][p2] = true;
}

// Floyd传递闭包
for(int k = 1; k <= n; k++){
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++){
            reachable[i][j] = reachable[i][j] || 
                              (reachable[i][k] && reachable[k][j]);
        }
    }
}

5. 典型应用场景

  1. 任意两点间最短距离

    • 地图导航:查询任意两个地点间的最短路径
    • 网络路由:计算所有节点对之间的最短路径
  2. 图的传递闭包

    • 判断节点间的可达性
    • 关系传递问题
  3. 最小环问题

    • 找到图中权值最小的环
    • 检测负权回路
  4. 网络中心节点选择

    • 找到到所有其他节点距离和最小的节点
    • 网络优化问题

6. 算法对比总结

特性 Dijkstra Bellman-Ford Floyd
类型 单源最短路 单源最短路 多源最短路
权值限制 非负权 可负权 可负权
时间复杂度 O((V+E)logV) O(V×E) O(V³)
空间复杂度 O(V+E) O(V) O(V²)
适用场景 单源,非负权 单源,负权 多源,任意权值
推荐 单源非负权图 单源负权图 多源或节点数少

选择建议

  • 单源最短路,非负权 → 使用 Dijkstra算法
  • 单源最短路,负权 → 使用 Bellman-Ford或SPFA算法
  • 多源最短路 → 使用 Floyd算法
  • 节点数较少(V < 200)Floyd算法简单高效

7. 注意事项

  1. 遍历顺序是Floyd的精髓

    • k必须是最外层循环
    • i和j的顺序可以任意
    • 如果k不是最外层,会导致使用未计算的值
  2. 防止整数溢出

    • 在更新时判断 grid[i][k] != INT_MAX && grid[k][j] != INT_MAX
    • 避免两个INT_MAX相加导致溢出
  3. 初始化问题

    • 节点到自身距离必须初始化为0:grid[i][i] = 0
    • 不直接相连的节点初始化为最大值:grid[i][j] = INT_MAX
  4. 有向图 vs 无向图

    • 有向图:只添加一条边 grid[p1][p2] = val
    • 无向图:需要添加两条边 grid[p1][p2] = val; grid[p2][p1] = val;
  5. Floyd的特点

    • 从节点的角度计算,时间复杂度较高
    • 适合稠密图或节点数较少的情况
    • 可以一次性求出所有节点对的最短路径
  6. 空间优化

    • 推荐使用二维数组版本(空间优化)
    • 三维版本便于理解,但空间开销大

题型:A*算法(A-Star)

1. 核心思路

A*算法是一种启发式搜索算法,是广度优先搜索(BFS)的改良版本。

1.1 基本概念

  • 启发式搜索:使用启发式函数来指导搜索方向,提高搜索效率
  • 与BFS的区别
    • BFS:没有目的性,一圈圈去搜索所有可能的路径
    • A*:有方向性的搜索,优先探索更可能到达目标的路径(关键在于启发式函数影响队列中元素的排序)

核心思想

对队列中节点的排序权值进行定义,利用权值进行排序,优先处理更可能到达目标的节点。

1.2 评估函数

F = G + H

  • G(实际代价):从起点到当前节点的实际距离(已走过的路径)
  • H(启发式函数):从当前节点到终点的预估距离(启发式估计)
  • F(总评估值):节点的优先级,F值越小优先级越高

距离定义方式

  • 曼哈顿距离|x1-x2| + |y1-y2|(只能上下左右移动)
  • 欧氏距离√((x1-x2)² + (y1-y2)²)(可以斜向移动)
  • 切比雪夫距离max(|x1-x2|, |y1-y2|)(可以八个方向移动)

1.3 算法特点

优势

  • 比BFS更高效,能够快速找到目标
  • 在启发式函数设计合理的情况下,能找到最优解

局限性

  • A*搜索的路径和启发式函数有关
  • 如果启发式函数不满足可采纳性(admissible),A*不能保证一定是最短路
  • 在保证运行效率的情况下,可能找到次短路而非最短路

可采纳性(Admissible)

  • 如果启发式函数H(n)永远不会高估从节点n到目标的实际距离,则A*保证找到最优解
  • 即:H(n) ≤ 实际最短距离

2. 算法模板

2.1 基础版本(网格地图)

cpp 复制代码
#include<iostream>
#include<queue>
#include<cstring>
#include<climits>
using namespace std;

int moves[1001][1001];  // 记录到达每个位置的最少步数
int dir[8][2] = {-2,-1, -2,1, -1,2, 1,2, 2,1, 2,-1, 1,-2, -1,-2};  // 马走日,8个方向
int b1, b2;  // 目标点坐标

// F = G + H
// G:从起点到当前节点的实际路径消耗
// H:从当前节点到终点的预估消耗(启发式函数)

struct Knight{
    int x, y;      // 当前位置
    int g, h, f;   // G值、H值、F值
    
    bool operator < (const Knight& k) const{
        // 重载运算符,F值小的优先级高(小顶堆)
        return f > k.f;  // 注意:priority_queue默认是大顶堆,所以用>实现小顶堆
    }
};

priority_queue<Knight> que;

// 启发式函数:欧氏距离的平方(为了精度不开根号)
int Heuristic(const Knight& k){
    return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2);
}

void astar(const Knight& start){
    Knight cur, next;
    que.push(start);
    
    while(!que.empty()){
        cur = que.top();
        que.pop();
        
        // 到达目标点
        if(cur.x == b1 && cur.y == b2) break;
        
        // 遍历8个方向(马走日)
        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]){
                moves[next.x][next.y] = moves[cur.x][cur.y] + 1;
                
                // 计算F值
                next.g = cur.g + 5;  // 马走日,1*1+2*2=5(不开根号)
                next.h = Heuristic(next);
                next.f = next.g + next.h;
                
                que.push(next);
            }
        }
    }
}

int main(){
    int n, a1, a2;  // a1,a2是起始点,b1,b2是目标点,n是测试数量
    cin >> n;
    
    while(n--){
        cin >> a1 >> a2 >> b1 >> b2;
        memset(moves, 0, sizeof(moves));
        
        // 初始化起点
        Knight start;
        start.x = a1;
        start.y = a2;
        start.g = 0;
        start.h = Heuristic(start);
        start.f = start.g + start.h;
        
        astar(start);
        
        // 清空队列
        while(!que.empty()) que.pop();
        
        // 输出结果
        cout << moves[b1][b2] << endl;
    }
    
    return 0;
}

2.2 通用版本(图结构)

cpp 复制代码
#include<iostream>
#include<vector>
#include<queue>
#include<climits>
using namespace std;

struct Node{
    int id;
    int g, h, f;  // G值、H值、F值
    
    bool operator < (const Node& n) const{
        return f > n.f;  // 小顶堆
    }
};

// 启发式函数:估算从当前节点到目标节点的距离
int heuristic(int current, int target, vector<vector<int>>& graph){
    // 这里可以使用曼哈顿距离、欧氏距离等
    // 示例:使用简单的直线距离估算
    return abs(current - target);
}

void astar(int start, int target, vector<vector<pair<int, int>>>& graph){
    priority_queue<Node> pq;
    vector<int> dist(graph.size(), INT_MAX);
    vector<bool> visited(graph.size(), false);
    
    Node startNode;
    startNode.id = start;
    startNode.g = 0;
    startNode.h = heuristic(start, target, graph);
    startNode.f = startNode.g + startNode.h;
    
    pq.push(startNode);
    dist[start] = 0;

    while(!pq.empty()){
        Node cur = pq.top();
        pq.pop();
        
        if(visited[cur.id]) continue;
        visited[cur.id] = true;
        
        // 到达目标
        if(cur.id == target) break;
        
        // 遍历邻居节点
        for(auto& edge : graph[cur.id]){
            int next = edge.first;
            int weight = edge.second;
            
            if(!visited[next]){
                int newG = cur.g + weight;
                if(newG < dist[next]){
                    dist[next] = newG;
                    
                    Node nextNode;
                    nextNode.id = next;
                    nextNode.g = newG;
                    nextNode.h = heuristic(next, target, graph);
                    nextNode.f = nextNode.g + nextNode.h;
                    
                    pq.push(nextNode);
                }
            }
        }
    }
    
    cout << dist[target] << endl;
}

2.3 时间复杂度分析

  • 时间复杂度 :O(b^d),其中b是分支因子,d是解的深度
    • 最优情况下:O(b^d)(如果启发式函数完美)
    • 最坏情况下:退化为BFS,O(b^d)
  • 空间复杂度:O(b^d)(需要存储所有探索的节点)
  • 实际性能:通常比BFS快得多,因为启发式函数指导搜索方向

3. 常见变形

3.1 不同的启发式函数

3.1.1 曼哈顿距离(Manhattan Distance)

适用于只能上下左右移动的网格:

cpp 复制代码
int manhattan(int x1, int y1, int x2, int y2){
    return abs(x1 - x2) + abs(y1 - y2);
}
3.1.2 欧氏距离(Euclidean Distance)

适用于可以斜向移动的网格:

cpp 复制代码
int euclidean(int x1, int y1, int x2, int y2){
    return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);  // 不开根号
    // 或 return sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
}
3.1.3 切比雪夫距离(Chebyshev Distance)

适用于可以八个方向移动的网格:

cpp 复制代码
int chebyshev(int x1, int y1, int x2, int y2){
    return max(abs(x1 - x2), abs(y1 - y2));
}

3.2 路径还原

记录父节点,还原完整路径:

cpp 复制代码
struct Node{
    int x, y;
    int g, h, f;
    int parentX, parentY;  // 父节点坐标
};

vector<pair<int, int>> reconstructPath(Node target){
    vector<pair<int, int>> path;
    Node cur = target;
    
    while(cur.parentX != -1){
        path.push_back({cur.x, cur.y});
        // 根据parentX和parentY找到父节点
        // ...
    }
    
    reverse(path.begin(), path.end());
    return path;
}

3.3 加权A*(Weighted A*)

在启发式函数前加权重,平衡搜索速度和最优性:

cpp 复制代码
// F = G + w * H,其中w是权重
next.f = next.g + w * next.h;

// w > 1:更偏向速度,可能不是最优解
// w = 1:标准A*
// w < 1:更偏向最优性,搜索更慢

4. 典型应用场景

  1. 路径规划问题

    • 游戏中的NPC寻路
    • 机器人路径规划
    • 地图导航系统
  2. 拼图问题

    • 8数码问题
    • 15数码问题
    • 滑块拼图
  3. 网格地图搜索

    • 迷宫求解
    • 最短路径查找
    • 障碍物避让
  4. 游戏AI

    • 策略游戏中的单位移动
    • 实时策略游戏的路径查找
    • 塔防游戏的敌人路径

5. 算法对比总结

特性 BFS Dijkstra A*
搜索策略 无方向性,逐层扩展 贪心,选择最短距离 启发式,优先探索有希望的方向
数据结构 队列 优先队列 优先队列(按F值)
时间复杂度 O(V+E) O((V+E)logV) O(b^d)
最优性 保证最优(无权图) 保证最优(非负权) 取决于启发式函数
适用场景 无权图,最短路径 加权图,单源最短路 有目标点的路径搜索
优势 简单,保证最优 处理加权图 高效,有方向性
劣势 效率低,无方向性 无方向性 需要好的启发式函数

选择建议

  • 无权图,需要最短路径 → 使用 BFS
  • 加权图,单源最短路 → 使用 Dijkstra算法
  • 有明确目标点,需要高效搜索 → 使用 A*算法
  • 需要保证最优解 → 确保启发式函数满足可采纳性

6. 注意事项

  1. 启发式函数的选择

    • 启发式函数必须可采纳(admissible)才能保证最优解
    • 即:H(n) ≤ 实际最短距离
    • 如果启发式函数高估了距离,可能找不到最优解
  2. 优先级队列的实现

    • 使用小顶堆,F值小的优先级高
    • C++中priority_queue默认是大顶堆,需要重载<运算符或使用greater
  3. 已访问节点的处理

    • 可以使用visited数组避免重复访问
    • 或者允许节点多次入队(如果找到更优路径)
  4. G值和H值的计算

    • G值:从起点到当前节点的实际距离,需要累加
    • H值:从当前节点到目标的预估距离,使用启发式函数计算
    • F值:G + H,用于优先级排序
  5. A*不能保证最优解的情况

    • 如果启发式函数不满足可采纳性
    • 在保证运行效率的情况下,可能找到次优解
    • 需要根据实际需求权衡最优性和效率
  6. 适用场景

    • 适合有明确起点和终点的路径搜索问题
    • 不适合需要遍历所有节点的问题
    • 在网格地图、游戏寻路等场景中表现优异
相关推荐
承渊政道1 小时前
C++学习之旅【C++基础知识介绍】
c语言·c++·学习·程序人生
民乐团扒谱机1 小时前
【微实验】大规模网络的社区检测Clauset–Newman–Moore聚类算法(附完整MATLAB代码)
算法·matlab·聚类·聚类算法·cnm·语义
烛衔溟1 小时前
C语言图论:有向图基础
c语言·数据结构·图论·有向图
枫叶丹41 小时前
【Qt开发】Qt窗口(七) -> QColorDialog 颜色对话框
c语言·开发语言·c++·qt
闻缺陷则喜何志丹1 小时前
【计算几何】三角函数的和角公式、矢量旋转
c++·数学·计算几何·三角函数和角公式·矢量旋转
4***99741 小时前
工业网关助力Altivar320与S7-1200协同运行
ide·python·算法·spring·eclipse·sqlite·tornado
IT·小灰灰1 小时前
DeepSeek-V3.2:开源大模型的里程碑式突破与硅基流动平台实战指南
大数据·人工智能·python·深度学习·算法·数据挖掘·开源
渡我白衣1 小时前
计算机组成原理(2):计算机硬件的基本组成
运维·服务器·网络·c++·人工智能·网络协议·dubbo
【建模先锋】1 小时前
精品数据分享 | 锂电池数据集(六)基于深度迁移学习的锂离子电池实时个性化健康状态预测
人工智能·深度学习·机器学习·迁移学习·锂电池寿命预测·锂电池数据集·寿命预测