news 2026/10/23 8:31:02

从回溯到状态压缩DP:校园自行车分配最小总距离问题详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从回溯到状态压缩DP:校园自行车分配最小总距离问题详解

看到 LintCode 3666 的assignBikesII(int[][] workers, int[][] bikes)这道题,很多人的第一反应是:上一个校园自行车分配不是用优先队列把所有“工人-自行车”距离对排序,然后逐个挑就行了么?这个第二版能难到哪去?等到真正提交时才发现,它要求的是“最小总距离”,本质上是个二分图最优匹配变体,需要从回溯、记忆化搜索一路升级到状态压缩 DP,才算把这题吃透。这篇文章我会用 Java 完整拆一遍三种实现,并且把为什么贪心必挂、状态压缩的 mask 到底怎么设计、以及面试时怎么把这个题讲出层次感,一次性说清楚。

1. 题目到底在算什么:别被“校园自行车”六个字带偏

1.1 把题目翻译成数学语言

这道题的场景很具象:校园里有若干个工人,每个工人有坐标;还有若干个自行车停放点,每个自行车也有坐标。一个工人只能骑一辆车,每辆车也只能被一个工人使用。要求“每位工人都被分配一辆自行车”,同时让所有人走到自己那辆车的曼哈顿距离之和最小。

先看曼哈顿距离怎么算,这是第一层送分点:

// 两个点 (x1, y1) 和 (x2, y2) 的曼哈顿距离 int dist = Math.abs(x1 - x2) + Math.abs(y1 - y2);

为什么不直接用欧氏距离?因为题目只允许上下左右移动,实际路径就是横向差加纵向差,曼哈顿距离才是真实代价,不是直线距离。

如果把工人编号为0 ~ n-1,自行车编号为0 ~ m-1,那我们要做的就是从m辆车里挑出n辆出来,和n个工人构成一个一一映射。设工人i分配到自行车bikeOf[i],目标函数是:

min sum( dist(workers[i], bikes[bikeOf[i]]) ), i = 0..n-1

约束条件是:

  • 每个工人都分配到一辆自行车。
  • 同一辆自行车不能被分配给两个工人。
  • 通常保证m >= n,自行车数量不少于工人数量。

到这一步,题目已经脱离了“按距离排序挑一挑”的模拟题范畴,变成一个组合优化问题。你要在庞大的分配方案集合里搜索代价最小的那一个。

1.2 为什么贪心不成立:亲手画一个 5 分钟反例

如果你以前刷过另一个校园自行车分配题,可能见过“按所有工人自行车的距离排序,从小到大优先分配”的做法。很多第一版题目确实可以贪心通过,但到了assignBikesII,这个套路一定会翻车,因为题目目标明确定义成“全局总距离最小”。

我举个非常小的反例,你画在纸上立刻能懂。

三个坐标点:

工人 W0 = (0, 0) 工人 W1 = (5, 0) 自行车 B0 = (5, 0) 自行车 B1 = (-100, 0)

距离矩阵如下:

距离B0B1
W05100
W10105

如果按“从 W0 开始,给每个工人挑一辆当前离他最近、且没被占用”的思路,W0 会看到 B0 距离是 5,B1 距离是 100,于是把 B0 拿走。W1 只能骑 B1,距离 105。总代价是 5 + 105 = 110。

但最优方案是这样:W0 骑 B1,代价 100;W1 骑 B0,代价 0。总代价是 100,比 110 更小。

这个例子说明了什么?B0 虽然离 W0 不算远,但它离 W1 是 0,是 W1 的“专属最优解”。W0 把 B0 抢走,只给自己省了很少的距离,却让 W1 付出了惨痛代价。局部最优的选择叠加起来,不一定是全局最优。

所以这道题别浪费时间证明什么贪心性质了,直接用搜索或者 DP 枚举所有合法分配方案。

1.3 动手写代码前,先把这几个边界条件问清楚

刷题最怕的是看题看一半就写,写完了才发现自己的版本和评测数据对不上。做assignBikesII之前,建议先对着题目确认四件事:

  • workers.length和bikes.length的取值范围。通常很小,可能只有 10 以内,否则状态压缩 DP 的2^m状态是扛不住的。
  • 是不是保证bikes.length >= workers.length。如果自行车比工人少,那“每个工人都分到车”的约束本身就不成立,得换问法。
  • 距离计算的单位是曼哈顿距离,不是欧氏距离,千万别在坐标里直接调Math.hypot。
  • 题目要求返回值是int,那么距离累加不会溢出吗?坐标绝对值通常不会太大,int够用;但如果你在练习时自己改大了数据范围,建议改用long兜底。

