2304. 网格中的最小路径代价 : 从「图论最短路」过渡到「O(1) 空间的原地模拟」

题目描述

这是 LeetCode 上的 2304. 网格中的最小路径代价 ,难度为 中等

Tag : 「最短路」、「图」、「模拟」、「序列 DP」、「动态规划」

给你一个下标从 0 开始的整数矩阵 grid,矩阵大小为 m x n,由从 0 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> m × n − 1 m \times n - 1 </math>m×n−1 的不同整数组成。

你可以在此矩阵中,从一个单元格移动到下一行的任何其他单元格。

如果你位于单元格 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y ) (x, y) </math>(x,y) ,且满足 <math xmlns="http://www.w3.org/1998/Math/MathML"> x < m − 1 x < m - 1 </math>x<m−1,你可以移动到 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x + 1 , 0 ) (x + 1, 0) </math>(x+1,0), <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x + 1 , 1 ) (x + 1, 1) </math>(x+1,1), ..., <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x + 1 , n − 1 ) (x + 1, n - 1) </math>(x+1,n−1) 中的任何一个单元格。注意: 在最后一行中的单元格不能触发移动。

每次可能的移动都需要付出对应的代价,代价用一个下标从 0 开始的二维数组 moveCost 表示,该数组大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m × n ) × n (m \times n) \times n </math>(m×n)×n ,其中 moveCost[i][j] 是从值为 i 的单元格移动到下一行第 j 列单元格的代价。从 grid 最后一行的单元格移动的代价可以忽略。

grid 一条路径的代价是:所有路径经过的单元格的值之和加上所有移动的代价之和 。从第一行任意单元格出发,返回到达最后一行任意单元格的最小路径代价。

示例 1:

css 复制代码
输入:grid = [[5,3],[4,0],[2,1]], moveCost = [[9,8],[1,5],[10,12],[18,6],[2,4],[14,3]]

输出:17

解释:最小代价的路径是 5 -> 0 -> 1 。
- 路径途经单元格值之和 5 + 0 + 1 = 6 。
- 从 5 移动到 0 的代价为 3 。
- 从 0 移动到 1 的代价为 8 。
路径总代价为 6 + 3 + 8 = 17 。

示例 2:

lua 复制代码
输入:grid = [[5,1,2],[4,0,3]], moveCost = [[12,10,15],[20,23,8],[21,7,1],[8,1,13],[9,10,25],[5,3,2]]

输出:6

解释:
最小代价的路径是 2 -> 3 。 
- 路径途经单元格值之和 2 + 3 = 5 。 
- 从 2 移动到 3 的代价为 1 。 
路径总代价为 5 + 1 = 6 。

提示:

  • <math xmlns="http://www.w3.org/1998/Math/MathML"> m = g r i d . l e n g t h m = grid.length </math>m=grid.length
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> n = g r i d [ i ] . l e n g t h n = grid[i].length </math>n=grid[i].length
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 < = m , n < = 50 2 <= m, n <= 50 </math>2<=m,n<=50
  • grid 由从 0m * n - 1 的不同整数组成
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> m o v e C o s t . l e n g t h = m × n moveCost.length = m \times n </math>moveCost.length=m×n
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> m o v e C o s t [ i ] . l e n g t h = n moveCost[i].length = n </math>moveCost[i].length=n
  • <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = m o v e C o s t [ i ] [ j ] < = 100 1 <= moveCost[i][j] <= 100 </math>1<=moveCost[i][j]<=100

建新图 + 建虚拟点 + 堆优化 Dijkstra

注意:可以直接使用解法二的方法,但先认真看完本做法,再去看解法二,会有相当丝滑的体验。

每次移动,实际路径权值 = 经过边的权值 + 目的地的权值

利用原图,构建新图:每个单元格视为一个点,除最后一行外,每个点对下一行的所有点连一条有向边,边权 = 原图中该边的权值 + 原图中该目的地的权值

分析新图中的点边数量:

  • 点:共 <math xmlns="http://www.w3.org/1998/Math/MathML"> m × n m \times n </math>m×n 个点,数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2.5 × 1 0 3 2.5 \times 10^3 </math>2.5×103
  • 边:不算最后一行,共 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m − 1 ) × n (m - 1) \times n </math>(m−1)×n 个点,这些点与下一行的每个点均有一条有向边,合计 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( m − 1 ) × n 2 (m - 1) \times n^2 </math>(m−1)×n2 条边,数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1.25 × 1 0 5 1.25 \times 10^5 </math>1.25×105

