1. 项目背景与核心需求
华为OD机考双机位C卷中的"组装新的数组"题目,是典型的数据结构与算法应用场景。这道题考察候选人在限定条件下对数组元素的组合与筛选能力,需要同时兼顾时间复杂度与空间复杂度的优化。作为机考高频题型,它融合了排列组合、剪枝优化等核心算法思想。
在实际开发中,类似场景广泛存在于商品组合推荐、资源调度优化等业务领域。例如电商平台根据用户预算推荐商品组合,或云计算资源池中选择最优虚拟机配置方案。题目要求用Java/Python/JS/C++/Go五种语言实现,体现了华为对开发者多语言适配能力的要求。
2. 题目解析与算法设计
2.1 题目描述还原
根据"组装新的数组"的标题特征和华为OD机考命题规律,题目大概率描述如下:
给定一个正整数数组nums和目标值target,要求找出所有满足以下条件的子数组:
- 子数组元素和不超过target
- 至少包含一个元素
- 元素可重复选取(完全背包问题变种)
示例:
输入:nums = [2,3,5], target = 8
输出:[[2],[3],[5],[2,2,2],[2,3],[3,2],[2,2,3],[3,3],[2,5],[5,2],[3,5],[5,3]]
2.2 核心算法选择
采用回溯算法框架,配合剪枝优化:
python复制def combinationSum(nums, target):
res = []
nums.sort() # 排序便于剪枝
def backtrack(start, path, remain):
if remain == 0:
res.append(path.copy())
return
for i in range(start, len(nums)):
if nums[i] > remain: # 剪枝
break
path.append(nums[i])
backtrack(i, path, remain - nums[i]) # 可重复选取
path.pop()
backtrack(0, [], target)
return res
关键优化点:
- 先排序数组,当剩余值小于当前元素时提前终止循环
- 使用start参数避免重复组合(如[2,3]和[3,2]视为不同解)
- 通过remain参数替代实时计算sum(path)
3. 多语言实现对比
3.1 Java实现要点
java复制public List<List<Integer>> combinationSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
backtrack(nums, target, 0, new ArrayList<>(), res);
return res;
}
private void backtrack(int[] nums, int remain, int start,
List<Integer> path, List<List<Integer>> res) {
if (remain == 0) {
res.add(new ArrayList<>(path));
return;
}
for (int i = start; i < nums.length; i++) {
if (nums[i] > remain) break;
path.add(nums[i]);
backtrack(nums, remain - nums[i], i, path, res);
path.remove(path.size() - 1);
}
}
注意事项:
- 使用ArrayList而非LinkedList提升随机访问性能
- 注意集合的深拷贝(new ArrayList<>(path))
- 基本类型数组与集合的类型转换
3.2 Python实现差异
python复制def combinationSum(nums, target):
nums.sort()
res = []
def dfs(start, path, remain):
if remain == 0:
res.append(path[:]) # 切片复制
return
for i in range(start, len(nums)):
if nums[i] > remain:
break
dfs(i, path + [nums[i]], remain - nums[i]) # 直接传新列表
dfs(0, [], target)
return res
语言特性利用:
- 列表切片实现浅拷贝
- 动态类型省略泛型声明
- 闭包直接访问外部res变量
3.3 JavaScript实现技巧
javascript复制function combinationSum(nums, target) {
nums.sort((a,b) => a-b);
const res = [];
function backtrack(start, path, remain) {
if (remain === 0) {
res.push([...path]); // 扩展运算符复制
return;
}
for (let i = start; i < nums.length; i++) {
if (nums[i] > remain) break;
path.push(nums[i]);
backtrack(i, path, remain - nums[i]);
path.pop();
}
}
backtrack(0, [], target);
return res;
}
特殊处理:
- 数值排序需要显式比较函数
- 扩展运算符实现数组复制
- 严格相等判断(===)避免类型转换
3.4 C++实现优化
cpp复制vector<vector<int>> combinationSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
vector<int> path;
function<void(int, int)> backtrack = [&](int start, int remain) {
if (remain == 0) {
res.push_back(path);
return;
}
for (int i = start; i < nums.size(); ++i) {
if (nums[i] > remain) break;
path.push_back(nums[i]);
backtrack(i, remain - nums[i]);
path.pop_back();
}
};
backtrack(0, target);
return res;
}
性能考量:
- 使用lambda表达式捕获外部变量
- 引用传递避免不必要的拷贝
- vector的push_back/pop_back操作
3.5 Go实现特点
go复制func combinationSum(nums []int, target int) [][]int {
sort.Ints(nums)
res := [][]int{}
var backtrack func(start int, path []int, remain int)
backtrack = func(start int, path []int, remain int) {
if remain == 0 {
tmp := make([]int, len(path))
copy(tmp, path)
res = append(res, tmp)
return
}
for i := start; i < len(nums); i++ {
if nums[i] > remain {
break
}
backtrack(i, append(path, nums[i]), remain - nums[i])
}
}
backtrack(0, []int{}, target)
return res
}
特殊处理:
- 需要显式复制切片(引用类型)
- 闭包函数需先声明后定义
- 强类型系统要求严格类型匹配
4. 双机位考试环境下的实战技巧
4.1 代码规范建议
- 统一使用蛇形命名法(Python/JS)或驼峰命名法(Java/C++/Go)
- 添加必要的注释说明算法思路
- 每个语言模块保持独立可运行
4.2 调试技巧
- 打印关键变量值(如remain、path)
- 添加测试用例验证边界条件
- 使用IDE的调试功能单步执行
4.3 性能优化checklist
- 数组是否预先排序
- 剪枝条件是否正确实现
- 容器操作是否避免频繁扩容
- 递归深度是否可控(target/min(nums))
5. 常见问题与解决方案
5.1 结果去重问题
错误现象:[2,3]和[3,2]被视为不同解
解决方法:传递start参数保持选取顺序
5.2 内存溢出问题
错误现象:大target导致递归栈溢出
优化方案:改为迭代实现+备忘录
java复制// 迭代版示例
public List<List<Integer>> combinationSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>>[] dp = new ArrayList[target + 1];
dp[0] = new ArrayList<>();
dp[0].add(new ArrayList<>());
for (int num : nums) {
for (int t = num; t <= target; t++) {
if (dp[t - num] != null) {
if (dp[t] == null) {
dp[t] = new ArrayList<>();
}
for (List<Integer> list : dp[t - num]) {
List<Integer> newList = new ArrayList<>(list);
newList.add(num);
dp[t].add(newList);
}
}
}
}
return dp[target] == null ? new ArrayList<>() : dp[target];
}
5.3 语言特性陷阱
- Java:注意自动装箱/拆箱性能损耗
- Python:列表复制使用path[:]而非list(path)
- Go:切片操作可能修改底层数组
- C++:vector的reserve预分配空间
- JS:==与===的类型转换差异
6. 复杂度分析与扩展
6.1 时间复杂度
- 最坏情况:O(n^target)(每个元素取多次)
- 平均情况:O(2^n)(通过剪枝优化)
6.2 空间复杂度
- 递归栈:O(target/min(nums))
- 结果存储:O(n * k)(k为解的数量)
6.3 题目变种
- 限制元素选取次数(0-1背包)
- 要求解按字典序输出
- 组合长度限制(如最多选5个元素)
- 浮点数目标值处理
python复制# 变种1:元素不可重复使用
def combinationSum2(nums, target):
nums.sort()
res = []
def backtrack(start, path, remain):
if remain == 0:
res.append(path[:])
return
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]: # 去重
continue
if nums[i] > remain:
break
backtrack(i+1, path + [nums[i]], remain - nums[i]) # i+1不可重复
backtrack(0, [], target)
return res
