1. 砝码称重问题解析
这道题目来自华为的编程机考,属于典型的组合数学与动态规划结合的问题。我们需要计算给定一组不同重量和数量的砝码,能够称出多少种不同的重量(包括0)。这个问题看似简单,但蕴含着几个关键算法思想。
1.1 问题本质理解
砝码称重问题的核心在于:给定n种砝码,每种砝码有特定的重量和数量限制,我们需要找出所有可能的重量组合。这里的"组合"指的是每种砝码可以取0到最大数量之间的任意整数个,然后将这些砝码的重量相加得到的总和。
举个例子,如果有2种砝码:
- 重量1的砝码有2个
- 重量2的砝码有1个
那么可能的组合方式有:
- 不取任何砝码(0)
- 取1个重量1(1)
- 取2个重量1(2)
- 取1个重量1和1个重量2(3)
- 取2个重量1和1个重量2(4)
因此总共有5种不同的重量。
1.2 算法选择思路
这个问题有几种可能的解法:
-
暴力枚举法:对于每种砝码,遍历其所有可能的数量(从0到最大数量),然后计算所有组合的总和。这种方法在小规模数据时可行,但当砝码种类增多或数量增大时,时间复杂度会急剧上升。
-
动态规划法:这是更优的解决方案。我们可以维护一个集合,记录当前能够称出的所有重量,然后对于每种砝码,将其所有可能的重量组合加入到集合中。
-
数学生成函数法:理论上可以用生成函数来表示所有可能的组合,但对于编程实现来说不如动态规划直观。
在实际编程中,动态规划是最常用且高效的解决方案,特别是考虑到题目中n的范围较小(1≤n≤10)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划解决方案详解
2.1 基本思路
动态规划解决这个问题的核心思想是:逐步构建能够称出的重量集合。初始时,集合中只有0(不取任何砝码)。然后对于每种砝码,我们将其所有可能的数量(从1到最大数量)与当前集合中的每个重量相加,得到新的重量,并将这些新重量加入到集合中。
具体步骤:
- 初始化一个包含0的集合result
- 对于每种砝码:
- 对于该砝码的每个可能数量(从1到最大数量):
- 对于当前result集合中的每个重量:
- 计算新重量 = 当前重量 + 砝码重量 × 数量
- 将新重量加入到临时集合中
- 对于当前result集合中的每个重量:
- 将临时集合合并到result中
- 对于该砝码的每个可能数量(从1到最大数量):
- 最终result集合的大小就是答案
2.2 代码实现
以下是使用JavaScript的实现代码:
javascript复制function countDifferentWeights(n, weights, counts) {
let result = new Set([0]); // 初始时只有0
for (let i = 0; i < n; i++) {
const currentWeight = weights[i];
const currentCount = counts[i];
const temp = new Set(result); // 复制当前结果
for (let j = 1; j <= currentCount; j++) {
for (const val of result) {
temp.add(val + currentWeight * j);
}
}
// 合并结果
for (const val of temp) {
result.add(val);
}
}
return result.size;
}
// 示例测试
console.log(countDifferentWeights(2, [1, 2], [2, 1])); // 输出5
2.3 代码解析
-
初始化:我们使用JavaScript的Set来存储能够称出的重量,因为它自动处理重复值。初始时集合中只有0。
-
三重循环:
- 外层循环遍历每种砝码
- 中层循环遍历该砝码的每个可能数量(从1到最大数量)
- 内层循环遍历当前结果集合中的每个重量,计算新重量
-
合并结果:每次处理完一种砝码的所有数量后,将临时集合合并到主结果集合中。
-
返回结果:最终集合的大小就是不同重量的数量。
注意:在实际编程题中,需要处理输入输出格式。上面的函数假设参数已经正确解析。
3. 算法优化与边界情况
3.1 性能优化
上述基础实现已经能够解决问题,但在某些情况下可以进行优化:
-
减少重复计算:在添加新重量时,可以一次性计算该砝码所有可能数量产生的新重量,而不是逐个数量处理。
-
提前终止:如果某种砝码的重量为0(虽然题目中规定1≤mi≤2000,不会出现),或者数量为0,可以跳过处理。
优化后的实现:
javascript复制function countDifferentWeightsOptimized(n, weights, counts) {
let result = new Set([0]);
for (let i = 0; i < n; i++) {
const currentWeight = weights[i];
const currentCount = counts[i];
const temp = new Set();
for (const val of result) {
for (let j = 0; j <= currentCount; j++) {
temp.add(val + currentWeight * j);
}
}
result = temp;
}
return result.size;
}
3.2 边界情况处理
在实际编程题中,需要考虑以下边界情况:
-
n=1:只有一种砝码时,结果就是该砝码的数量+1(包括0)。
-
所有砝码数量为1:这时问题退化为子集和问题,结果是2^n(包括空集)。
-
大重量砝码:虽然单个砝码重量可达2000,但n≤10且每种砝码数量≤10,所以最大总重量是10×2000×10=200,000,这在现代计算机内存中完全可以处理。
-
重复重量:题目说明重量互不相等,所以无需处理重复重量情况。
4. 实际编程中的注意事项
4.1 输入处理
在华为机考等编程平台中,需要正确处理输入格式。对于JavaScript(Node.js)环境,典型的输入处理如下:
javascript复制const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let lines = [];
rl.on('line', (line) => {
lines.push(line);
if (lines.length === 3) {
const n = parseInt(lines[0]);
const weights = lines[1].split(' ').map(Number);
const counts = lines[2].split(' ').map(Number);
console.log(countDifferentWeights(n, weights, counts));
rl.close();
}
});
4.2 调试技巧
在解决这类问题时,可以添加调试输出:
javascript复制function countDifferentWeightsDebug(n, weights, counts) {
let result = new Set([0]);
console.log("初始结果:", [...result]);
for (let i = 0; i < n; i++) {
console.log(`处理第${i+1}种砝码,重量:${weights[i]},数量:${counts[i]}`);
const currentWeight = weights[i];
const currentCount = counts[i];
const temp = new Set();
for (const val of result) {
for (let j = 0; j <= currentCount; j++) {
const newVal = val + currentWeight * j;
temp.add(newVal);
console.log(` 当前值:${val},取${j}个,新值:${newVal}`);
}
}
result = temp;
console.log("当前结果:", [...result]);
}
return result.size;
}
4.3 常见错误
-
忘记包含0:题目明确要求包括重量0,即不取任何砝码的情况。
-
重复计算:如果没有使用Set等自动去重的数据结构,可能会重复计算相同的重量。
-
数量边界:在处理砝码数量时,要注意是从0到最大数量都包含,还是从1开始。
-
大数处理:虽然JavaScript的Number可以处理大整数,但在其他语言中可能需要考虑整数溢出问题。
5. 算法复杂度分析
让我们分析一下这个算法的时间和空间复杂度:
-
时间复杂度:
- 最外层循环:n次(砝码种类)
- 对于每种砝码,遍历当前结果集合,假设当前集合大小为S
- 对于每个重量,遍历该砝码的所有可能数量(最多10次)
- 因此,总时间复杂度约为O(n×S×10)
- 在最坏情况下,S可以达到Π(xi+1),即所有砝码数量+1的乘积
-
空间复杂度:
- 主要取决于结果集合的大小
- 最坏情况下需要存储所有可能的重量组合
- 因此空间复杂度也是O(Π(xi+1))
对于题目给定的约束条件(n≤10,xi≤10),这个复杂度是完全可接受的。
6. 扩展思考
这个问题可以有多种变体,理解基础解法后,可以尝试解决以下变体问题:
-
砝码分放天平两侧:这时每个砝码有三种选择:不放、放左侧、放右侧。问题会变得更加复杂。
-
限制总重量:给定一个上限,问不超过该上限的重量有多少种称法。
-
找出具体的称重方案:不仅计算有多少种重量,还要输出每种重量对应的砝码组合。
-
砝码重量可能相同:如果题目不保证砝码重量互不相同,算法需要做何调整?
在实际面试或机考中,理解问题本质并能够根据约束条件选择合适的算法,比死记硬背模板更重要。这道题目很好地考察了对动态规划思想的理解和应用能力。