原问题转换为:求点 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> j j </math>j 的最短路,其中点 <math xmlns="http://www.w3.org/1998/Math/MathML"> i i </math>i 所在位置为第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 行,点 <math xmlns="http://www.w3.org/1998/Math/MathML"> j j </math>j 所在位置为第 <math xmlns="http://www.w3.org/1998/Math/MathML"> m − 1 m - 1 </math>m−1 行。

这似乎是一个「多源汇最短路」问题?但求解多源汇最短路的 Floyd 算法是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 ) O(n^3) </math>O(n3) 的,会超时。

实际上,我们也并不真的关心图中任意点之间的最短路,仅仅关心第一行到最后一行的最短路。

因此,我们可通过建立"虚拟源点"和"虚拟汇点"的方式,来将"多源汇最短路"问题转换为"单源最短路"问题。

具体的,我们创建一个"虚拟源点",该点向所有第一行的点连权值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> g r i d [ 0 ] [ i ] grid[0][i] </math>grid[0][i] 的有向边;同时创建一个"虚拟汇点",最后一行的所有点向该点连权值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 的有向边。

问题进一步转化为:求"虚拟源点"到"虚拟汇点"的最短路。

至此,我们通过 建新图 -> 创建虚拟源汇点(转换为单源最短路)-> 套用单源最短路算法 解决本题。

将新图中点的数量记为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,边数记为 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m,朴素 Dijkstra 复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2),堆优化的 Dijkstra 的复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m log ⁡ n ) O(m\log{n}) </math>O(mlogn),当 <math xmlns="http://www.w3.org/1998/Math/MathML"> m < n 2 m < n^2 </math>m<n2 (相对稀疏)时,优先使用堆优化 Dijkstra

Java 代码:

Java 复制代码
class Solution {
    int N = 50 * 50 + 2, M = 50 * 50 * 50, idx = 0, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int N = grid.length, M = grid[0].length;
        int S = N * M, T = S + 1;
        n = N * M + 2;
        Arrays.fill(he, -1);
        //「虚拟源点」向「第一行」进行连边
        for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]);
        // 转换原图
        for (int i = 0; i < N - 1; i++) {
            for (int j = 0; j < M; j++) {
                int a = grid[i][j];
                for (int k = 0; k < M; k++) {
                    int b = grid[i + 1][k];
                    add(a, b, moveCost[a][k] + b);
                }
            }
        }
        //「最后一行」向「虚拟汇点」进行连边
        for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0);
        // 最短路
        int[] dist = dijkstra(S);
        return dist[T];
    }
    int[] dijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        boolean[] vis = new boolean[n];
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{x, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int u = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.add(new int[]{j, dist[j]});
            }
        }
        return dist;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    static const int N = 50 * 50 + 2, M = 50 * 50 * 50;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int N = grid.size(), M = grid[0].size();
        int S = N * M, T = S + 1;
        n = N * M + 2;
        fill(he, he + n, -1);
        //「虚拟源点」向「第一行」进行连边
        for (int i = 0; i < M; i++) add(S, grid[0][i], grid[0][i]);
        // 转换原图
        for (int i = 0; i < N - 1; i++) {
            for (int j = 0; j < M; j++) {
                int a = grid[i][j];
                for (int k = 0; k < M; k++) {
                    int b = grid[i + 1][k];
                    add(a, b, moveCost[a][k] + b);
                }
            }
        }
        //「最后一行」向「虚拟汇点」进行连边
        for (int i = 0; i < M; i++) add(grid[N - 1][i], T, 0);
        // 最短路
        vector<int> dist = dijkstra(S);
        return dist[T];
    }
    vector<int> dijkstra(int x) {
        vector<int> dist(n, 0x3f3f3f3f);
        vector<bool> vis(n, false);
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点
        priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
        q.push({0, x});
        while (!q.empty()) {
            // 每次从「优先队列」中弹出
            auto [step, u] = q.top();
            q.pop();
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.push({dist[j], j});
            }
        }
        return dist;
    }
};

Python 代码:

Python 复制代码
import heapq

