【双机位A卷】华为OD笔试之【回溯/DP】双机位A-小明减肥【Py/Java/C++/C/JS/Go六种语言】【欧弟算法】全网注释最详细分类最全的华子OD真题题解
可上 欧弟OJ系统 练习华子OD、大厂真题
绿色聊天软件戳od1441了解算法冲刺训练(备注【CSDN】否则不通过)
文章目录

相关推荐阅读
- 【华为OD机考正在更新】2025年双机位A卷真题【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】
- 【华为OD机考】2025C+2025B+2024E+D卷真题【完全原创题解 | 详细考点分类 | 不断更新题目】
- 【华为OD笔试】双机位A+2025C+2025B+2024E+D卷真题机考套题汇总【真实反馈,不断更新,限时免费】
- 【华为OD笔试】2024E+D卷命题规律解读【分析500+场OD笔试考点总结】
- 【华为OD流程】性格测试选项+注意事项】
题目练习网址:【回溯/DP】双机位A-小明减肥
题目描述与示例
题目描述
小明有n个可选运动,每个运动有对应卡路里,想选出其中k个运动且卡路里和为t。
k,t,n都是给定的。求出可行解数量。
输入描述
第一行输入 n t k,用空格进行分割
第二行输入 每个运动的卡路里 按照空格进行分割
输出描述
求出可行解数量
补充说明
0 < n < 10t > 0, 0 < k <= n- 每个运动量的卡路里 > 0
示例
输入
4 3 2
1 1 2 3
输出
2
说明
可行解为2,选取{0,2}, {1,2}两种方式。
解题思路
回溯方法
注意到本题的数据量很小,0 < n < 10。很容易想到可以用回溯来枚举所有的组合。
这些组合存在两个限制条件,一是组合的和为t,二是选择的数量为k。
那么,类似于 LeetCode39. 组合总数 的做法,我们在原先的限制条件上,多加上一个关于选择数量的条件。因此核心的回溯函数为
# 回溯算法框架
def backtrack(nums, t, k, start_idx, cur_num, cur_sum):
global ans
# 递归终止条件1
# 如果当前已经选择的运动数量等于k,并且当前消耗的总卡路里数等于t,则找到了一种方案
if cur_num == k and cur_sum == t:
ans += 1
return
# 递归终止条件2,同时也是剪枝
# 如果当前已经选择的运动数量大于k,
# 或者当前消耗的总卡路里数大于t,
# 则不可能找到满足条件的方案,直接返回
if cur_num > k or cur_sum > t:
return
# 遍历所有可选的运动
# 从start_idx开始,避免重复选择
for i in range(start_idx, len(nums)):
# 递归调用回溯函数,选择下一个运动
# 下一层递归的起始选择位置为i+1
# 当前已经选择的运动数量加1,
# 当前消耗的总卡路里数需要加上当前选择的运动消耗的卡路里数量
backtrack(nums, t, k, i+1, cur_num+1, cur_sum+nums[i])
dp方法
回溯并不是本题的最佳做法,只是因为更加套路化和模板化,显得更加简单。
如果本题的数据量更大,则回溯做法会超时。
由于本题的设问是求方法数而并非具体的组合情况,所以我们可以将其看成是一个背包问题来解决。
和常规的路径无关01背包问题相比,本题增加了所选择元素个数必须为k个的条件限制。
如果先不考虑这个条件限制,我们可以直接套用路径无关01背包的1维dp数组模板来解决这个问题。
# dp[i]表示总卡路里数为i的方法数
# dp[t]表示总卡路里数为t的方法数,为答案
dp = [0] * (t+1)
# 初始化dp[0]为1,表示总卡路里数为0的方法数为1种,即不取任何元素
dp[0] = 1
# 先正序遍历物品
for num in nums:
# 后逆序遍历背包
# 逆序遍历范围 [0, t - num] 对应的卡路里数为 pre_num 时方法数 dp[pre_num]
for pre_num in range(t - num, -1, -1):
# 考虑加上当前卡路里数num的 pre_num + num 所对应的方案数 dp[pre_num + num]
# 应该递增 dp[pre_num] 种方案数
dp[num + pre_num] += dp[pre_num]
现在将限制条件考虑上。
我们可以多设置一个维度,来表示当前一共选择了多少个元素。
即,可以使用dp[i][j]来表示选择了j个元素,总卡路里数为i的方法数。
显然,我们将修改动态转移方程为
dp[num + pre_num][j] += dp[pre_num][j-1]
即我们在已经选择了j-1个元素,总卡路里数为pre_num的基础上,多选择了num这个元素,则将前者的方法数dp[pre_num][j-1]更新到选择了j个元素,总卡路里数为num+pre_num的方法数dp[num+pre_num][j]上。
再加上关于j的循环,整体代码将修改为
# 先正序遍历物品
for num in nums:
# 后逆序遍历背包
for pre_num in range(t - num, -1, -1):
# 遍历所选择的元素个数
# 注意此处j = 0是取不到的,j最小取到1
for j in range(1, k+1):
# 考虑选择 j 个元素,加上当前卡路里数 num 的 pre_num + num 所对应的方案数 dp[pre_num + num][j]
# 应该递增 dp[pre_num][j-1] 种方案数
dp[num + pre_num][j] += dp[pre_num][j-1]
代码
解法一:回溯
Python
# 欢迎来到「欧弟算法 - 华为OD全攻略」,收录华为OD题库、面试指南、八股文与学员案例!
# 地址:https://www.odalgo.com
# 华为OD机试刷题网站:https://www.algomooc.com
# 添加微信 278166530 获取华为 OD 笔试真题题库和视频
# 题目:【回溯】2025B/2025C/双机位A-小明减肥
# 分值:100
# 作者:许老师-闭着眼睛学数理化
# 算法:回溯
# 代码看不懂的地方,请直接在群上提问
# 回溯算法框架
def backtrack(nums, t, k, start_idx, cur_num, cur_sum):
global ans
# 递归终止条件1
# 如果当前已经选择的运动数量等于k,并且当前消耗的总卡路里数等于t,则找到了一种方案
if cur_num == k and cur_sum == t:
ans += 1
return
# 递归终止条件2,同时也是剪枝
# 如果当前已经选择的运动数量大于k,
# 或者当前消耗的总卡路里数大于t,
# 则不可能找到满足条件的方案,直接返回
if cur_num > k or cur_sum > t:
return
# 遍历所有可选的运动
# 从start_idx开始,避免重复选择
for i in range(start_idx, len(nums)):
# 递归调用回溯函数,选择下一个运动
# 下一层递归的起始选择位置为i+1
# 当前已经选择的运动数量加1,
# 当前消耗的总卡路里数需要加上当前选择的运动消耗的卡路里数量
backtrack(nums, t, k, i+1, cur_num+1, cur_sum+nums[i])
# 输入运动的数量n,目标总卡路里数t,可选运动数量
n, t, k = map(int, input().split())
# 输入每种运动消耗的卡路里数量nums
nums = list(map(int, input().split()))
# 组合总数,初始化为0
ans = 0
# 调用回溯函数,从第一个运动开始选择
# 当前已经选择的运动数量为0
# 当前消耗的总卡路里数为0
backtrack(nums, t, k, 0, 0, 0)
print(ans)
Java
import java.util.*;
public class Main {
static int ans = 0; // 记录方案总数
// 回溯函数
public static void backtrack(int[] nums, int t, int k, int startIdx, int curNum, int curSum) {
// 递归终止条件1:恰好选了k个运动,且总卡路里数恰好为t
if (curNum == k && curSum == t) {
ans++;
return;
}
// 递归终止条件2:超过k个运动或总卡路里数超过t,剪枝
if (curNum > k || curSum > t) {
return;
}
// 从startIdx开始,避免重复选择
for (int i = startIdx; i < nums.length; i++) {
// 下一层递归:已选运动数+1,卡路里总和加上当前运动
backtrack(nums, t, k, i + 1, curNum + 1, curSum + nums[i]);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输入 n, t, k
int n = scanner.nextInt();
int t = scanner.nextInt();
int k = scanner.nextInt();
// 输入每个运动的卡路里消耗
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = scanner.nextInt();
}
// 从第0个运动开始,当前选择数=0,总消耗=0
backtrack(nums, t, k, 0, 0, 0);
System.out.println(ans);
}
}
C++
#include <iostream>
#include <vector>
using namespace std;
int ans = 0; // 记录方案总数
// 回溯函数
void backtrack(const vector<int>& nums, int t, int k, int startIdx, int curNum, int curSum) {
// 递归终止条件1:恰好选了k个运动,且总卡路里数恰好为t
if (curNum == k && curSum == t) {
ans++;
return;
}
// 递归终止条件2:超过k个运动或总卡路里数超过t,剪枝
if (curNum > k || curSum > t) {
return;
}
// 从startIdx开始,避免重复选择
for (int i = startIdx; i < nums.size(); i++) {
// 下一层递归:已选运动数+1,卡路里总和加上当前运动
backtrack(nums, t, k, i + 1, curNum + 1, curSum + nums[i]);
}
}
int main() {
int n, t, k;
cin >> n >> t >> k;
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 从第0个运动开始,当前选择数=0,总消耗=0
backtrack(nums, t, k, 0, 0, 0);
cout << ans << endl;
return 0;
}
C
#include <stdio.h>
// 全局变量,用于记录方案总数
int ans = 0;
// 回溯函数
void backtrack(int nums[], int n, int t, int k, int startIdx, int curNum, int curSum) {
// 递归终止条件1:恰好选了k个运动,且总卡路里数恰好为t
if (curNum == k && curSum == t) {
ans++;
return;
}
// 递归终止条件2:超过k个运动或总卡路里数超过t,剪枝
if (curNum > k || curSum > t) {
return;
}
// 从startIdx开始,避免重复选择
for (int i = startIdx; i < n; i++) {
// 下一层递归:已选运动数+1,卡路里总和加上当前运动
backtrack(nums, n, t, k, i + 1, curNum + 1, curSum + nums[i]);
}
}
int main() {
int n, t, k;
scanf("%d %d %d", &n, &t, &k);
int nums[50]; // 根据实际情况,假设最多有50个运动
for (int i = 0; i < n; i++) {
scanf("%d", &nums[i]);
}
// 从第0个运动开始,当前选择数=0,总消耗=0
backtrack(nums, n, t, k, 0, 0, 0);
printf("%d\n", ans);
return 0;
}
Node JavaScript
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let inputLines = [];
rl.on("line", function (line) {
inputLines.push(line);
}).on("close", function () {
// 解析输入
const [n, t, k] = inputLines[0].split(" ").map(Number);
const nums = inputLines[1].split(" ").map(Number);
let ans = 0; // 记录方案总数
// 回溯函数
function backtrack(startIdx, curNum, curSum) {
// 递归终止条件1:恰好选了k个运动,且总卡路里数恰好为t
if (curNum === k && curSum === t) {
ans++;
return;
}
// 递归终止条件2:超过k个运动或总卡路里数超过t,剪枝
if (curNum > k || curSum > t) {
return;
}
// 从startIdx开始,避免重复选择
for (let i = startIdx; i < nums.length; i++) {
// 下一层递归:已选运动数+1,卡路里总和加上当前运动
backtrack(i + 1, curNum + 1, curSum + nums[i]);
}
}
// 从第0个运动开始,当前选择数=0,总消耗=0
backtrack(0, 0, 0);
console.log(ans);
});
Go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
var ans int // 记录方案总数
// 回溯函数
func backtrack(nums []int, t int, k int, startIdx int, curNum int, curSum int) {
// 递归终止条件1:恰好选了k个运动,且总卡路里数恰好为t
if curNum == k && curSum == t {
ans++
return
}
// 递归终止条件2:超过k个运动或总卡路里数超过t,剪枝
if curNum > k || curSum > t {
return
}
// 从startIdx开始,避免重复选择
for i := startIdx; i < len(nums); i++ {
// 下一层递归:已选运动数+1,卡路里总和加上当前运动
backtrack(nums, t, k, i+1, curNum+1, curSum+nums[i])
}
}
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取第一行
line1, _ := reader.ReadString('\n')
line1 = strings.TrimSpace(line1)
params := strings.Split(line1, " ")
n, _ := strconv.Atoi(params[0])
t, _ := strconv.Atoi(params[1])
k, _ := strconv.Atoi(params[2])
// 读取第二行
line2, _ := reader.ReadString('\n')
line2 = strings.TrimSpace(line2)
numStrs := strings.Split(line2, " ")
nums := make([]int, n)
for i := 0; i < n; i++ {
nums[i], _ = strconv.Atoi(numStrs[i])
}
ans = 0
// 从第0个运动开始,当前选择数=0,总消耗=0
backtrack(nums, t, k, 0, 0, 0)
fmt.Println(ans)
}
时空复杂度
时间复杂度:O(k*C(n, k))。在n个元素中挑出k个元素作为组合,组合数为C(n, k),此为状态树最多的叶子节点数,而状态树的高度为k。
空间复杂度:O(1)。不考虑编译栈所占空间,仅需使用若干常数。
解法二:背包dp
Python
# 欢迎来到「欧弟算法 - 华为OD全攻略」,收录华为OD题库、面试指南、八股文与学员案例!
# 地址:https://www.odalgo.com
# 华为OD机试刷题网站:https://www.algomooc.com
# 添加微信 278166530 获取华为 OD 笔试真题题库和视频
# 题目:【回溯】2025B/2025C/双机位A-小明减肥
# 分值:100
# 作者:许老师-闭着眼睛学数理化
# 算法:背包dp
# 代码看不懂的地方,请直接在群上提问
# 输入运动的数量n,目标总卡路里数t,可选运动数量
n, t, k = map(int, input().split())
# 输入每种运动消耗的卡路里数量nums
nums = list(map(int, input().split()))
# 初始化大小为(t+1)*(k+1)的二维数组dp
# dp[i][j]表示选择了j个元素,总卡路里数为i的方法数
# dp[t][k]表示选择了k个元素,总卡路里数为t的方法数,即为答案
dp = [[0] * (k+1) for _ in range(t+1)]
# 初始化dp[0][0]为1,表示在不取任何元素的情况且总卡路里数为0的方法数,有且只有1种
dp[0][0] = 1
# 先正序遍历物品
for num in nums:
# 后逆序遍历背包
for pre_num in range(t - num, -1, -1):
# 遍历所选择的元素个数
# 注意此处j = 0是取不到的,j最小取到1
for j in range(1, k+1):
# 考虑选择 j 个元素,加上当前卡路里数 num 的 pre_num + num 所对应的方案数 dp[pre_num + num][j]
# 应该递增 dp[pre_num][j-1] 种方案数
dp[num + pre_num][j] += dp[pre_num][j-1]
# 输出答案
print(dp[t][k])
Java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 输入 n, t, k
int n = sc.nextInt();
int t = sc.nextInt();
int k = sc.nextInt();
int[] nums = new int[n];
// 输入每种运动消耗的卡路里
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
// dp[i][j] 表示选择 j 个运动,消耗总卡路里为 i 的方案数
int[][] dp = new int[t + 1][k + 1];
// 不选任何运动,总卡路里为 0,方案数为 1
dp[0][0] = 1;
// 遍历所有运动
for (int num : nums) {
// 逆序遍历背包容量,避免重复选取
for (int preSum = t - num; preSum >= 0; preSum--) {
// 遍历选择的个数
for (int j = 1; j <= k; j++) {
dp[preSum + num][j] += dp[preSum][j - 1];
}
}
}
// 输出答案
System.out.println(dp[t][k]);
}
}
C++
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n, t, k;
cin >> n >> t >> k;
vector<int> nums(n);
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// dp[i][j] 表示选择 j 个运动,总卡路里为 i 的方案数
vector<vector<int>> dp(t + 1, vector<int>(k + 1, 0));
// 初始化:不选任何运动,总卡路里为 0,方案数为 1
dp[0][0] = 1;
// 遍历所有运动
for (int num : nums) {
// 从后往前遍历,防止重复选取
for (int preSum = t - num; preSum >= 0; preSum--) {
for (int j = 1; j <= k; j++) {
dp[preSum + num][j] += dp[preSum][j - 1];
}
}
}
// 输出答案
cout << dp[t][k] << endl;
return 0;
}
C
#include <stdio.h>
#include <string.h>
#define MAX_T 1005
#define MAX_K 105
int dp[MAX_T][MAX_K]; // dp[i][j] 表示选择 j 个运动,总卡路里为 i 的方案数
int main() {
int n, t, k;
scanf("%d %d %d", &n, &t, &k);
int nums[n];
for (int i = 0; i < n; i++) {
scanf("%d", &nums[i]);
}
// 初始化,表示不选任何运动,总卡路里为 0,方案数为 1
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
// 遍历所有运动
for (int idx = 0; idx < n; idx++) {
int num = nums[idx];
// 从后往前遍历,防止重复选取
for (int preSum = t - num; preSum >= 0; preSum--) {
for (int j = 1; j <= k; j++) {
dp[preSum + num][j] += dp[preSum][j - 1];
}
}
}
// 输出答案
printf("%d\n", dp[t][k]);
return 0;
}
Node JavaScript
const readline = require("readline");
// 创建读取接口
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
let inputLines = [];
rl.on("line", (line) => {
inputLines.push(line);
});
rl.on("close", () => {
// 解析输入
const [n, t, k] = inputLines[0].split(" ").map(Number);
const nums = inputLines[1].split(" ").map(Number);
// dp[i][j] 表示选择 j 个运动,总消耗卡路里为 i 的方案数
const dp = Array.from({ length: t + 1 }, () => Array(k + 1).fill(0));
// 初始化:不选任何运动,总消耗为 0,方案数为 1
dp[0][0] = 1;
// 遍历每个运动
for (const num of nums) {
// 从后往前遍历,防止重复选择
for (let preSum = t - num; preSum >= 0; preSum--) {
for (let j = 1; j <= k; j++) {
dp[preSum + num][j] += dp[preSum][j - 1];
}
}
}
// 输出答案
console.log(dp[t][k]);
});
Go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取第一行,解析 n, t, k
line1, _ := reader.ReadString('\n')
parts := strings.Fields(line1)
n, _ := strconv.Atoi(parts[0])
t, _ := strconv.Atoi(parts[1])
k, _ := strconv.Atoi(parts[2])
// 读取第二行,解析每个运动消耗的卡路里
line2, _ := reader.ReadString('\n')
numStrs := strings.Fields(line2)
nums := make([]int, n)
for i := 0; i < n; i++ {
nums[i], _ = strconv.Atoi(numStrs[i])
}
// dp[i][j] 表示选择 j 个运动,总卡路里为 i 的方案数
dp := make([][]int, t+1)
for i := range dp {
dp[i] = make([]int, k+1)
}
// 初始化:不选任何运动,总卡路里为 0,方案数为 1
dp[0][0] = 1
// 遍历所有运动
for _, num := range nums {
// 从后往前遍历防止重复选择
for preSum := t - num; preSum >= 0; preSum-- {
for j := 1; j <= k; j++ {
dp[preSum+num][j] += dp[preSum][j-1]
}
}
}
// 输出答案
fmt.Println(dp[t][k])
}
时空复杂度
时间复杂度:O(nkt)。三重循环所需时间复杂度
空间复杂度:O(tk)。dp数组所占空间。
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华子OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务1000+同学成功上岸!
-
课程讲师为全网200w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
90+天陪伴式学习,100+直播课时,300+动画图解视频,500+LeetCode经典题,500+华为OD真题/大厂真题,还有简历修改、模拟面试、陪伴小群、资深HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接OD真题汇总(持续更新)
-
绿色聊天软件戳
od1441或了解更多
更多推荐


所有评论(0)