算法:动态规划
算法动态规划概述动态规划Dynamic Programming, DP是一种解决多阶段决策最优化问题的算法思想。其产生的直接背景是传统递归分治在处理重叠子问题时存在大量重复计算如斐波那契数列复杂度 O(2ⁿ)。DP 通过保存已解决子问题的结果避免重复计算将指数级复杂度降为多项式级如 O(n²)。动态规划 暴力递归 备忘录记忆化 表格化顺序计算 状态压缩其中•动态决策过程是多阶段的当前决策会影响未来状态。•规划在给定约束下寻找最优方案。场景使用 DP 必须同时满足以下两个条件条件定义最优子结构原问题的最优解包含子问题的最优解重叠子问题不同阶段会反复使用相同的子问题解若不满足最优子结构如有环图最长路径DP 无效若不满足重叠子问题如快速排序DP 无性能收益。特点•空间换时间额外存储子问题解避免重复计算子问题减少时间复杂度。•保证全局最优只要问题满足最优子结构DP保证找到全局最优解。•可压缩性DP 表通常可降维滚动数组适合嵌入式内存受限环境。核心DP 的核心由四个要素组成1.状态用变量如dp[i]或dp[i][j]描述子问题。状态空间大小直接决定内存需求。2.状态转移方程描述当前状态与之前状态的关系如dp[i] f(dp[i-1], ...)。3.边界条件最小子问题的解作为递推的起始值。4.计算顺序确保计算当前状态时其依赖的状态已经完成。注意这里类似嵌入式中的状态机机制。分析按以下四步完成 DP 问题的分析1.定义状态明确dp[i]或dp[i][j]的含义尽量精简状态维度。2.推导转移方程分析当前状态与前面状态的关系写出数学表达式。3.确定边界找到递推起点如dp[0]、dp[n-1][j]等。4.确定计算顺序根据依赖关系决定从小到大、从大到小、从上到下等顺序。示例爬楼梯有n级台阶每次可以跨 1 级或 2 级。问爬到第n级总共有多少种不同的方法。分析• 状态dp[i]表示爬到第i级台阶的方法总数。i从 0 到 n。• 转移要到达第i级要么从第i-1级跨 1 级上来要么从第i-2级跨 2 级上来。dp[i] dp[i-1] dp[i-2]i ≥ 3)• 边界dp[0] 1站在地面1 种方法即不动dp[1] 1只能跨 1 级dp[2] 2可以跨 2 次每次跨 1 级或者跨 1 次跨 2 级• 顺序从第 1 级台阶向上迭代。代码/* * * 爬楼梯问题有 n 级台阶 * 每次可以跨 1 级或 2 级。问爬到第 n 级总共有多少种不同的方法 * n - 需要求解的最终台阶位置 * 返回爬到 n 级台阶的方法总和 * */ int climb_stairs(int n) { /* 状态定义dp[i] 表示爬到第 i 级台阶的方法总和 */ int i, dp[MAX_STAIR1] { 0 }; if (n MAX_STAIR - 1) return -1; /* 边界条件 */ dp[0] 1; /* 原地不动值并无什么实际意义 */ dp[1] 1; /* 上第一级台阶只有一种方法 */ dp[2] 2; /* 上第二级台阶可以分两次每次上一级也可一次上2级台阶*/ /* 状态转移方程dp[i] dp[i-1] dp[i-2] 到达i级台阶的方法从i-1级台阶跨1步或者从i-2级台阶跨2步 */ for (i3; in; i) { dp[i] dp[i-2] dp[i-1]; } return (dp[n]); }优化分析当前台阶只与前两级台阶的值有关从节省内存角度出发不考虑记录所有台阶的值只保存最新 2 级台阶的值即可。/* * * 爬楼梯问题优化当前台阶只与前两级台阶的值有关 * 从节省内存角度出发不考虑记录所有台阶的值只保存最新2级台阶的值即可 * n - 需要求解的最终台阶位置 * 返回爬到 n 级台阶的方法总和 * */ int climb_stairs_optimize(int n) { /* pre_2、pre_1 分别记录爬到当前台阶的前两级、前一级方法总和值 */ int i, pre_2, pre_1, curr; /* 边界条件 */ pre_2 1; /* 上第一级台阶只有一种方法 */ pre_1 2; /* 上第二级台阶可以分两次每次上一级也可一次上2级台阶*/ /* 状态转移方程curr pre_2 pre_1 */ for (i3; in; i) { curr pre_2 pre_1; /* 更新前两级台阶方法值为下一次计算做准备 */ pre_2 pre_1; pre_1 curr; } return (curr); }最大子数组和给定一个整数数组nums找出一个连续子数组至少包含一个元素使该子数组的和最大返回这个最大和。分析• 状态dp[i]表示以第i个元素结尾的连续子数组的最大和。• 转移对于nums[i]要么把它接到前面的子数组后面dp[i-1] nums[i]要么自己单独成为一个新子数组nums[i]。取较大者。dp[i] max(dp[i-1] nums[i], nums[i])• 边界dp[0] nums[0]以第一个元素结尾的子数组只能是它自己• 顺序从第 1 个元素向后迭代。代码/* * * 最大子数组和问题给定一个整数数组nums * 找出一个连续子数组至少包含一个元素使该子数组的和最大返回这个最大和。 * nums - 数据 n - 元素个数 * 返回最大连续子数组和 * */ int max_sub_array_sum(int *nums, int n) { /* 状态定义dp[i] 表示以第 i 个元素结尾的连续子数组的最大和 */ int i, max, dp[MAX_ARR_NUM1] { 0 }; if (n MAX_ARR_NUM - 1) return -1; /* 边界条件 */ dp[0] nums[0]; /* 第一个元素结尾的子链就是它自己 */ max dp[0]; /* 记录最大连续子数组和 */ /* 状态转移方程dp[i] MAX(dp[i-1]nums[i], nums[i]) 当前最大值取接上一个子数组或者单独成新子数组的最大值 */ for (i1; in; i) { dp[i] MAX(dp[i-1]nums[i], nums[i]); max MAX(dp[i], max); } return (max); }优化当前元素下的数组最大值只与上一个元素结尾子数组的最大值有关从节省内存角度出发不考虑记录所有元素结尾子数组的值只保存上一个元素结尾子数组最大值即可。/* * * 最大子数组和问题优化当前元素下的数组最大值只与上一个元素结尾子数组的最大值有关 * 从节省内存角度出发不考虑记录所有元素结尾子数组的值只保存上一个元素结尾子数组最大值即可 * nums - 数据 n - 元素个数 * 返回最大连续子数组和 * */ int max_sub_array_sum_optimize(int *nums, int n) { /* pre表示上一个元素最大值 */ int i, pre, max; /* 边界条件 */ pre nums[0]; /* 第一个元素就是当前最大值 */ max nums[0]; /* 记录最大子数组和的值 */ /* 状态转移方程curr pre_2 pre_1 */ for (i1; in; i) { pre MAX(prenums[i], nums[i]); max MAX(pre, max); } return (max); }不同路径一个机器人位于m x n网格的左上角起点(0,0)它每次只能向右或向下移动一步要到达右下角(m-1, n-1)。问总共有多少条不同的路径分析• 状态dp[i][j]表示从起点(0,0)走到格子(i,j)的不同路径数。• 转移要到达(i,j)只能从它的上方(i-1,j)向下走一步或者从左方(i,j-1)向右走一步。因此dp[i][j] dp[i-1][j] dp[i][j-1]当i0且j0• 边界第一行(0,j)只能一直向右所以dp[0][j] 1第一列(i,0)只能一直向下所以dp[i][0] 1dp[0][0] 1• 顺序从第 00出发迭代。代码/* * * 不同路径问题一个机器人位于m x n网格的左上角起点 (0,0) * 它每次只能向右或向下移动一步要到达右下角 (m-1, n-1)。问总共有多少条不同的路径 * m - 网格行数 n - 网格列数 * 返回不同路径数量总和 * */ int unique_paths(int m, int n) { /* 状态定义dp[i][j] 表示到达格子(i,j)有多少条路径 */ int i, j, dp[MAX_ARR_NUM1][MAX_ARR_NUM1] { {0} }; if ((n MAX_ARR_NUM - 1) || (m MAX_ARR_NUM - 1)) return -1; /* 边界条件,第 1 行和第 1 列都只有一种路径 */ for (i0; im; i) dp[i][0] 1; for (i0; in; i) dp[0][i] 1; /* 状态转移方程dp[i][j] dp[i-1][j] dp[i][j-1]; 要到达当前格子的方法到达上边格子的方法到达右边格子的方法 */ for (i1; im; i) { for (j1; jn; j) { dp[i][j] dp[i-1][j] dp[i][j-1]; } } return (dp[m-1][n-1]); }优化将二维数组压缩成一维逐行计算各个格子不同路径总数此时计算(i, j)格子时(i, j-1)的格子左边已更新未更新的(i, j)实际存储为上面的格子的路径数。/* * * 不同路径问题优化将二维数组压缩成一维逐行计算各个格子不同路径总数 * 此时计算(i,j)格子时(i,j-1)的格子左边已更新未更新的(i,j)实际存储为上面的格子的路径数 * m - 网格行数 n - 网格列数 * 返回不同路径数量总和 * */ int unique_paths_optimize(int m, int n) { /* 状态定义dp[i] 表示到达指定格子有多少条路径每次存储一行的数据 */ int i, j, dp[MAX_ARR_NUM1] {0}; if (n MAX_ARR_NUM - 1) return -1; /* 边界条件,第 1 行只有一种路径 */ for (j0; jn; j) dp[j] 1; /* 状态转移方程dp[j] dp[j-1](右边) dp[j]上边; 要到达当前格子的方法到达上边格子的方法到达右边格子的方法 */ for (i1; im; i) { for (j1; jn; j) { dp[j] dp[j-1] dp[j]; } } return (dp[n-1]); }0-1 背包问题有N件物品和一个容量为W的背包。第i件物品的重量是wt[i]价值是val[i]。每件物品只能选一次0-1 背包。求在不超过背包容量的前提下能装入的最大总价值。分析• 状态dp[i][j]表示考虑前i件物品索引 1~i背包容量为j时能获得的最大价值。• 转移对于第i件物品重量wt[i-1]价值val[i-1]有两种选择取两者最大值dp[i][j] max(dp[i-1][j], dp[i-1][j - wt[i-1]] val[i-1])当j ≥ wt[i-1]不装dp[i][j] dp[i-1][j]装前提j ≥ wt[i-1]dp[i][j] dp[i-1][j - wt[i-1]] val[i-1]• 边界第一行没有物品可选价值为0。所以dp[0][j] 0第一列容量为0价值为0。所以dp[i][0] 0• 顺序从dp[1][1]开始顺序迭代。代码/* * * 0-1背包问题有N件物品和一个容量为W的背包。第i件物品的重量是wt[i]价值是val[i]。 * 每件物品只能选一次0-1 背包。求在不超过背包容量的前提下能装入的最大总价值。 * W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值 * 返回最大总价值 * */ int knapsack(int W, int wt[], int N, int val[]) { /* 状态定义dp[i][j] 表示考虑前 i 件物品重量不超过 j 的最大价值 */ int i, j, dp[MAX_N1][MAX_C1] { {0} }; if ((N MAX_N - 1) || (W MAX_C - 1)) return -1; /* 边界条件,背包重量限制为0时或者背包没有物品时价值均为0 */ for (i0; iN; i) dp[i][0] 0; for (j0; jW; j) dp[0][j] 0; /* 状态转移方程dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]) 面对第 i 件物品(wt[i-1], val[i-1])取不装dp[i-1][j]或者装dp[i-1][j-wt[i-1]]val[i-1]的最大值 */ for (i1; iN; i) { for (jW; jwt[i-1]; j--) dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]); } return (dp[N][W]); }优化观察计算dp[i][j]时只用到了第 i-1 行的dp[i-1][j]和dp[i-1][j - wt[i-1]]。也就是只依赖上一行的数据不会用到更早的行。所以我们只需要保留一行当前正在计算的行就够了。注意当你计算dp[j]时右边的dp[j - wt[i]]可能是已经更新过的因为容量小先算。如果dp[j - wt[i]]已经包含了当前物品i那么dp[j]就会再次尝试加入物品i相当于同一物品可以用多次这就变成了完全背包而不是 0-1 背包每个物品只能用一次。所以我们只能倒叙遍历保证j - wt[i-1]更新在j更新之后。/* * * 0-1背包问题优化观察上述状态转移方程dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]) * 计算dp[i][j]时只用到了第i-1行的dp[i-1][j]和dp[i-1][j - wt[i-1]]。 * 即只依赖上一行的数据不会用到更早的行。所以我们只需要保留一行当前正在计算的行就够了。 * 注意j-wt[i-1] j,如果顺序遍历计算j重量的时候j - wt[i-1]必然已经更新过此时会变成完全背包。 * 所以我们只能倒叙遍历保证j - wt[i-1]更新在j更新之后。 * W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值 * 返回最大总价值 * */ int knapsack_optimize(int W, int wt[], int N, int val[]) { /* 状态定义dp[i] 表示考虑单个物品不同重量下的最大价值 */ int i, j, dp[MAX_C1] {0}; if (W MAX_C - 1) return -1; /* 边界条件,背包没有物品时价值为0 */ for (i0; iW; i) dp[i] 0; /* 状态转移方程dp[j] MAX(dp[j], dp[j-wt[i-1]] val[i-1]); 商品最大价值 取装与不装分别获取的价值的最大值 */ for (i1; iN; i) { for (jW; jwt[i-1]; j--) dp[j] MAX(dp[j], dp[j-wt[i-1]] val[i-1]); } return (dp[W]); }总结动态规划是一种“填表”算法它把大问题拆成一系列相互依赖的子问题自底向上或自顶向下带备忘录依次计算出每个子问题的答案最终组合出原问题的解。它的时间复杂度通常与状态数量呈多项式关系远优于指数级的暴力递归。DP 是解决重叠子问题最优化问题的标准方法核心四要素状态、转移方程、边界、顺序。步骤要回答的问题1. 状态用几个变量维度能描述子问题如dp[i]或dp[i][j]2. 转移当前状态 前面某些状态的某种组合、max、min等3. 边界边界i0、j0、容量0 时的值是什么4. 顺序最终确定递推方向从下往上从每行左到右?注意计算顺序不唯一。你也可以从上往下但状态定义和转移方程会变比如定义为从顶部走到(i,j)的最大和。两种顺序都可以但边界不同。完整代码/** * Filename : study_dp.c * Revision : $Revision: 1.00 $ * Author : Feng(更多编程相关的知识和源码见微信公众号不只会拍照的程序猿欢迎订阅) * Description : 动态规划学习 **/ #include stdio.h #include string.h #define MAX_STAIR 30 /* 台阶最大值 */ #define MAX_ARR_NUM 30 /* 数组最大值 */ #define MAX_N 10 /* 最大物品数量 */ #define MAX_C 30 /* 最大背包重量 */ #define MAX(x, y) (x) (y) ? (x) : (y) #define MIN(x, y) (x) (y) ? (x) : (y) #define ARR_SIZE(x) sizeof(x)/sizeof(x[0]) /* * * 爬楼梯问题有 n 级台阶 * 每次可以跨 1 级或 2 级。问爬到第 n 级总共有多少种不同的方法 * n - 需要求解的最终台阶位置 * 返回爬到 n 级台阶的方法总和 * */ int climb_stairs(int n) { /* 状态定义dp[i] 表示爬到第 i 级台阶的方法总和 */ int i, dp[MAX_STAIR1] { 0 }; if (n MAX_STAIR - 1) return -1; /* 边界条件 */ dp[0] 1; /* 原地不动值并无什么实际意义 */ dp[1] 1; /* 上第一级台阶只有一种方法 */ dp[2] 2; /* 上第二级台阶可以分两次每次上一级也可一次上2级台阶*/ /* 状态转移方程dp[i] dp[i-1] dp[i-2] 到达i级台阶的方法从i-1级台阶跨1步或者从i-2级台阶跨2步 */ for (i3; in; i) { dp[i] dp[i-2] dp[i-1]; } return (dp[n]); } /* * * 爬楼梯问题优化当前台阶只与前两级台阶的值有关 * 从节省内存角度出发不考虑记录所有台阶的值只保存最新2级台阶的值即可 * n - 需要求解的最终台阶位置 * 返回爬到 n 级台阶的方法总和 * */ int climb_stairs_optimize(int n) { /* pre_2、pre_1 分别记录爬到当前台阶的前两级、前一级方法总和值 */ int i, pre_2, pre_1, curr; /* 边界条件 */ pre_2 1; /* 上第一级台阶只有一种方法 */ pre_1 2; /* 上第二级台阶可以分两次每次上一级也可一次上2级台阶*/ /* 状态转移方程curr pre_2 pre_1 */ for (i3; in; i) { curr pre_2 pre_1; /* 更新前两级台阶方法值为下一次计算做准备 */ pre_2 pre_1; pre_1 curr; } return (curr); } /* * * 最大子数组和问题给定一个整数数组nums * 找出一个连续子数组至少包含一个元素使该子数组的和最大返回这个最大和。 * nums - 数据 n - 元素个数 * 返回最大连续子数组和 * */ int max_sub_array_sum(int *nums, int n) { /* 状态定义dp[i] 表示以第 i 个元素结尾的连续子数组的最大和 */ int i, max, dp[MAX_ARR_NUM1] { 0 }; if (n MAX_ARR_NUM - 1) return -1; /* 边界条件 */ dp[0] nums[0]; /* 第一个元素结尾的子链就是它自己 */ max dp[0]; /* 记录最大连续子数组和 */ /* 状态转移方程dp[i] MAX(dp[i-1]nums[i], nums[i]) 当前最大值取接上一个子数组或者单独成新子数组的最大值 */ for (i1; in; i) { dp[i] MAX(dp[i-1]nums[i], nums[i]); max MAX(dp[i], max); } return (max); } /* * * 最大子数组和问题优化当前元素下的数组最大值只与上一个元素结尾子数组的最大值有关 * 从节省内存角度出发不考虑记录所有元素结尾子数组的值只保存上一个元素结尾子数组最大值即可 * nums - 数据 n - 元素个数 * 返回最大连续子数组和 * */ int max_sub_array_sum_optimize(int *nums, int n) { /* pre表示上一个元素最大值 */ int i, pre, max; /* 边界条件 */ pre nums[0]; /* 第一个元素就是当前最大值 */ max nums[0]; /* 记录最大子数组和的值 */ /* 状态转移方程curr pre_2 pre_1 */ for (i1; in; i) { pre MAX(prenums[i], nums[i]); max MAX(pre, max); } return (max); } /* * * 不同路径问题一个机器人位于m x n网格的左上角起点 (0,0) * 它每次只能向右或向下移动一步要到达右下角 (m-1, n-1)。问总共有多少条不同的路径 * m - 网格行数 n - 网格列数 * 返回不同路径数量总和 * */ int unique_paths(int m, int n) { /* 状态定义dp[i][j] 表示到达格子(i,j)有多少条路径 */ int i, j, dp[MAX_ARR_NUM1][MAX_ARR_NUM1] { {0} }; if ((n MAX_ARR_NUM - 1) || (m MAX_ARR_NUM - 1)) return -1; /* 边界条件,第 1 行和第 1 列都只有一种路径 */ for (i0; im; i) dp[i][0] 1; for (i0; in; i) dp[0][i] 1; /* 状态转移方程dp[i][j] dp[i-1][j] dp[i][j-1]; 要到达当前格子的方法到达上边格子的方法到达右边格子的方法 */ for (i1; im; i) { for (j1; jn; j) { dp[i][j] dp[i-1][j] dp[i][j-1]; } } return (dp[m-1][n-1]); } /* * * 不同路径问题优化将二维数组压缩成一维逐行计算各个格子不同路径总数 * 此时计算(i,j)格子时(i,j-1)的格子左边已更新未更新的(i,j)实际存储为上面的格子的路径数 * m - 网格行数 n - 网格列数 * 返回不同路径数量总和 * */ int unique_paths_optimize(int m, int n) { /* 状态定义dp[i] 表示到达指定格子有多少条路径每次存储一行的数据 */ int i, j, dp[MAX_ARR_NUM1] {0}; if (n MAX_ARR_NUM - 1) return -1; /* 边界条件,第 1 行只有一种路径 */ for (j0; jn; j) dp[j] 1; /* 状态转移方程dp[j] dp[j-1](右边) dp[j]上边; 要到达当前格子的方法到达上边格子的方法到达右边格子的方法 */ for (i1; im; i) { for (j1; jn; j) { dp[j] dp[j-1] dp[j]; } } return (dp[n-1]); } /* * * 0-1背包问题有N件物品和一个容量为W的背包。第i件物品的重量是wt[i]价值是val[i]。 * 每件物品只能选一次0-1 背包。求在不超过背包容量的前提下能装入的最大总价值。 * W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值 * 返回最大总价值 * */ int knapsack(int W, int wt[], int N, int val[]) { /* 状态定义dp[i][j] 表示考虑前 i 件物品重量不超过 j 的最大价值 */ int i, j, dp[MAX_N1][MAX_C1] { {0} }; if ((N MAX_N - 1) || (W MAX_C - 1)) return -1; /* 边界条件,背包重量限制为0时或者背包没有物品时价值均为0 */ for (i0; iN; i) dp[i][0] 0; for (j0; jW; j) dp[0][j] 0; /* 状态转移方程dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]) 面对第 i 件物品(wt[i-1], val[i-1])取不装dp[i-1][j]或者装dp[i-1][j-wt[i-1]]val[i-1]的最大值 */ for (i1; iN; i) { for (jW; jwt[i-1]; j--) dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]); } return (dp[N][W]); } /* * * 0-1背包问题优化观察上述状态转移方程dp[i][j] MAX(dp[i-1][j], dp[i-1][j-wt[i-1]] val[i-1]) * 计算dp[i][j]时只用到了第i-1行的dp[i-1][j]和dp[i-1][j - wt[i-1]]。 * 即只依赖上一行的数据不会用到更早的行。所以我们只需要保留一行当前正在计算的行就够了。 * 注意j-wt[i-1] j,如果顺序遍历计算j重量的时候j - wt[i-1]必然已经更新过此时会变成完全背包。 * 所以我们只能倒叙遍历保证j - wt[i-1]更新在j更新之后。 * W - 容量上限 wt - 物品重量 N - 物品数 val - 物品价值 * 返回最大总价值 * */ int knapsack_optimize(int W, int wt[], int N, int val[]) { /* 状态定义dp[i] 表示考虑单个物品不同重量下的最大价值 */ int i, j, dp[MAX_C1] {0}; if (W MAX_C - 1) return -1; /* 边界条件,背包没有物品时价值为0 */ for (i0; iW; i) dp[i] 0; /* 状态转移方程dp[j] MAX(dp[j], dp[j-wt[i-1]] val[i-1]); 商品最大价值 取装与不装分别获取的价值的最大值 */ for (i1; iN; i) { for (jW; jwt[i-1]; j--) dp[j] MAX(dp[j], dp[j-wt[i-1]] val[i-1]); } return (dp[W]); } int main(void) { int n 11; int r 3, c 7; int nums[] {-2, 1, -3, 4, -1, 2, 1, -5, 4}; int W 10; // 背包容量 int wt[] {2, 3, 4, 5}; // 物品重量 int val[] {3, 4, 5, 6}; // 物品价值 int N ARR_SIZE(wt); int size ARR_SIZE(nums); printf(爬 %d 级台阶的方法数: %d\n, n, climb_stairs(n)); printf(爬 %d 级台阶的方法数优化版本: %d\n, n, climb_stairs_optimize(n)); printf(最大子数组和: %d\n, max_sub_array_sum(nums, size)); printf(最大子数组和优化版本: %d\n, max_sub_array_sum_optimize(nums, size)); printf(%dx%d 网格的不同路径数: %d\n, r, c, unique_paths(r, c)); printf(%dx%d 网格的不同路径数优化版本: %d\n, r, c, unique_paths_optimize(r, c)); printf(最大背包价值: %d\n, knapsack(W, wt, N, val)); printf(最大背包价值优化版本: %d\n, knapsack_optimize(W, wt, N, val)); return 0; }运行结果