class Solution:
    def minPathCost(self, grid, moveCost):
        N, M = len(grid), len(grid[0])
        S, T = N * M, N * M + 1
        n = N * M + 2
        he = [-1] * n
        e, ne, w = [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50), [-1] * (50 * 50 * 50)
        idx = 0

        def add(a, b, c):
            nonlocal idx
            e[idx] = b
            ne[idx] = he[a]
            w[idx] = c
            he[a] = idx
            idx += 1

        def dijkstra(x):
            dist = [float('inf')] * n
            vis = [False] * n
            dist[x] = 0
            # 使用「优先队列」存储所有可用于更新的点
            # 以 (到起点的距离, 点编号) 进行存储,优先弹出「最短距离」较小的点
            q = [(0, x)]
            heapq.heapify(q)
            while q:
                # 每次从「优先队列」中弹出
                step, u = heapq.heappop(q)
                # 如果弹出的点被标记「已更新」,则跳过
                if vis[u]: continue
                # 标记该点「已更新」,并使用该点更新其他点的「最短距离」
                vis[u] = True
                i = he[u]
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    heapq.heappush(q, (dist[j], j))
            return dist

        #「虚拟源点」向「第一行」进行连边
        for i in range(M):
            add(S, grid[0][i], grid[0][i])
        # 转换原图
        for i in range(N - 1):
            for j in range(M):
                a = grid[i][j]
                for k in range(M):
                    b = grid[i + 1][k]
                    add(a, b, moveCost[a][k] + b)
        #「最后一行」向「虚拟汇点」进行连边
        for i in range(M):
            add(grid[N - 1][i], T, 0)
        # 最短路
        dist = dijkstra(S)
        return dist[T]
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m log ⁡ n ) O(m\log{n}) </math>O(mlogn),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为新图中的点数 <math xmlns="http://www.w3.org/1998/Math/MathML"> N × M = 2.5 × 1 0 3 N \times M = 2.5 \times 10^3 </math>N×M=2.5×103, <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 为新图中的边数 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( M − 1 ) × N 2 = 1.25 × 1 0 5 (M - 1) \times N^2 = 1.25 \times 10^5 </math>(M−1)×N2=1.25×105
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + m ) O(n + m) </math>O(n+m)

堆优化 Dijkstra

什么?你说你实在不想建新图,也不想搞什么虚拟点,就想用你心爱的 BFS 来做?!

我懂你意思,但那不叫 BFS

只是将「建新图」和「建虚拟点」的过程省掉,仍需要使用优先队列(堆)来每次取出当前"路径代价最小"的点来进行扩充,执行过程仍为堆优化 Dijkstra 的核心操作。

尤其所谓"省掉" 建新图 和 建虚拟点,真就字面上的"省掉",并非不存在,因为两种做法思路是完全一致的。可简单列举「本解法」与「解法一」的对应关系:

  • 起始往队列放入首行元素,对应了解法一的"建立虚拟源点"过程;
  • 从队列中取元素出来扩充时,若当前元素所在行是最后一行时,用当前路径代价来更新答案,对应了解法一的"建立虚拟汇点"过程;
  • 扩充时直接遍历列(即下一行的所有点),对应的解法一的"用原图边建新图"的过程。

Java 代码:

Java 复制代码
class Solution {
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) dist[i][j] = INF;
        }
        PriorityQueue<int[]> d = new PriorityQueue<>((a,b)->a[2]-b[2]);
        for (int i = 0; i < n; i++) {
            d.add(new int[]{0, i, grid[0][i]});
            dist[0][i] = grid[0][i];
        }
        while (!d.isEmpty()) {
            int[] info = d.poll();
            int x = info[0], y = info[1], cur = info[2];
            if (x == m - 1) {
                ans = Math.min(ans, cur);
                continue;
            }
            for (int i = 0; i < n; i++) {
                int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i];
                int tot = cur + step + ne;
                if (tot >= ans || dist[x + 1][i] <= tot) continue;
                dist[x + 1][i] = tot;
                d.add(new int[]{x + 1, i, tot});
            }
        }
        return ans;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int m = grid.size(), n = grid[0].size(), INF = 0x3f3f3f3f, ans = INF;
        vector<vector<int>> dist(m, vector<int>(n, INF));
        priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> pq;
        for (int i = 0; i < n; i++) {
            pq.push({0, i, grid[0][i]});
            dist[0][i] = grid[0][i];
        }
        while (!pq.empty()) {
            vector<int> info = pq.top();
            pq.pop();
            int x = info[0], y = info[1], cur = info[2];
            if (x == m - 1) {
                ans = min(ans, cur);
                continue;
            }
            for (int i = 0; i < n; i++) {
                int step = moveCost[grid[x][y]][i], ne = grid[x + 1][i];
                int tot = cur + step + ne;
                if (tot >= ans || dist[x + 1][i] <= tot) continue;
                dist[x + 1][i] = tot;
                pq.push({x + 1, i, tot});
            }
        }
        return ans;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def minPathCost(self, grid, moveCost):
        m, n, INF = len(grid), len(grid[0]), float('inf')
        ans = INF
        dist = [[INF] * n for _ in range(m)]
        for i in range(n):
            dist[0][i] = grid[0][i]
        pq = [(0, i, grid[0][i]) for i in range(n)]
        while pq:
            x, y, cur = heapq.heappop(pq)
            if x == m - 1:
                ans = min(ans, cur)
                continue
            for i in range(n):
                step, ne = moveCost[grid[x][y]][i], grid[x + 1][i]
                tot = cur + step + ne
                if tot >= ans or dist[x + 1][i] <= tot: continue
                dist[x + 1][i] = tot
                heapq.heappush(pq, (x + 1, i, tot))
        return ans
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m log ⁡ n ) O(m\log{n}) </math>O(mlogn),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 为新图中的点数 <math xmlns="http://www.w3.org/1998/Math/MathML"> N × M = 2.5 × 1 0 3 N \times M = 2.5 \times 10^3 </math>N×M=2.5×103, <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 为新图中的边数 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( M − 1 ) × N 2 = 1.25 × 1 0 5 (M - 1) \times N^2 = 1.25 \times 10^5 </math>(M−1)×N2=1.25×105
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + m ) O(n + m) </math>O(n+m)