我见过不少人在这些地方翻车:有人把自行车数量和工人数量搞反,导致数组越界;有人在状态转移里忘记车少人多的情况;还有人距离矩阵计算错了下标,样例过了但大样例全挂。这些都算不上算法不会,纯粹是细节没锁死。

2. 第一版能跑的方案:回溯 + 剪枝是面试保底答案

2.1 回溯的决策树到底长什么样

先别急着上状态压缩 DP,因为面试时你大概率要先用一个最朴素的方案证明自己理解了问题。

回溯的本质是:从工人 0 开始,一辆车一辆车试。工人 0 可以尝试任意一辆没被占用的自行车;选定之后,工人 1 再从剩余自行车里选一辆……直到所有工人都分配完,记录当前方案的总距离,并和全局最优值比较。

当有n个工人、m辆自行车时,每一个合法分配方案就是从m辆车里按顺序选出n辆,并和工人一一配对。方案的数量的上限大约是:

P(m, n) = m! / (m - n)!

如果n = m = 9,那就是 362880 种方案。这个数量级在面试题里可以直接用回溯跑出来。如果n = m = 10,是 3628800 种,纯回溯也勉强能跑,但是加上剪枝会更稳。

这里的决策树可以理解为:每一层对应一个工人,每个节点要去枚举“该工人能骑哪一辆还没被用的自行车”。

2.2 Java 回溯实现

需要一个visited数组或者位掩码来记录哪些自行车已经被占用了。点不多的情况下,我更推荐用位掩码,因为和后面的状态压缩 DP 顺路。