相关新闻

买AR远程协作眼镜怎么避免踩坑

买AR远程协作眼镜怎么避免踩坑

在工业4.0和数字化转型的浪潮下,AR(增强现实)远程协作系统正从“概念演示”走向“生产实战”。对于企业技术决策者和架构师而言,采购AR硬件与软件系统不再仅仅是购买一套通讯工具,而是引入一套复杂的工业AR远程协作系统…

2026/7/31 14:42:09阅读更多 →
XCOM 2模组管理终极指南:用AML启动器告别游戏崩溃烦恼

XCOM 2模组管理终极指南:用AML启动器告别游戏崩溃烦恼

XCOM 2模组管理终极指南:用AML启动器告别游戏崩溃烦恼 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/x…

2026/7/31 14:40:08阅读更多 →
SpringBoot+Three.js构建元宇宙房屋租赁系统实践

SpringBoot+Three.js构建元宇宙房屋租赁系统实践

1. 项目背景与核心价值元宇宙房屋租赁管理系统是当前数字化转型浪潮下的典型应用场景。这个毕业设计项目巧妙地将传统房屋租赁业务与元宇宙概念相结合,通过SpringBoot技术栈实现了一套完整的解决方案。我在实际开发过程中发现,这种融合了虚拟现实技术的租…

