一、问题描述
给定一个无重复元素的数组 arr
和一个目标数 target
,找出 arr
中所有可以使数字和为 target
的组合。arr
中的数字可以无限制重复被选取。
例如:
-
输入:
arr = [2, 3, 6, 7]
,target = 7
-
输出:
2 2 3 7
二、代码实现
以下是实现组合总和问题的C语言代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <math.h>
#include <ctype.h>
#include <limits.h>
int n, target, k = 0;
int arr[10000] = { 0 };
int path[10000];
// qsort比较函数从小到大排序,用于快速剪枝
//比如,如果当前的总和加上当前元素已经超过target
//那么后面的更大的元素就不需要再考虑了。这可以剪枝。
void cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
// 回溯函数
void back(int start, int sum) {
// 如果当前路径的和等于目标值,输出路径
if (sum == target) {
for (int i = 0; i < k; i++) {
printf("%d ", path[i]);
}
printf("\n");
return;
}
// 如果当前路径的和已经超过目标值,直接返回
if (sum > target) {
return;
}
// 从 start 开始遍历数组,避免重复
for (int i = start; i < n; i++) {
if (sum + arr[i] > target) { // 剪枝:如果当前值已经超过目标值,跳过
continue;
}
path[k] = arr[i]; // 将当前值加入路径
k++;
back(i, sum + arr[i]); // 递归调用,允许重复使用相同值
k--; // 回溯,撤销当前选择
}
}
int main() {
// 输入数组大小和目标值
scanf("%d %d", &n, &target);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]); // 输入数组元素
}
qsort(arr, n, sizeof(int), cmp); // 对数组进行排序
back(0, 0); // 从索引 0 开始,当前路径和为 0
return 0;
}
三、代码解析
1. 核心思想
组合总和问题的核心在于递归和回溯。递归用于深度优先搜索,回溯用于撤销当前选择,尝试其他可能性。具体逻辑如下:
-
使用一个数组
path
来存储当前的组合路径。 -
使用
sum
来记录当前路径的和。 -
当
sum
等于目标值target
时,输出当前路径。 -
当
sum
超过目标值时,直接返回,避免无效的递归。 -
在每一层递归中,尝试从当前索引开始的所有可能值,并允许重复使用相同值。
2. 关键点解析
-
排序优化:通过
qsort
对数组进行排序,可以提前剪枝,避免无效的递归调用。 -
剪枝操作:如果当前值加上
sum
已经超过目标值,则跳过后续所有值。 -
回溯操作:在每次递归返回时,撤销当前选择,恢复路径状态。
递归树示意图
数组 [2, 3, 6, 7]
和目标值 target = 7
的递归树结构示意图。
[0, 0] (sum=0)
/ | | \
/ | | \
[2, 0] [3, 0] [6, 0] [7, 0]
(sum=2) (sum=3) (sum=6) (sum=7)
/ | | |
/ | | |
[2, 2] [2, 3] [3, 3] [7] ✔️
(sum=4) (sum=5) (sum=6) (sum=7)
/ | | |
/ | | |
[2, 2, 2] [2, 2, 3] [3, 3, 3]
(sum=6) (sum=7) ✔️ (sum=9) ✕
/ | |
/ | |
[2, 2, 2, 2] [2, 2, 3]
(sum=8) ✕ (sum=7) ✔️
然后再来看看
组合总和问题与全排列问题的异同
相同点
-
递归和回溯:
-
组合总和问题和全排列问题都使用了递归和回溯的思想。通过递归搜索所有可能的解,并在找到一个解后回溯,尝试其他可能性。
-
两者都依赖于深度优先搜索(DFS)来探索所有可能的路径。
-
-
数组排序:
-
在某些情况下,两者都可能需要对数组进行排序。排序可以帮助优化算法性能,例如通过剪枝减少不必要的递归调用。
-
-
路径记录:
-
两者都使用了一个路径数组(如
path
)来记录当前的解路径,并在递归过程中不断更新这个路径。
-
不同点
-
问题目标:
-
全排列问题:目标是生成一个数组的所有可能排列。每个排列是数组元素的一种重新排列,且每个元素在每个排列中只能出现一次。
-
组合总和问题:目标是找到所有可能的组合,使得这些组合的和等于目标值。数组中的元素可以重复使用,但组合本身不能重复。
-
-
递归逻辑:
-
全排列问题:递归时需要遍历所有未使用的元素,并将它们加入当前排列。递归完成后回溯,撤销当前选择。
-
组合总和问题:递归时从当前索引开始,允许重复使用相同值。如果当前路径的和超过目标值,则直接返回。
-
-
剪枝操作:
-
全排列问题:通常不需要剪枝,因为每个排列都是合法的。
-
组合总和问题:可以通过排序和剪枝优化。如果当前路径的和已经超过目标值,或者加上某个值后超过目标值,则可以直接跳过。
-
-
输出结果:
-
全排列问题:输出所有可能的排列,每个排列包含数组的所有元素。
-
组合总和问题:输出所有可能的组合,每个组合的和等于目标值。
-