
本文详解如何在由 2 的幂构成的服务器数组中,找出和恰好等于目标负载的最少服务器数量;提供递归回溯实现、关键边界处理逻辑,并指出原始思路中暴力双层循环的局限性。
本文详解如何在由 2 的幂构成的服务器数组中,找出和恰好等于目标负载的最少服务器数量;提供递归回溯实现、关键边界处理逻辑,并指出原始思路中暴力双层循环的局限性。
该问题本质是 带计数约束的子集和最小化问题(Minimum Subset Sum Count):给定一个正整数 expected_load 和一个元素均为 2 的非负整数次幂(即 [1, 1, 2, 4, 8, 16, ...])的数组 servers,要求选出最少数量的元素(可重复?不可重复?——根据题干“indeces”及示例 [1,1,2,4,8,16] 中含两个 1,结合典型面试设定,此处应为每个索引至多用一次,即「0-1 背包式」选择),使其和恰好等于 expected_load;若不存在合法组合,返回 -1。
⚠️ 注意:原始尝试中的双重 for 循环仅枚举长度为 2 的子数组,无法覆盖使用 1 个、3 个或更多服务器的情况(例如 expected_load = 7 需要 1+2+4 共 3 台),因此必须升级为全子集搜索 + 最优计数剪枝。
✅ 正确解法:记忆化递归(DFS + 状态压缩)
我们定义状态 dp(i, remain) 表示:从索引 i 开始到末尾,在剩余需求为 remain 的前提下,达成精确匹配所需的最少服务器数量。
-
状态转移:
-
选第 i 个服务器:若
servers[i] ≤ remain,则dp(i, remain) = 1 + dp(i + 1, remain - servers[i]) -
不选第 i 个服务器:
dp(i, remain) = dp(i + 1, remain) - 取二者最小值(需妥善处理
-1不可达状态)
-
选第 i 个服务器:若
-
边界条件:
-
remain === 0→ 找到解,返回0(无需再选) remain 或 <code>i === servers.length→ 无效路径,返回-1
-
? 为什么不用贪心?虽然数组含 2 的幂,看似可用“从大到小贪心选取”,但注意题干明确给出
[1, 1, 2, 4, 8, 16](含重复1),说明输入不保证严格升序/无重,且贪心无法保证全局最优计数(例如load=3,arr=[1,1,2]:贪心选2+1(2台),但最优是1+1+1?不成立——因数组无三1;本例中1+2确实最优。但若load=3,arr=[1,1,1,2],贪心仍得2+1=2台,而1+1+1=3更差;所以贪心可行。但题目未限定数组有序或无重,最稳妥仍是通用 DP/DFS)。
以下是优化后的完整实现(含记忆化避免重复计算,时间复杂度 O(n × load),空间 O(n × load)):
function getMinServers(expected_load, servers) {
const n = servers.length;
// memo[i][remain] 表示从索引 i 开始凑出 remain 所需最小服务器数
const memo = new Map();
function dfs(i, remain) {
if (remain === 0) return 0;
if (remain = n) return -1;
const key = `${i},${remain}`;
if (memo.has(key)) return memo.get(key);
// 选 servers[i]
const take = dfs(i + 1, remain - servers[i]);
// 不选 servers[i]
const skip = dfs(i + 1, remain);
let res;
if (take === -1 && skip === -1) {
res = -1;
} else if (take === -1) {
res = skip;
} else if (skip === -1) {
res = take + 1;
} else {
res = Math.min(take + 1, skip);
}
memo.set(key, res);
return res;
}
return dfs(0, expected_load);
}
// 测试用例
console.log(getMinServers(10, [1, 1, 2, 4, 8, 16])); // 输出: 2 (4 + 8 = 12 ❌;等等!10 = 2 + 8 → 索引2+索引4 → 2台 ✓)
console.log(getMinServers(7, [1, 2, 4])); // 输出: 3 (1+2+4)
console.log(getMinServers(5, [2, 4])); // 输出: -1 (无法凑出5)
✅ 关键修正点说明:
- 原答案中
sort((a,b) => b-a)并非必需(甚至可能误导):降序排序对 DFS 正确性无影响,但不改变最坏时间复杂度;且若数组已含重复值(如两个1),排序后仍需考虑所有索引组合,故保留原始顺序更直观。 - 原实现中
include = findMinServers(load - _array[index], index)使用了同一索引可重复选取(即完全背包),但题干强调 “indeces”(复数索引)及示例[1,1,2,4,8,16]暗示每个位置独立,应为0-1 背包,故include分支应为findMinServers(load - _array[index], index + 1)。 - 实际运行
getMinServers(10, [1,1,2,4,8,16]):可行解有2+8=10(2台)、1+1+8=10(3台)、1+1+2+4+2?无效——故最小为 2,结果正确。
? 总结与进阶建议
-
核心洞察:这是经典的「最小硬币数目」变种(Coin Change Problem),其中“硬币面额”为
servers,“总金额”为expected_load,目标是最小硬币数。 -
时间优化方向:当
expected_load较大(如 > 1e6)时,DFS+memo 可能栈溢出或超时,此时应改用自底向上动态规划或 BFS(按使用服务器数量分层扩展),确保首次到达remain === 0时即为最小数量。 -
空间优化提示:由于只依赖
dp[i+1][*],可将二维 DP 压缩为一维滚动数组。 - 面试表达重点:先明确问题模型(0-1 子集和 + 最小基数),再对比暴力/贪心/DP 的适用性,最后给出可读、健壮、带注释的代码。
掌握此题,即打通了子集优化类问题的通用解题链路:建模 → 状态设计 → 边界定义 → 转移方程 → 实现与验证。