2026/7/31 14:40:08阅读更多 →
AutoCAD2013完整安装教程:从下载到激活的详细步骤与常见问题解决

AutoCAD2013完整安装教程:从下载到激活的详细步骤与常见问题解决

最近在整理CAD学习资料时,发现很多初学者在安装AutoCAD2013时遇到各种问题,从下载资源找不到、安装过程报错到激活失败,每个环节都可能让人头疼。本文基于实际安装经验,整理一套完整的AutoCAD2013安装教程,包含详细的步…

2026/7/31 17:11:19阅读更多 →
科技金融热点下的开发者指南:从贵金属到自研芯片的技术关联

科技金融热点下的开发者指南:从贵金属到自研芯片的技术关联

最近在跟踪全球科技和金融市场动态时,发现不少开发者对贵金属期货、商业航天、芯片自研等领域的关联性很感兴趣。特别是当这些宏观趋势与具体的技术投资、企业战略相结合时,往往会直接影响我们关注的开发方向、技术选型甚至就业选择。本文将围绕近期几大…

2026/7/31 17:11:19阅读更多 →
文心一言长文写作实战突围:24小时极速交付SOP(含金融/法律/教育三大垂直领域定制框架)

文心一言长文写作实战突围:24小时极速交付SOP(含金融/法律/教育三大垂直领域定制框架)