原地模拟

什么?你说你连图论的方法都不想用,想就着题意做一遍?

可以。甚至当你调整更新方向,还能利用已有的 grid,实现原地模拟。

具体的,我们将"从上往下走"调整为"从下往上走",这样可以确保当我们使用底下一行 <math xmlns="http://www.w3.org/1998/Math/MathML"> g r i d [ i + 1 ] [ j ] grid[i + 1][j] </math>grid[i+1][j] 来更新当前行 <math xmlns="http://www.w3.org/1998/Math/MathML"> g r i d [ i ] [ j ] grid[i][j] </math>grid[i][j] 时,所用到的 <math xmlns="http://www.w3.org/1998/Math/MathML"> g r i d [ i + 1 ] [ j ] grid[i + 1][j] </math>grid[i+1][j] 不会被覆盖。

Java 代码:

Java 复制代码
class Solution {
    public int minPathCost(int[][] grid, int[][] moveCost) {
        int m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
        for (int i = m - 2; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                int cur = INF;
                for (int k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
                grid[i][j] += cur;
            }
        }
        for (int i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]);
        return ans;
    }
}

C++ 代码:

C++ 复制代码
class Solution {
public:
    int minPathCost(vector<vector<int>>& grid, vector<vector<int>>& moveCost) {
        int m = grid.size(), n = grid[0].size(), INF = INT_MAX, ans = INF;
        for (int i = m - 2; i >= 0; i--) {
            for (int j = 0; j < n; j++) {
                int cur = INF;
                for (int k = 0; k < n; k++) cur = min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
                grid[i][j] += cur;
            }
        }
        for (int i = 0; i < n; i++) ans = min(ans, grid[0][i]);
        return ans;
    }
};

Python 代码:

Python 复制代码
class Solution:
    def minPathCost(self, grid, moveCost):
        m, n = len(grid), len(grid[0])
        for i in range(m - 2, -1, -1):
            for j in range(n):
                grid[i][j] += min([grid[i + 1][k] + moveCost[grid[i][j]][k] for k in range(n)])
        return min([grid[0][i] for i in range(n)])

TypeScript 代码:

TypeScript 复制代码
function minPathCost(grid: number[][], moveCost: number[][]): number {
    let m = grid.length, n = grid[0].length, INF = 0x3f3f3f3f, ans = INF;
    for (let i = m - 2; i >= 0; i--) {
        for (let j = 0; j < n; j++) {
            let cur = INF;
            for (let k = 0; k < n; k++) cur = Math.min(cur, grid[i + 1][k] + moveCost[grid[i][j]][k]);
            grid[i][j] += cur;
        }
    }
    for (let i = 0; i < n; i++) ans = Math.min(ans, grid[0][i]);
    return ans;
};
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( m × n 2 ) O(m \times n^2) </math>O(m×n2),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 分别代表给定 grid 的长宽
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.2304 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
sdgsdgdsgc13 小时前
Next.js企业级应用开发:SSR、ISR与性能监控方案
开发语言·前端·javascript
哲此一生98413 小时前
搭建Vue3工程(去除不必要的文件)
前端·javascript·vue.js
摇滚侠15 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
黑云压城After16 小时前
H5使用环信实现视频或语音通话
前端·javascript·vue.js
未来之窗软件服务17 小时前
自己写算法(九)网页数字动画函数——东方仙盟化神期
前端·javascript·算法·仙盟创梦ide·东方仙盟·东方仙盟算法
程序员小凯17 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友18 小时前
什么是断言?
前端·后端·安全
FIN666819 小时前
昂瑞微:实现精准突破,攻坚射频“卡脖子”难题
前端·人工智能·安全·前端框架·信息与通信
椎49519 小时前
苍穹外卖前端nginx错误之一解决
运维·前端·nginx
@。12419 小时前
对于灰度发布(金丝雀发布)的了解
开发语言·前端