public int assignBikesII(int[][] workers, int[][] bikes) { int n = workers.length; int[] ans = { Integer.MAX_VALUE }; dfs(0, 0, 0, workers, bikes, ans); return ans[0]; } private void dfs(int workerIdx, int bikeMask, int curCost, int[][] workers, int[][] bikes, int[] ans) { if (curCost >= ans[0]) { return; // 剪枝:当前花费已经不可能成为最优 } if (workerIdx == workers.length) { ans[0] = Math.min(ans[0], curCost); return; } for (int j = 0; j < bikes.length; j++) { if ((bikeMask & (1 << j)) != 0) { continue; // 这辆车已经被占用 } int d = Math.abs(workers[workerIdx][0] - bikes[j][0]) + Math.abs(workers[workerIdx][1] - bikes[j][1]); dfs(workerIdx + 1, bikeMask | (1 << j), curCost + d, workers, bikes, ans); } }

这段代码最需要理解的是bikeMask的用法。bikeMask是一个整数,它的第j位如果为 1,就表示编号为j的自行车已经被前面的工人选走了;如果为 0,说明空闲。

判断是否占用:(bikeMask & (1 << j)) != 0。

标记占用:bikeMask | (1 << j)。

这种用一个整数的二进制位来记录布尔数组状态的技巧,就是“状态压缩”。你现在就可以在回溯版本里先体会一下,后面 DP 版本本质上是对它的升级。

2.3 剪枝条件不是越多越好

上面代码里只有一个剪枝:当curCost >= ans[0]就无条件返回。这个剪枝在数据随机时效果非常明显,因为一旦找到一个比较小的可行解,很多较差的搜索路径都会被提前掐断。

但要注意它的效果高度依赖“找到好解的速度”。如果你运气差,第一组完整方案就是 10000,那么后续上万条路径在超过 10000 才会被剪掉,效果有限。更稳定的做法是优先按成本低的自行车去尝试,也就是在枚举前先把当前工人的自行车按距离从小到大排序,让更优秀的解更早出现。

不过排序需要花费额外时间和空间,而且回溯版本在真正的大测验数据下依然会超时,所以它更适合作为“我理解这个问题”的面试第一版,而不是作为最终答案。

2.4 回溯的实际瓶颈:重复子问题没有复用

如果你只是背模板,可能看不出来回溯的问题在哪。我举个例子:

工人有 3 个,自行车有 5 辆。某个时刻,我们已经给工人 0 和工人 1 分配了自行车 {B0, B3},现在要给工人 2 分配;另一个搜索路径上,工人 0 和工人 1 分配的是 {B3, B0}。虽然顺序反了,但剩余可用的自行车集合完全一样,都是 {B1, B2, B4},而且工人编号都是 2。

这意味着这两个搜索分支后续要处理的问题完全相同,但回溯会傻傻地再算一遍。状态压缩 DP 的目标,就是把这个重复计算给消掉。

3. 进阶状态压缩 DP:把“可用车集合”当作状态

3.1 状态设计的核心意象:每辆车只有被占用和空闲两种状态

状态压缩 DP 的第一步,仍然是选定一个能准确描述“当前局面”的东西。

在回溯里,我们需要两个信息:当前处理到第几个工人了、目前哪些自行车已经被占用了。其实第二个信息拿到后,第一个信息是可以推导出来的——已经分配掉的工人数量,一定等于自行车占用数量。所以一个bikeMask就足够描述状态。

我们定义:

dp[mask] = 已经用 mask 对应的自行车集合,分配给前 k 个工人(k = mask 中 1 的个数)时,前 k 个工人的最小总距离。

举个例子:mask = 0b101,二进制低 3 位是 101,表示编号 0 和编号 2 的自行车已经被占用。此时1的个数为 2,也就是已经有 2 个工人被分配完毕,下一个待分配工人下标是 2。

再深入理解一下这个问题为什么天然适合 DP:由于所有工人的坐标是固定的,任意一个“已使用自行车集合”和“即将开始分配的第 k 个工人”唯一决定了一个子问题。后面无论选择哪辆车,都会进入一个新的子问题;而子问题之间不是树形的,而是图状的,存在大量交汇,所以可以缓存。

3.2 状态转移方程

假设当前已经分配了前k个工人,其中k = bitCount(mask)。现在尝试把编号为j的空闲自行车分配给工人k,转移后的状态是:

newMask = mask | (1 << j)

代价是:

dp[newMask] = min(dp[newMask], dp[mask] + dist(workers[k], bikes[j]))

注意这里有一个隐藏技巧:mask中 1 的数量正好等于“已经分配好的工人个数”,所以下一轮要坐下一个是第几个工人,由Integer.bitCount(mask)直接得到,不用再传单独的工人下标。

如果自行车数量等于工人数量,最终答案是dp[(1 << bikes.length) - 1],也就是说所有自行车都被使用了。但在实际题里,自行车数量通常可能大于工人数量,所以最终答案要从“恰好用了 n 辆车的状态”里取最小值,而不是傻傻返回所有车都用完的那个状态。

3.3 Java 状态压缩 DP 实现

public int assignBikesII(int[][] workers, int[][] bikes) { int n = workers.length; int m = bikes.length; // dist[i][j]: 工人 i 到自行车 j 的曼哈顿距离 int[][] dist = new int[n][m]; for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { dist[i][j] = Math.abs(workers[i][0] - bikes[j][0]) + Math.abs(workers[i][1] - bikes[j][1]); } } int totalStates = 1 << m; int INF = Integer.MAX_VALUE / 2; int[] dp = new int[totalStates]; Arrays.fill(dp, INF); dp[0] = 0; // 一辆车都没用,0 个工人已分配,代价 0 for (int mask = 0; mask < totalStates; mask++) { int workerIdx = Integer.bitCount(mask); if (workerIdx >= n) { // 所有工人都已经分配完毕,不能再继续加车了 continue; } // 考虑当前这个工人 workerIdx 可以骑哪一辆空闲自行车 for (int j = 0; j < m; j++) { if ((mask & (1 << j)) != 0) { continue; // 车 j 已被占用 } int nextMask = mask | (1 << j); dp[nextMask] = Math.min(dp[nextMask], dp[mask] + dist[workerIdx][j]); } } // 自行车数量可能多于工人数量,找“恰好分配了 n 辆车”的最小值 int ans = INF; for (int mask = 0; mask < totalStates; mask++) { if (Integer.bitCount(mask) == n) { ans = Math.min(ans, dp[mask]); } } return ans; }

简单说下为什么遍历mask从 0 到totalStates - 1是安全的:每一次转移都会把一个 0 位改成 1,二进制对应整数会变大,所以nextMask一定大于当前mask。从小到大遍历,保证更新某个状态时,它所有可能的前驱状态都已经被计算好了。

这个版本的复杂度很好估算。状态数大约2^m,每个状态尝试的车辆数不超过m,所以渐进复杂度是O(m * 2^m),空间复杂度O(2^m)。当m <= 10时,状态数只有 1024,完全可以接受。

3.4 一个最容易忽略的坑:final ans为什么不是dp[全1]

我见过很多同学把状态压缩 DP 写完后,直接返回dp[(1 << m) - 1],在m == n的测试用例上没问题,但一旦题目改成自行车比工人多,就会全错。

为什么?因为“所有自行车都占用”意味着你已经给超过工人数量的自行车都分配了人,这是不可能的。只要自行车比工人多,一定有一些自行车最终空闲。

所以更通用、更稳的收尾方式是:对所有二进制中 1 的个数恰好等于工人数的状态取最小值。

int ans = Integer.MAX_VALUE; for (int mask = 0; mask < totalStates; mask++) { if (Integer.bitCount(mask) == workers.length) { ans = Math.min(ans, dp[mask]); } } return ans;

如果你确定这道题保证n == m,直接返回dp[(1 << n) - 1]也不是不行,但我还是建议写成通用版本,因为这才是这个状态定义的完整语义。

4. 记忆化搜索:比背递推公式更不容易写错的方案

4.1 从回溯到记忆化,只差一个缓存

很多人会觉得状态压缩 DP 的递推顺序需要额外的“拓扑感”,总要小心别把状态之间依赖顺序搞乱。其实有一种更贴合人类直觉的解法:保持回溯那种“从当前状态往后搜索”的思路,但把已经计算过的结果保存下来,避免重复搜索。这就是记忆化搜索。

代码结构和回溯几乎一样,区别在于:

  • 回溯遇到完整方案,更新全局最优。
  • 记忆化搜索则是用返回值表示“从当前状态开始,完成所有后续分配所需的最小代价”。

比如当前workerIdx等于 3,bikeMask已经占用了某 3 辆车,那么这个子问题的最优值就写到memo[mask]里。即使以后从另一条路径到达同样的 mask,也不需要重算。

4.2 为什么缓存只用一维就够

这是理解这个版本的关键。

回忆一下递归的参数:workerIdx和bikeMask。初始时workerIdx = 0,bikeMask = 0。每分配一辆车,workerIdx加 1,同时bikeMask也会多一个 1 位。所以整个递归过程始终满足:

workerIdx == Integer.bitCount(bikeMask)

因此只要给定bikeMask,当前是第几个工人其实是确定的,不需要单独作为缓存维度。这也是为什么很多高效题解只用一个memo[mask]数组。

public int assignBikesII(int[][] workers, int[][] bikes) { int n = workers.length; int m = bikes.length; int totalStates = 1 << m; int[] memo = new int[totalStates]; Arrays.fill(memo, -1); return dfs(0, 0, workers, bikes, memo); } private int dfs(int workerIdx, int bikeMask, int[][] workers, int[][] bikes, int[] memo) { if (workerIdx == workers.length) { return 0; // 所有工人都分配完了,后续代价是 0 } if (memo[bikeMask] != -1) { return memo[bikeMask]; } int best = Integer.MAX_VALUE; for (int j = 0; j < bikes.length; j++) { if ((bikeMask & (1 << j)) != 0) { continue; } int d = Math.abs(workers[workerIdx][0] - bikes[j][0]) + Math.abs(workers[workerIdx][1] - bikes[j][1]); int rest = dfs(workerIdx + 1, bikeMask | (1 << j), workers, bikes, memo); best = Math.min(best, d + rest); } memo[bikeMask] = best; return best; }

这个版本在时间复杂度和空间复杂度上和状态压缩 DP 基本一致,也同样不会重复计算同一个子问题。区别只是它是自顶向下的,天然适合那些不擅长设计递推顺序的人,也适合面试时边说思路边写。

4.3 记忆化版本还能随手加剪枝

既然它保留了搜索的外壳,你还可以在dfs开头加一个“当前累计已经超过了最优解”的分支剪枝,如果用一个全局ans来记录已知可行解的话。不过加了之后要注意:因为返回值语义变成了“剩余最小代价”,剪枝判断要放在外层函数操作累计值时使用,并不是在dfs内直接比较。

我个人的建议是:在面试中如果时间紧张,直接用记忆化搜索,思路最好讲,代码最不容易出错,缓存数组开1 << m大小,也不会爆内存。递推版留给追求极致简洁、想展示自己对 DP 熟练度的场景。

5. 复杂度与典型翻车点:提交不过时先查这几个地方

5.1 三种写法复杂度对照

写法时间复杂度空间复杂度面试推荐度
暴力回溯 + 剪枝最好情况不定,最坏约P(m, n)递归深度O(n)用作第一版破冰
状态压缩 DPO(m * 2^m)O(2^m)最优标准解
记忆化搜索状态数O(2^m),每状态尝试O(m)O(2^m)最不容易写错,强烈推荐

注意哈希版本如果没有使用数组memo,而是使用HashMap<Integer, Integer>,理论上也没有问题,但常数会稍微高一点。数据范围小,完全无所谓;数据范围一旦到 20 左右,就尽量用数组而不是哈希表。

5.2 位运算这里最容易出 bug

状态压缩核心就是位运算,看似简单,实际坑不少。

第一个常见 bug:1 << j的优先级。如果你写if ((mask & 1 << j) == 0),Java 会先算1 << j,但由于&的优先级低于==,很可能会编译报错或逻辑混乱。我建议任何时候都加上括号,写成(mask & (1 << j)) == 0,宁可多一个括号也不要省。

第二个常见 bug:把自行车和工人编号搞混。dp[mask]里的mask表示自行车的占用情况,不是工人的占用情况。如果你把bitCount(mask)和工人编号匹配错,结果完全是错的。

第三个常见 bug:对Integer.MAX_VALUE直接做加法。在状态转移里,如果dp[mask]还是无穷大,你贸然加上dist,在 Java 里会溢出变成负数,然后Math.min反而把负数当成最小值,结果全乱。因此初始化要留够余量,比如Integer.MAX_VALUE / 2。

第四个常见 bug:递归的记忆化版本中,缓存数组初始值-1和实际代价冲突吗?不会,因为曼哈顿距离是非负整数,最小代价可能是 0,所以初始化为-1是安全哨兵。如果你用0作为初始值,那么代价本来为 0 的子问题就会被误判成“还没计算过”,导致潜在错误。

5.3 自己造数据做小规模对拍

不管你是新手还是老手,做这种搜索类题目,特别建议自己写一个暴力枚举的“最朴素版本”来对拍。

朴素版本可以用递归枚举每个工人的分配,不剪枝、不记忆化,保证逻辑最简单。跑小数据时,把它和状态压缩 DP 的结果对比,如果两个结果相同,说明 DP 状态定义没问题;如果不同,就打印所有中间状态,看是哪个转移出了问题。

我在刷这题时用的自测数据结构是:

  • 工人和自行车坐标随机生成,范围在[0, 5],这样距离矩阵小,肉眼可验算。
  • 分别尝试n = 2, m = 2、n = 2, m = 3、n = 3, m = 4几组数据。
  • 每组数据都从暴力回溯和 DP 两种写法分别跑一遍,验证结果一致。

有一次就是靠随机数据发现了一个问题:我的代码里把工人总数和自行车总数写反了,导致m > n时返回的结果偏大。这种问题如果只做官方样例,根本看不出来。

6. 同一个题型还能怎么延伸:面试官换问法的应对思路

6.1 如果自行车比工人少,如何套用现有代码

原题大概率保证自行车数量不少于工人数量,但面试官可能会说“我现在只保证自行车数量小于工人”,问你能不能继续做。

这种情况下,“每个工人都分配车”不成立,就必须明确新目标。如果问题改成“尽量让更多工人骑到车,在人数最多的前提下最小化总距离”,那问题就不一样了。现有状态压缩 DP 仍然可以用,但最终更新答案的条件不再是“恰好分配 n 辆车”,而是“找到能分配的最大人数 t”,再在bitCount(mask) == t的所有状态里取最小代价。

一种更省事的方法是把状态转移改成:当前工人可以选择“不分配自行车”,作为跳过处理;然后最终通过人数优先的方式更新答案。

6.2 如果目标改成“让跑得最远的那个工人距离最小”

这是一个更常见、也更符合直觉的变体:不再求总距离最小,而是让“所有工人中距离最大的那个”尽量小。

解法也不需要完全推翻。状态压缩 DP 的框架可以保留,但转移时不再累加距离,而是用:

dp[nextMask] = min( dp[nextMask], max(dp[mask], dist[workerIdx][j]) )

这样每一个状态保存的就不是“总代价”,而是“当前分配方案中的最大单点距离”。最终答案是从所有恰好分配完的状态中取最小最大值。这个版本在面试里非常常见,理解了原题之后,扩展起来就是几分钟的事。

顺便说一句,这种“最小化最大值”的问法有时还会搭配二分答案:先猜一个最大距离上限limit,然后只允许分配距离不超过limit的自行车,再看是否存在合法方案。不过数据量小的时候,直接用上面的 DP 更简单。

6.3 大数据版本:匈牙利算法和最小费用最大流

如果把数据范围扩大,比如工人和自行车数都到了几百,状态压缩 DP 的2^m肯定会爆炸。这时候问题就变成一个标准的二分图最小权匹配,可以直接用匈牙利算法求最小权完美匹配,或者用最小费用最大流建模。

不过面试里直接问几百规模校园自行车分配的概率不高,大多数情况是考你从搜索到状态压缩的优化思路。如果面试时你能主动提一句“当 m 到 15 以上,O(m * 2^m) 会超时,这时我会考虑二分图匹配算法”,就足以证明你不是背了模板,而是真的理解复杂度的来源。

我个人实际刷这题时的体会是:状态压缩 DP 确实是为解决“多退少补”的重复子问题而生的,但如果一上来就背代码,很容易把mask的含义搞混。建议你先亲手把回溯版写一遍,再在它的基础上加缓存,让代码自己告诉你“为什么这个状态只需要一个整数”。

把这件事想通了,以后遇到类似的“小规模配对最小代价”问题,比如座位分配、任务分配、旅行商简化版,你都会条件反射地去想:这个决策集合能不能压成一个 int 的各位状态?如果能,那基本就是状态压缩 DP 没跑了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 8:27:57

TeXlyre 国际化实战:i18next + Crowdin 多语言翻译流水线全解

【免费下载链接】texlyre A local-first LaTeX & Typst web editor with real-time collaboration & offline support 项目地址&#xff1a; https://gitcode.com/gh_mirrors/te/texlyre 点击查看 免费下载 TeXlyre 国际化实战&#xff1a;本地优先的 LaTeX & Typ…

作者头像 李华
网站建设 2026/10/23 8:25:54

双 11 倒计时 15 天指挥室哨音:全链路压测熔断阈值最终校准手记

在双 11 大促的作战倒计时翻到“最后 15 天”的这一刻&#xff0c;大促作战指挥室里的键盘敲击声反而逐渐沉寂下来。各大核心战役的技术骨干们全部围坐在总指挥长桌旁&#xff0c;屏幕上投射出密密麻麻的表格与时序曲线。在正式封网前夕&#xff0c;我们迎来了一个最关乎系统生…

作者头像 李华
网站建设 2026/10/23 8:25:53

Typora正版授权、免费替代与Markdown写作配置迁移指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 8:25:29

动态规划Part06:从背包问题到建模思维的核心突破

坚持到 day33&#xff0c;动态规划 part06&#xff0c;这个进度条本身就说明你已经熬过了从 0 到 1 的阶段。前五天你在斐波那契、爬楼梯、不同路径这类线性递推题上反复练习&#xff0c;基本能闭眼写出转移方程&#xff0c;于是第六天开始接触真正的硬核主题&#xff1a;背包问…

作者头像 李华
网站建设 2026/10/23 8:23:41

Qwen3.5在昇腾NPU上部署实战:vLLM-Ascend全链路解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 8:20:58

股票买卖DP全解:从121到123题的状态设计演进

1. 121题打头阵&#xff1a;为什么买卖时机问题要拆成“持有”和“未持有”两个状态到了训练营第41天&#xff0c;动态规划的题目难度已经明显上来了。今天这三道题都是股票买卖系列&#xff0c;121是最基础的一次买入一次卖出&#xff0c;122放开了交易次数&#xff0c;123则限…

作者头像 李华