更多请点击: https://kaifayun.com 第一章:文心一言长文写作实战突围:24小时极速交付SOP(含金融/法律/教育三大垂直领域定制框架) 面对高强度内容交付压力,传统人工撰写模式在金融研报、法律尽调文书与教…

2026/7/31 17:11:19阅读更多 →
智能车竞赛图像处理:从硬件选型到算法优化的全链路实践

智能车竞赛图像处理:从硬件选型到算法优化的全链路实践

1. 项目概述与核心价值 “图像”这两个字,在智能车竞赛的四轮车组别里,分量有多重?我干了这么多年,可以很负责任地说,它几乎决定了你成绩的上限。这不仅仅是摄像头拍个画面那么简单,它是一整套从物理世界的…

2026/7/31 17:11:19阅读更多 →
百考通AI:开题报告一键生成,让学术研究起步更从容,覆盖全场景需求

百考通AI:开题报告一键生成,让学术研究起步更从容,覆盖全场景需求

开题报告是学术写作的第一步,也是决定论文方向与质量的关键环节。从选题定题到框架搭建,从梳理研究背景到规划研究方法,繁琐的流程常常让专科、本科及研究生们倍感压力。百考通AI(https://www.baikaotongai.com)凭借智…

2026/7/31 17:11:19阅读更多 →
Ozlo Sleepbuds 2评测:舒适助眠体验佳,但睡眠追踪准确性待提升!

Ozlo Sleepbuds 2评测:舒适助眠体验佳,但睡眠追踪准确性待提升!

优缺点分析Ozlo Sleepbuds 2有优点也有缺点。优点方面,它佩戴极为舒适,能有效屏蔽大部分干扰噪音,每次佩戴都能稳固地留在耳朵里,配套应用程序也简单易用。缺点是价格昂贵,睡眠数据加载耗时较长,睡眠阶段分…

2026/7/31 17:09:18阅读更多 →
覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

覆盖国产 + 海外 + 开源模型,OpenClaw 2.7.9 Windows/Mac 双端部署详解

🔹 工具基础介绍 OpenClaw 是开源生态中一款实用性较强的本地智能工具,凭借本地离线运行、可视化图形操作和任务自动化三大核心特性,赢得了众多用户的青睐。与普通在线对话AI工具不同,它属于能够直接操控本机软硬件的智能数字员工…

2026/7/30 15:03:16阅读更多 →
伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

伺服阀焊完微漏毁整机?精密激光焊接三关锁住高压

所谓液压伺服阀体的精密激光焊接,是用激光束对阀座壳体(通常为不锈钢或铝合金)进行密封焊接,使阀体在21-35MPa的高压液压油或压缩气体中长期运行而不发生介质泄漏。液压伺服阀是高端液压系统的"大脑"。从航空航天飞行控…

2026/7/30 12:22:27阅读更多 →
D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南

D2DX:三步实现《暗黑破坏神2》高清宽屏体验的终极指南 【免费下载链接】d2dx D2DX is a complete solution to make Diablo II run well on modern PCs, with high fps and better resolutions. 项目地址: https://gitcode.com/gh_mirrors/d2/d2dx 你是否还在…

2026/7/30 15:13:02阅读更多 →
物理复制比逻辑复制好在哪?数据库复制原理详解

物理复制比逻辑复制好在哪?数据库复制原理详解

数据库复制是把主库数据同步到备库的机制,分为逻辑复制和物理复制两种。逻辑复制传输的是 SQL 语句或行变更事件,物理复制传输的是存储引擎底层的物理日志。阿里云 PolarDB(云原生数据库)采用物理复制,在同步延迟、数据…

2026/7/31 0:00:40阅读更多 →
BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown:3分钟学会B站视频下载的终极指南

BilibiliDown:3分钟学会B站视频下载的终极指南 【免费下载链接】BilibiliDown (GUI-多平台支持) B站 哔哩哔哩 视频下载器。支持稍后再看、收藏夹、UP主视频批量下载|Bilibili Video Downloader 😳 项目地址: https://gitcode.com/gh_mirrors/bi/Bilib…

2026/7/31 0:00:41阅读更多 →
有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

有哪些游戏数据AI平台?游戏行业Data+AI融合方案盘点

当前,游戏行业的“DataAI融合”已从概念验证进入价值落地阶段。根据IDC 2025年数据,中国AI游戏云市场规模已达18.6亿元;同时,游戏研发环节AI渗透率高达86%,生成式AI内容普及率超过50%。面对庞大的市场,游戏…

2026/7/31 0:00:41阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/31 0:49:33阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/31 5:08:18阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/31 16:02:17阅读更多 →